machinaos 0.0.76 → 0.0.78
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/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -1,25 +1,30 @@
|
|
|
1
|
+
"""Google Workspace OAuth 2.0 (composition pattern -- Wave 11.I, S.2).
|
|
2
|
+
|
|
3
|
+
Composes around ``google_auth_oauthlib.flow.Flow`` rather than
|
|
4
|
+
subclassing :class:`services.plugin.oauth.OAuth2PKCEClient`. The Flow
|
|
5
|
+
library handles the PKCE flow, scope handling, and offline-access
|
|
6
|
+
mechanics that Google's token endpoint expects -- hand-rolling those
|
|
7
|
+
would lose the ``OAUTHLIB_RELAX_TOKEN_SCOPE=1`` workaround for
|
|
8
|
+
oauthlib upstream issue #562.
|
|
9
|
+
|
|
10
|
+
What we DO share with the Twitter (subclass) path:
|
|
11
|
+
|
|
12
|
+
* :class:`OAuthStateStore` from :mod:`services.plugin.oauth` --
|
|
13
|
+
identical TTL + cleanup contract, deduplicates the state dict +
|
|
14
|
+
``cleanup_expired_states`` helper that pre-S lived here too.
|
|
15
|
+
* The async method shape (``async exchange_code``,
|
|
16
|
+
``async fetch_user_info``, ``async refresh_access_token``,
|
|
17
|
+
``async revoke_token``) consumed by
|
|
18
|
+
:func:`services.events.oauth_lifecycle.make_oauth_lifecycle_handlers`
|
|
19
|
+
and :func:`make_oauth_callback_router`. Sync calls into Flow /
|
|
20
|
+
Credentials wrap through ``asyncio.to_thread``.
|
|
1
21
|
"""
|
|
2
|
-
Google Workspace OAuth 2.0 using google-auth-oauthlib library.
|
|
3
|
-
|
|
4
|
-
Unified OAuth for all Google services:
|
|
5
|
-
- Gmail (send, search, read emails)
|
|
6
|
-
- Google Calendar (create, list, update, delete events)
|
|
7
|
-
- Google Drive (upload, download, list, share files)
|
|
8
|
-
- Google Sheets (read, write, append data)
|
|
9
|
-
- Google Tasks (create, list, complete tasks)
|
|
10
|
-
- Google Contacts (create, list, search contacts)
|
|
11
|
-
|
|
12
|
-
Two access modes:
|
|
13
|
-
1. Owner Mode - Your own Google account (Credentials Modal)
|
|
14
|
-
2. Customer Mode - Customer's Google account (database storage)
|
|
15
|
-
|
|
16
|
-
API endpoints loaded from config/google_apis.json
|
|
17
|
-
Docs: https://developers.google.com/identity/protocols/oauth2
|
|
18
|
-
"""
|
|
19
22
|
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
20
26
|
import json
|
|
21
27
|
import os
|
|
22
|
-
import time
|
|
23
28
|
import warnings
|
|
24
29
|
from pathlib import Path
|
|
25
30
|
from typing import Any, Dict, List, Optional
|
|
@@ -44,19 +49,28 @@ os.environ.setdefault("OAUTHLIB_RELAX_TOKEN_SCOPE", "1")
|
|
|
44
49
|
# errors, deprecation notices) keeps surfacing.
|
|
45
50
|
warnings.filterwarnings("ignore", message=r"Scope has changed.*")
|
|
46
51
|
|
|
52
|
+
import httpx
|
|
47
53
|
from google.auth.transport.requests import Request
|
|
48
54
|
from google.oauth2.credentials import Credentials
|
|
49
55
|
from google_auth_oauthlib.flow import Flow
|
|
50
56
|
from googleapiclient.discovery import build
|
|
51
57
|
|
|
52
58
|
from core.logging import get_logger
|
|
59
|
+
from services.plugin.oauth import OAuthStateStore
|
|
53
60
|
|
|
54
61
|
logger = get_logger(__name__)
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
|
|
64
|
+
# ============================================================================
|
|
65
|
+
# Config loaders (consumed by _auth_helper, oauth_utils, _credentials, tests)
|
|
66
|
+
# ============================================================================
|
|
67
|
+
|
|
68
|
+
# Walk up two parents from server/nodes/google/_oauth.py -> server/, then
|
|
69
|
+
# into config/google_apis.json.
|
|
70
|
+
_config_path = Path(__file__).resolve().parents[2] / "config" / "google_apis.json"
|
|
58
71
|
_google_config: Dict[str, Any] = {}
|
|
59
72
|
|
|
73
|
+
|
|
60
74
|
def _load_config() -> Dict[str, Any]:
|
|
61
75
|
"""Load Google API config from JSON file."""
|
|
62
76
|
global _google_config
|
|
@@ -70,33 +84,53 @@ def _load_config() -> Dict[str, Any]:
|
|
|
70
84
|
_google_config = _get_default_config()
|
|
71
85
|
return _google_config
|
|
72
86
|
|
|
87
|
+
|
|
73
88
|
def _get_default_config() -> Dict[str, Any]:
|
|
74
|
-
"""
|
|
89
|
+
"""Default config if JSON load fails."""
|
|
75
90
|
return {
|
|
76
91
|
"oauth": {
|
|
77
92
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
78
93
|
"token_uri": "https://oauth2.googleapis.com/token",
|
|
79
94
|
"revoke_uri": "https://oauth2.googleapis.com/revoke",
|
|
80
|
-
"userinfo_uri": "https://www.googleapis.com/oauth2/v2/userinfo"
|
|
95
|
+
"userinfo_uri": "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
81
96
|
},
|
|
82
97
|
"scopes": {
|
|
83
|
-
"userinfo": [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
"userinfo": [
|
|
99
|
+
"openid",
|
|
100
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
101
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
102
|
+
],
|
|
103
|
+
"gmail": [
|
|
104
|
+
"https://www.googleapis.com/auth/gmail.send",
|
|
105
|
+
"https://www.googleapis.com/auth/gmail.readonly",
|
|
106
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
107
|
+
],
|
|
108
|
+
"calendar": [
|
|
109
|
+
"https://www.googleapis.com/auth/calendar",
|
|
110
|
+
"https://www.googleapis.com/auth/calendar.events",
|
|
111
|
+
],
|
|
112
|
+
"drive": [
|
|
113
|
+
"https://www.googleapis.com/auth/drive",
|
|
114
|
+
"https://www.googleapis.com/auth/drive.file",
|
|
115
|
+
],
|
|
87
116
|
"sheets": ["https://www.googleapis.com/auth/spreadsheets"],
|
|
88
117
|
"tasks": ["https://www.googleapis.com/auth/tasks"],
|
|
89
|
-
"contacts": [
|
|
90
|
-
|
|
118
|
+
"contacts": [
|
|
119
|
+
"https://www.googleapis.com/auth/contacts",
|
|
120
|
+
"https://www.googleapis.com/auth/contacts.readonly",
|
|
121
|
+
],
|
|
122
|
+
},
|
|
91
123
|
}
|
|
92
124
|
|
|
125
|
+
|
|
93
126
|
def get_oauth_endpoints() -> Dict[str, str]:
|
|
94
|
-
"""
|
|
127
|
+
"""OAuth endpoint URLs from config."""
|
|
95
128
|
config = _load_config()
|
|
96
129
|
return config.get("oauth", _get_default_config()["oauth"])
|
|
97
130
|
|
|
131
|
+
|
|
98
132
|
def get_callback_paths() -> Dict[str, str]:
|
|
99
|
-
"""
|
|
133
|
+
"""OAuth callback paths from config."""
|
|
100
134
|
config = _load_config()
|
|
101
135
|
oauth = config.get("oauth", {})
|
|
102
136
|
return {
|
|
@@ -106,63 +140,70 @@ def get_callback_paths() -> Dict[str, str]:
|
|
|
106
140
|
|
|
107
141
|
|
|
108
142
|
def get_service_config(service: str) -> Dict[str, Any]:
|
|
109
|
-
"""
|
|
143
|
+
"""Service-specific config (base_url, version, etc.)."""
|
|
110
144
|
config = _load_config()
|
|
111
145
|
return config.get("services", {}).get(service, {})
|
|
112
146
|
|
|
147
|
+
|
|
113
148
|
def get_all_scopes() -> List[str]:
|
|
114
|
-
"""
|
|
149
|
+
"""Combined scopes for all Google Workspace services."""
|
|
115
150
|
config = _load_config()
|
|
116
151
|
scopes_config = config.get("scopes", _get_default_config()["scopes"])
|
|
117
152
|
all_scopes = []
|
|
118
153
|
for scope_list in scopes_config.values():
|
|
119
154
|
all_scopes.extend(scope_list)
|
|
120
|
-
return list(dict.fromkeys(all_scopes)) #
|
|
155
|
+
return list(dict.fromkeys(all_scopes)) # dedupe, preserve order
|
|
156
|
+
|
|
121
157
|
|
|
122
158
|
def get_scopes_for_services(services: List[str]) -> List[str]:
|
|
123
|
-
"""
|
|
159
|
+
"""Scopes for specific services only."""
|
|
124
160
|
config = _load_config()
|
|
125
161
|
scopes_config = config.get("scopes", _get_default_config()["scopes"])
|
|
126
|
-
scopes = []
|
|
127
|
-
# Always include userinfo
|
|
128
|
-
scopes.extend(scopes_config.get("userinfo", []))
|
|
162
|
+
scopes = list(scopes_config.get("userinfo", []))
|
|
129
163
|
for service in services:
|
|
130
164
|
scopes.extend(scopes_config.get(service, []))
|
|
131
165
|
return list(dict.fromkeys(scopes))
|
|
132
166
|
|
|
133
|
-
|
|
167
|
+
|
|
134
168
|
GOOGLE_WORKSPACE_SCOPES = get_all_scopes()
|
|
169
|
+
DEFAULT_SCOPES = GOOGLE_WORKSPACE_SCOPES # legacy alias
|
|
135
170
|
|
|
136
|
-
# Legacy alias for backward compatibility
|
|
137
|
-
DEFAULT_SCOPES = GOOGLE_WORKSPACE_SCOPES
|
|
138
171
|
|
|
139
|
-
#
|
|
140
|
-
|
|
172
|
+
# ============================================================================
|
|
173
|
+
# GoogleOAuth (composition wrapper)
|
|
174
|
+
# ============================================================================
|
|
141
175
|
|
|
142
176
|
|
|
143
177
|
class GoogleOAuth:
|
|
144
|
-
"""Google Workspace OAuth 2.0
|
|
178
|
+
"""Google Workspace OAuth 2.0 client (composition wrapper).
|
|
179
|
+
|
|
180
|
+
Conforms to the duck-typed protocol consumed by
|
|
181
|
+
:func:`services.events.oauth_lifecycle.make_oauth_lifecycle_handlers`
|
|
182
|
+
and :func:`make_oauth_callback_router`: shared :class:`OAuthStateStore`,
|
|
183
|
+
async ``exchange_code`` / ``fetch_user_info`` /
|
|
184
|
+
``refresh_access_token`` / ``revoke_token``.
|
|
145
185
|
|
|
146
|
-
|
|
147
|
-
|
|
186
|
+
Internally, ``Flow.from_client_config`` does the PKCE dance and
|
|
187
|
+
Flow.fetch_token does the token exchange under the
|
|
188
|
+
``OAUTHLIB_RELAX_TOKEN_SCOPE=1`` env var.
|
|
148
189
|
"""
|
|
149
190
|
|
|
191
|
+
# Plugin-scoped state store -- isolated from Twitter's instance.
|
|
192
|
+
state_store = OAuthStateStore()
|
|
193
|
+
|
|
150
194
|
def __init__(
|
|
151
195
|
self,
|
|
152
196
|
client_id: str,
|
|
153
197
|
client_secret: str,
|
|
154
198
|
redirect_uri: str,
|
|
155
199
|
scopes: Optional[List[str]] = None,
|
|
156
|
-
):
|
|
200
|
+
) -> None:
|
|
157
201
|
self.client_id = client_id
|
|
158
202
|
self.client_secret = client_secret
|
|
159
203
|
self.redirect_uri = redirect_uri
|
|
160
204
|
self.scopes = scopes or GOOGLE_WORKSPACE_SCOPES
|
|
161
205
|
|
|
162
|
-
# Get OAuth endpoints from config
|
|
163
206
|
oauth_endpoints = get_oauth_endpoints()
|
|
164
|
-
|
|
165
|
-
# Build client config in Google's format
|
|
166
207
|
self.client_config = {
|
|
167
208
|
"web": {
|
|
168
209
|
"client_id": client_id,
|
|
@@ -173,91 +214,50 @@ class GoogleOAuth:
|
|
|
173
214
|
}
|
|
174
215
|
}
|
|
175
216
|
self.token_uri = oauth_endpoints["token_uri"]
|
|
217
|
+
self.revoke_uri = oauth_endpoints.get(
|
|
218
|
+
"revoke_uri", "https://oauth2.googleapis.com/revoke",
|
|
219
|
+
)
|
|
176
220
|
|
|
177
221
|
def generate_authorization_url(
|
|
178
|
-
self,
|
|
179
|
-
state_data: Optional[Dict[str, Any]] = None,
|
|
222
|
+
self, *, state_data: Optional[Dict[str, Any]] = None,
|
|
180
223
|
) -> Dict[str, str]:
|
|
181
|
-
"""
|
|
182
|
-
Generate OAuth authorization URL.
|
|
183
|
-
|
|
184
|
-
Args:
|
|
185
|
-
state_data: Optional data (customer_id, mode, redirect_after)
|
|
186
|
-
|
|
187
|
-
Returns:
|
|
188
|
-
Dict with url and state
|
|
189
|
-
"""
|
|
190
|
-
# Create Flow from client config
|
|
224
|
+
"""Build the Google authorization URL + register state."""
|
|
191
225
|
flow = Flow.from_client_config(
|
|
192
|
-
self.client_config,
|
|
193
|
-
scopes=self.scopes,
|
|
194
|
-
redirect_uri=self.redirect_uri,
|
|
226
|
+
self.client_config, scopes=self.scopes, redirect_uri=self.redirect_uri,
|
|
195
227
|
)
|
|
196
|
-
|
|
197
|
-
# Generate authorization URL with offline access for refresh tokens
|
|
198
228
|
authorization_url, state = flow.authorization_url(
|
|
199
229
|
access_type="offline",
|
|
200
230
|
include_granted_scopes="true",
|
|
201
|
-
prompt="consent", #
|
|
231
|
+
prompt="consent", # force consent to get refresh token
|
|
202
232
|
)
|
|
203
|
-
|
|
204
|
-
# Store state data for callback verification
|
|
205
|
-
# PKCE: google-auth-oauthlib auto-generates code_verifier; save it
|
|
206
|
-
# so exchange_code() can restore it on the new Flow instance.
|
|
207
|
-
_oauth_states[state] = {
|
|
208
|
-
"created_at": time.time(),
|
|
233
|
+
self.state_store.put(state, {
|
|
209
234
|
"data": state_data or {"mode": "owner"},
|
|
210
235
|
"redirect_uri": self.redirect_uri,
|
|
211
236
|
"code_verifier": getattr(flow, "code_verifier", None),
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
logger.info("Generated Google OAuth URL", state=state[:8])
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
"url": authorization_url,
|
|
218
|
-
"state": state,
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
def exchange_code(self, code: str, state: str) -> Dict[str, Any]:
|
|
222
|
-
"""
|
|
223
|
-
Exchange authorization code for credentials.
|
|
237
|
+
})
|
|
238
|
+
return {"url": authorization_url, "state": state}
|
|
224
239
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Returns:
|
|
230
|
-
Dict with tokens and user info
|
|
231
|
-
"""
|
|
232
|
-
# Verify state
|
|
233
|
-
oauth_state = _oauth_states.pop(state, None)
|
|
234
|
-
if not oauth_state:
|
|
240
|
+
async def exchange_code(self, code: str, state: str) -> Dict[str, Any]:
|
|
241
|
+
"""Exchange an auth code for credentials (async wrapper)."""
|
|
242
|
+
record = self.state_store.take(state)
|
|
243
|
+
if not record:
|
|
235
244
|
return {"success": False, "error": "Invalid or expired state"}
|
|
236
245
|
|
|
237
|
-
state_data =
|
|
246
|
+
state_data = record.get("data", {})
|
|
247
|
+
code_verifier = record.get("code_verifier")
|
|
238
248
|
|
|
239
|
-
|
|
240
|
-
# Create Flow and fetch token
|
|
249
|
+
def _exchange_sync() -> Dict[str, Any]:
|
|
241
250
|
flow = Flow.from_client_config(
|
|
242
251
|
self.client_config,
|
|
243
252
|
scopes=self.scopes,
|
|
244
253
|
redirect_uri=self.redirect_uri,
|
|
245
254
|
state=state,
|
|
246
255
|
)
|
|
247
|
-
# PKCE: restore code_verifier so token exchange includes it
|
|
248
|
-
code_verifier = oauth_state.get("code_verifier")
|
|
249
256
|
if code_verifier:
|
|
250
257
|
flow.code_verifier = code_verifier
|
|
251
258
|
flow.fetch_token(code=code)
|
|
252
|
-
|
|
253
|
-
# Get credentials from flow
|
|
254
259
|
creds = flow.credentials
|
|
255
|
-
|
|
256
|
-
# Get user info
|
|
257
|
-
user_info = self._get_user_info(creds)
|
|
258
|
-
|
|
259
|
-
logger.info("Google OAuth successful", email=user_info.get("email", "")[:20])
|
|
260
|
-
|
|
260
|
+
user_info = self._get_user_info_sync(creds)
|
|
261
261
|
return {
|
|
262
262
|
"success": True,
|
|
263
263
|
"access_token": creds.token,
|
|
@@ -267,30 +267,85 @@ class GoogleOAuth:
|
|
|
267
267
|
"client_id": creds.client_id,
|
|
268
268
|
"client_secret": creds.client_secret,
|
|
269
269
|
"scopes": list(creds.scopes) if creds.scopes else self.scopes,
|
|
270
|
+
"scope": " ".join(creds.scopes) if creds.scopes else "",
|
|
270
271
|
"state_data": state_data,
|
|
271
272
|
"email": user_info.get("email"),
|
|
272
273
|
"name": user_info.get("name"),
|
|
273
274
|
"picture": user_info.get("picture"),
|
|
274
275
|
}
|
|
275
276
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
try:
|
|
278
|
+
return await asyncio.to_thread(_exchange_sync)
|
|
279
|
+
except Exception as exc: # noqa: BLE001
|
|
280
|
+
logger.error(f"[google] Token exchange failed: {exc}")
|
|
281
|
+
return {"success": False, "error": str(exc)}
|
|
282
|
+
|
|
283
|
+
async def fetch_user_info(self, access_token: str) -> Dict[str, Any]:
|
|
284
|
+
"""Build credentials from an access token + fetch user info."""
|
|
285
|
+
creds = self.build_credentials(
|
|
286
|
+
access_token=access_token,
|
|
287
|
+
refresh_token="", # not needed for a single read
|
|
288
|
+
client_id=self.client_id,
|
|
289
|
+
client_secret=self.client_secret,
|
|
290
|
+
token_uri=self.token_uri,
|
|
291
|
+
scopes=self.scopes,
|
|
292
|
+
)
|
|
293
|
+
try:
|
|
294
|
+
info = await asyncio.to_thread(self._get_user_info_sync, creds)
|
|
295
|
+
except Exception as exc: # noqa: BLE001
|
|
296
|
+
return {"success": False, "error": str(exc)}
|
|
297
|
+
if not info:
|
|
298
|
+
return {"success": False, "error": "Failed to read user info"}
|
|
299
|
+
return {"success": True, **info}
|
|
300
|
+
|
|
301
|
+
async def refresh_access_token(self, refresh_token: str) -> Dict[str, Any]:
|
|
302
|
+
"""Async wrapper around :meth:`refresh_credentials`."""
|
|
303
|
+
return await asyncio.to_thread(
|
|
304
|
+
self.refresh_credentials,
|
|
305
|
+
refresh_token, self.client_id, self.client_secret, self.token_uri,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
async def revoke_token(
|
|
309
|
+
self, token: str, token_type: str = "access_token",
|
|
310
|
+
) -> Dict[str, Any]:
|
|
311
|
+
"""Best-effort token revocation via Google's revoke endpoint."""
|
|
312
|
+
try:
|
|
313
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
314
|
+
response = await client.post(
|
|
315
|
+
self.revoke_uri,
|
|
316
|
+
data={"token": token},
|
|
317
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
318
|
+
)
|
|
319
|
+
except httpx.HTTPError as exc:
|
|
320
|
+
logger.warning(f"[google] revoke_token network error: {exc}")
|
|
321
|
+
return {"success": False, "error": str(exc)}
|
|
322
|
+
if response.status_code == 200:
|
|
323
|
+
return {"success": True}
|
|
324
|
+
return {
|
|
325
|
+
"success": False,
|
|
326
|
+
"error": (response.json() if response.text else {}).get(
|
|
327
|
+
"error_description", "Revocation failed",
|
|
328
|
+
),
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# ---- internal helpers ----------------------------------------------
|
|
279
332
|
|
|
280
|
-
def
|
|
281
|
-
"""
|
|
333
|
+
def _get_user_info_sync(self, creds: Credentials) -> Dict[str, Any]:
|
|
334
|
+
"""Sync user-info read; called inside ``asyncio.to_thread``."""
|
|
282
335
|
try:
|
|
283
336
|
service = build("oauth2", "v2", credentials=creds)
|
|
284
|
-
|
|
337
|
+
info = service.userinfo().get().execute()
|
|
285
338
|
return {
|
|
286
|
-
"email":
|
|
287
|
-
"name":
|
|
288
|
-
"picture":
|
|
339
|
+
"email": info.get("email"),
|
|
340
|
+
"name": info.get("name"),
|
|
341
|
+
"picture": info.get("picture"),
|
|
289
342
|
}
|
|
290
|
-
except Exception as
|
|
291
|
-
logger.error("Failed to get user info"
|
|
343
|
+
except Exception as exc: # noqa: BLE001
|
|
344
|
+
logger.error(f"[google] Failed to get user info: {exc}")
|
|
292
345
|
return {}
|
|
293
346
|
|
|
347
|
+
# ---- statics: kept for _auth_helper.py + other consumers ----------
|
|
348
|
+
|
|
294
349
|
@staticmethod
|
|
295
350
|
def refresh_credentials(
|
|
296
351
|
refresh_token: str,
|
|
@@ -298,21 +353,9 @@ class GoogleOAuth:
|
|
|
298
353
|
client_secret: str,
|
|
299
354
|
token_uri: Optional[str] = None,
|
|
300
355
|
) -> Dict[str, Any]:
|
|
301
|
-
"""
|
|
302
|
-
Refresh expired credentials.
|
|
303
|
-
|
|
304
|
-
Args:
|
|
305
|
-
refresh_token: The refresh token
|
|
306
|
-
client_id: OAuth client ID
|
|
307
|
-
client_secret: OAuth client secret
|
|
308
|
-
token_uri: Token endpoint (loaded from config if not provided)
|
|
309
|
-
|
|
310
|
-
Returns:
|
|
311
|
-
Dict with new access_token
|
|
312
|
-
"""
|
|
356
|
+
"""Refresh expired credentials. Returns ``{success, access_token, ...}``."""
|
|
313
357
|
if not token_uri:
|
|
314
358
|
token_uri = get_oauth_endpoints()["token_uri"]
|
|
315
|
-
|
|
316
359
|
try:
|
|
317
360
|
creds = Credentials(
|
|
318
361
|
token=None,
|
|
@@ -322,15 +365,14 @@ class GoogleOAuth:
|
|
|
322
365
|
client_secret=client_secret,
|
|
323
366
|
)
|
|
324
367
|
creds.refresh(Request())
|
|
325
|
-
|
|
326
368
|
return {
|
|
327
369
|
"success": True,
|
|
328
370
|
"access_token": creds.token,
|
|
329
371
|
"expires_in": 3600,
|
|
330
372
|
}
|
|
331
|
-
except Exception as
|
|
332
|
-
logger.error("Token refresh failed"
|
|
333
|
-
return {"success": False, "error": str(
|
|
373
|
+
except Exception as exc: # noqa: BLE001
|
|
374
|
+
logger.error(f"[google] Token refresh failed: {exc}")
|
|
375
|
+
return {"success": False, "error": str(exc), "needs_reauth": True}
|
|
334
376
|
|
|
335
377
|
@staticmethod
|
|
336
378
|
def build_credentials(
|
|
@@ -341,14 +383,9 @@ class GoogleOAuth:
|
|
|
341
383
|
token_uri: Optional[str] = None,
|
|
342
384
|
scopes: Optional[List[str]] = None,
|
|
343
385
|
) -> Credentials:
|
|
344
|
-
"""
|
|
345
|
-
Build Credentials object from stored tokens.
|
|
346
|
-
|
|
347
|
-
Use this to create credentials for Google API calls.
|
|
348
|
-
"""
|
|
386
|
+
"""Build Credentials object from stored tokens (used by _auth_helper)."""
|
|
349
387
|
if not token_uri:
|
|
350
388
|
token_uri = get_oauth_endpoints()["token_uri"]
|
|
351
|
-
|
|
352
389
|
return Credentials(
|
|
353
390
|
token=access_token,
|
|
354
391
|
refresh_token=refresh_token,
|
|
@@ -358,53 +395,49 @@ class GoogleOAuth:
|
|
|
358
395
|
scopes=scopes or GOOGLE_WORKSPACE_SCOPES,
|
|
359
396
|
)
|
|
360
397
|
|
|
361
|
-
# Service builders for each Google API
|
|
362
398
|
@staticmethod
|
|
363
399
|
def build_gmail_service(creds: Credentials):
|
|
364
|
-
"""Build Gmail API service from credentials."""
|
|
365
400
|
return build("gmail", "v1", credentials=creds)
|
|
366
401
|
|
|
367
402
|
@staticmethod
|
|
368
403
|
def build_calendar_service(creds: Credentials):
|
|
369
|
-
"""Build Calendar API service from credentials."""
|
|
370
404
|
return build("calendar", "v3", credentials=creds)
|
|
371
405
|
|
|
372
406
|
@staticmethod
|
|
373
407
|
def build_drive_service(creds: Credentials):
|
|
374
|
-
"""Build Drive API service from credentials."""
|
|
375
408
|
return build("drive", "v3", credentials=creds)
|
|
376
409
|
|
|
377
410
|
@staticmethod
|
|
378
411
|
def build_sheets_service(creds: Credentials):
|
|
379
|
-
"""Build Sheets API service from credentials."""
|
|
380
412
|
return build("sheets", "v4", credentials=creds)
|
|
381
413
|
|
|
382
414
|
@staticmethod
|
|
383
415
|
def build_tasks_service(creds: Credentials):
|
|
384
|
-
"""Build Tasks API service from credentials."""
|
|
385
416
|
return build("tasks", "v1", credentials=creds)
|
|
386
417
|
|
|
387
418
|
@staticmethod
|
|
388
419
|
def build_people_service(creds: Credentials):
|
|
389
|
-
"""Build People API service (Contacts) from credentials."""
|
|
390
420
|
return build("people", "v1", credentials=creds)
|
|
391
421
|
|
|
392
422
|
|
|
393
|
-
#
|
|
423
|
+
# Legacy alias.
|
|
394
424
|
GmailOAuth = GoogleOAuth
|
|
395
425
|
|
|
396
426
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
state for state, data in _oauth_states.items()
|
|
402
|
-
if current_time - data["created_at"] > max_age_seconds
|
|
403
|
-
]
|
|
404
|
-
for state in expired:
|
|
405
|
-
_oauth_states.pop(state, None)
|
|
427
|
+
# Module-level alias to the state store's backing dict so the contract
|
|
428
|
+
# tests in tests/credentials/test_google_oauth.py can ``_oauth_states.clear()``.
|
|
429
|
+
# Same trick the Twitter migration uses.
|
|
430
|
+
_oauth_states = GoogleOAuth.state_store._states
|
|
406
431
|
|
|
407
432
|
|
|
408
|
-
|
|
409
|
-
""
|
|
410
|
-
|
|
433
|
+
__all__ = [
|
|
434
|
+
"GoogleOAuth",
|
|
435
|
+
"GmailOAuth",
|
|
436
|
+
"GOOGLE_WORKSPACE_SCOPES",
|
|
437
|
+
"DEFAULT_SCOPES",
|
|
438
|
+
"get_oauth_endpoints",
|
|
439
|
+
"get_callback_paths",
|
|
440
|
+
"get_service_config",
|
|
441
|
+
"get_all_scopes",
|
|
442
|
+
"get_scopes_for_services",
|
|
443
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Google Workspace ``loadOptionsMethod`` loaders.
|
|
2
|
+
|
|
3
|
+
Wave 11.I, milestone M.2. Each function is registered with
|
|
4
|
+
``services.ws_handler_registry.register_option_loader`` from
|
|
5
|
+
``__init__.py``.
|
|
6
|
+
|
|
7
|
+
Reuses :func:`._auth_helper.get_google_credentials` so the OAuth dance
|
|
8
|
+
is identical to the workflow-execution path. ``params`` may carry
|
|
9
|
+
``account_mode`` / ``customer_id`` (multi-tenant customer mode) -- the
|
|
10
|
+
auth helper falls back to owner tokens otherwise.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
from typing import Any, Dict, List
|
|
17
|
+
|
|
18
|
+
from googleapiclient.discovery import build
|
|
19
|
+
|
|
20
|
+
from ._auth_helper import get_google_credentials
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def _google_service(api: str, version: str, params: Dict[str, Any]):
|
|
24
|
+
"""Build a googleapiclient service under the right OAuth credentials."""
|
|
25
|
+
creds = await get_google_credentials(params, {})
|
|
26
|
+
loop = asyncio.get_event_loop()
|
|
27
|
+
return await loop.run_in_executor(
|
|
28
|
+
None, lambda: build(api, version, credentials=creds)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def load_gmail_labels(params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
33
|
+
"""Gmail labels for the label-filter selector on gmailReceive and
|
|
34
|
+
gmail (search)."""
|
|
35
|
+
service = await _google_service("gmail", "v1", params)
|
|
36
|
+
loop = asyncio.get_event_loop()
|
|
37
|
+
response = await loop.run_in_executor(
|
|
38
|
+
None, lambda: service.users().labels().list(userId="me").execute()
|
|
39
|
+
)
|
|
40
|
+
labels = response.get("labels", [])
|
|
41
|
+
# Stable sort: system labels alphabetical first, then user labels.
|
|
42
|
+
labels.sort(
|
|
43
|
+
key=lambda label: (
|
|
44
|
+
label.get("type") != "system",
|
|
45
|
+
(label.get("name") or "").lower(),
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
return [
|
|
49
|
+
{"value": label["id"], "label": label.get("name") or label["id"]}
|
|
50
|
+
for label in labels
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def load_calendar_list(params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
55
|
+
"""Calendar list for the calendarId picker on calendar CRUD."""
|
|
56
|
+
service = await _google_service("calendar", "v3", params)
|
|
57
|
+
loop = asyncio.get_event_loop()
|
|
58
|
+
response = await loop.run_in_executor(
|
|
59
|
+
None, lambda: service.calendarList().list().execute()
|
|
60
|
+
)
|
|
61
|
+
entries = response.get("items", [])
|
|
62
|
+
# Primary first, rest alphabetised.
|
|
63
|
+
entries.sort(
|
|
64
|
+
key=lambda c: (not c.get("primary", False), (c.get("summary") or "").lower())
|
|
65
|
+
)
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
"value": c.get("id", ""),
|
|
69
|
+
"label": c.get("summary") or c.get("id", ""),
|
|
70
|
+
"description": "Primary" if c.get("primary") else c.get("description", ""),
|
|
71
|
+
}
|
|
72
|
+
for c in entries
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def load_drive_folders(params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
77
|
+
"""Drive folders for the folderId picker on drive upload/list."""
|
|
78
|
+
service = await _google_service("drive", "v3", params)
|
|
79
|
+
loop = asyncio.get_event_loop()
|
|
80
|
+
query = "mimeType='application/vnd.google-apps.folder' and trashed=false"
|
|
81
|
+
response = await loop.run_in_executor(
|
|
82
|
+
None,
|
|
83
|
+
lambda: service.files()
|
|
84
|
+
.list(q=query, fields="files(id, name, parents)", pageSize=200)
|
|
85
|
+
.execute(),
|
|
86
|
+
)
|
|
87
|
+
folders = response.get("files", [])
|
|
88
|
+
folders.sort(key=lambda f: (f.get("name") or "").lower())
|
|
89
|
+
return [
|
|
90
|
+
{"value": f.get("id", ""), "label": f.get("name") or f.get("id", "")}
|
|
91
|
+
for f in folders
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def load_tasklists(params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
96
|
+
"""Task lists for the tasklistId picker on tasks CRUD."""
|
|
97
|
+
service = await _google_service("tasks", "v1", params)
|
|
98
|
+
loop = asyncio.get_event_loop()
|
|
99
|
+
response = await loop.run_in_executor(
|
|
100
|
+
None, lambda: service.tasklists().list().execute()
|
|
101
|
+
)
|
|
102
|
+
lists = response.get("items", [])
|
|
103
|
+
lists.sort(key=lambda label: (label.get("title") or "").lower())
|
|
104
|
+
return [
|
|
105
|
+
{"value": label.get("id", ""), "label": label.get("title") or label.get("id", "")}
|
|
106
|
+
for label in lists
|
|
107
|
+
]
|