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,411 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Twitter/X OAuth 2.0 with PKCE - Browser-based login flow for X API v2.
|
|
3
|
-
|
|
4
|
-
Based on official X API documentation:
|
|
5
|
-
- Authorization URL: https://x.com/i/oauth2/authorize
|
|
6
|
-
- Token URL: https://api.x.com/2/oauth2/token
|
|
7
|
-
- Docs: https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code
|
|
8
|
-
|
|
9
|
-
Access tokens expire in 2 hours by default. Use offline.access scope for refresh tokens.
|
|
10
|
-
Authorization codes expire in 30 seconds.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import base64
|
|
14
|
-
import hashlib
|
|
15
|
-
import secrets
|
|
16
|
-
import time
|
|
17
|
-
from typing import Any, Dict, Optional
|
|
18
|
-
from urllib.parse import urlencode
|
|
19
|
-
|
|
20
|
-
import httpx
|
|
21
|
-
|
|
22
|
-
from core.logging import get_logger
|
|
23
|
-
|
|
24
|
-
logger = get_logger(__name__)
|
|
25
|
-
|
|
26
|
-
# X API OAuth 2.0 endpoints (updated URLs per latest docs)
|
|
27
|
-
AUTHORIZATION_URL = "https://x.com/i/oauth2/authorize"
|
|
28
|
-
TOKEN_URL = "https://api.x.com/2/oauth2/token"
|
|
29
|
-
REVOKE_URL = "https://api.x.com/2/oauth2/revoke"
|
|
30
|
-
USER_INFO_URL = "https://api.x.com/2/users/me"
|
|
31
|
-
|
|
32
|
-
# Required scopes for full Twitter integration
|
|
33
|
-
# See: https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code
|
|
34
|
-
DEFAULT_SCOPES = [
|
|
35
|
-
"tweet.read", # Read tweets
|
|
36
|
-
"tweet.write", # Post tweets
|
|
37
|
-
"users.read", # User lookup
|
|
38
|
-
"follows.read", # Read followers/following
|
|
39
|
-
"like.read", # Read likes
|
|
40
|
-
"like.write", # Like/unlike tweets
|
|
41
|
-
"offline.access", # Enable refresh tokens (access tokens expire in 2 hours)
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
# In-memory store for PKCE state (production should use Redis/database)
|
|
45
|
-
_oauth_states: Dict[str, Dict[str, Any]] = {}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _generate_code_verifier() -> str:
|
|
49
|
-
"""
|
|
50
|
-
Generate a cryptographically random code verifier (43-128 chars).
|
|
51
|
-
Per RFC 7636, must be 43-128 characters from [A-Z, a-z, 0-9, -, ., _, ~].
|
|
52
|
-
"""
|
|
53
|
-
# Generate 96 random bytes, encode to base64url (128 chars after stripping padding)
|
|
54
|
-
random_bytes = secrets.token_bytes(96)
|
|
55
|
-
verifier = base64.urlsafe_b64encode(random_bytes).rstrip(b"=").decode("ascii")
|
|
56
|
-
return verifier[:128]
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _generate_code_challenge(code_verifier: str) -> str:
|
|
60
|
-
"""
|
|
61
|
-
Generate S256 code challenge from verifier.
|
|
62
|
-
challenge = BASE64URL(SHA256(code_verifier))
|
|
63
|
-
"""
|
|
64
|
-
digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
|
|
65
|
-
challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
|
|
66
|
-
return challenge
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _generate_state() -> str:
|
|
70
|
-
"""Generate random state parameter for CSRF protection (up to 500 chars per X docs)."""
|
|
71
|
-
return secrets.token_urlsafe(32)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class TwitterOAuth:
|
|
75
|
-
"""
|
|
76
|
-
Twitter/X OAuth 2.0 with PKCE flow implementation.
|
|
77
|
-
|
|
78
|
-
Usage:
|
|
79
|
-
oauth = TwitterOAuth(client_id="...", redirect_uri="http://localhost:3010/api/twitter/callback") # URI derived at runtime
|
|
80
|
-
|
|
81
|
-
# Step 1: Generate authorization URL
|
|
82
|
-
auth_data = oauth.generate_authorization_url()
|
|
83
|
-
# Redirect user to auth_data["url"]
|
|
84
|
-
|
|
85
|
-
# Step 2: Handle callback and exchange code
|
|
86
|
-
tokens = await oauth.exchange_code(code="...", state="...")
|
|
87
|
-
|
|
88
|
-
# Step 3: Use access_token for API calls
|
|
89
|
-
user_info = await oauth.get_user_info(tokens["access_token"])
|
|
90
|
-
"""
|
|
91
|
-
|
|
92
|
-
def __init__(
|
|
93
|
-
self,
|
|
94
|
-
client_id: str,
|
|
95
|
-
redirect_uri: str,
|
|
96
|
-
client_secret: Optional[str] = None,
|
|
97
|
-
scopes: Optional[list] = None,
|
|
98
|
-
):
|
|
99
|
-
"""
|
|
100
|
-
Initialize Twitter OAuth client.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
client_id: Twitter OAuth 2.0 Client ID (from Developer Portal)
|
|
104
|
-
client_secret: Client Secret (required for confidential clients, optional for public clients with PKCE)
|
|
105
|
-
redirect_uri: OAuth callback URL (must match Developer Portal settings exactly)
|
|
106
|
-
scopes: List of OAuth scopes (defaults to DEFAULT_SCOPES)
|
|
107
|
-
"""
|
|
108
|
-
self.client_id = client_id
|
|
109
|
-
self.client_secret = client_secret
|
|
110
|
-
self.redirect_uri = redirect_uri
|
|
111
|
-
self.scopes = scopes or DEFAULT_SCOPES
|
|
112
|
-
|
|
113
|
-
def generate_authorization_url(self) -> Dict[str, str]:
|
|
114
|
-
"""
|
|
115
|
-
Generate OAuth authorization URL with PKCE parameters.
|
|
116
|
-
|
|
117
|
-
Returns:
|
|
118
|
-
Dict with:
|
|
119
|
-
- url: Authorization URL to redirect user to
|
|
120
|
-
- state: State parameter (for verification in callback)
|
|
121
|
-
- code_verifier: PKCE code verifier (save for token exchange)
|
|
122
|
-
"""
|
|
123
|
-
state = _generate_state()
|
|
124
|
-
code_verifier = _generate_code_verifier()
|
|
125
|
-
code_challenge = _generate_code_challenge(code_verifier)
|
|
126
|
-
|
|
127
|
-
# Store state for verification (with expiry tracking)
|
|
128
|
-
_oauth_states[state] = {
|
|
129
|
-
"code_verifier": code_verifier,
|
|
130
|
-
"created_at": time.time(),
|
|
131
|
-
"redirect_uri": self.redirect_uri,
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
params = {
|
|
135
|
-
"response_type": "code",
|
|
136
|
-
"client_id": self.client_id,
|
|
137
|
-
"redirect_uri": self.redirect_uri,
|
|
138
|
-
"scope": " ".join(self.scopes),
|
|
139
|
-
"state": state,
|
|
140
|
-
"code_challenge": code_challenge,
|
|
141
|
-
"code_challenge_method": "S256",
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
authorization_url = f"{AUTHORIZATION_URL}?{urlencode(params)}"
|
|
145
|
-
|
|
146
|
-
logger.info("Generated Twitter OAuth authorization URL", state=state[:8])
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
"url": authorization_url,
|
|
150
|
-
"state": state,
|
|
151
|
-
"code_verifier": code_verifier,
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async def exchange_code(self, code: str, state: str) -> Dict[str, Any]:
|
|
155
|
-
"""
|
|
156
|
-
Exchange authorization code for access token.
|
|
157
|
-
|
|
158
|
-
Note: Authorization codes expire in 30 seconds per X docs.
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
code: Authorization code from callback
|
|
162
|
-
state: State parameter for verification
|
|
163
|
-
|
|
164
|
-
Returns:
|
|
165
|
-
Dict with:
|
|
166
|
-
- success: True/False
|
|
167
|
-
- access_token: Bearer token for API calls (expires in 2 hours)
|
|
168
|
-
- refresh_token: Token for refreshing access (if offline.access scope)
|
|
169
|
-
- expires_in: Token expiry in seconds
|
|
170
|
-
- scope: Granted scopes
|
|
171
|
-
"""
|
|
172
|
-
# Verify state and get code_verifier
|
|
173
|
-
oauth_state = _oauth_states.pop(state, None)
|
|
174
|
-
if not oauth_state:
|
|
175
|
-
logger.error("Invalid or expired OAuth state", state=state[:8] if state else "None")
|
|
176
|
-
return {"success": False, "error": "Invalid or expired state"}
|
|
177
|
-
|
|
178
|
-
code_verifier = oauth_state["code_verifier"]
|
|
179
|
-
|
|
180
|
-
# Prepare token request body
|
|
181
|
-
data = {
|
|
182
|
-
"grant_type": "authorization_code",
|
|
183
|
-
"code": code,
|
|
184
|
-
"redirect_uri": self.redirect_uri,
|
|
185
|
-
"code_verifier": code_verifier,
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
# Authentication: Basic auth for confidential clients, client_id in body for public clients
|
|
189
|
-
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
190
|
-
|
|
191
|
-
if self.client_secret:
|
|
192
|
-
# Confidential client: use Basic auth
|
|
193
|
-
credentials = base64.b64encode(
|
|
194
|
-
f"{self.client_id}:{self.client_secret}".encode()
|
|
195
|
-
).decode()
|
|
196
|
-
headers["Authorization"] = f"Basic {credentials}"
|
|
197
|
-
else:
|
|
198
|
-
# Public client: include client_id in body
|
|
199
|
-
data["client_id"] = self.client_id
|
|
200
|
-
|
|
201
|
-
try:
|
|
202
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
203
|
-
response = await client.post(
|
|
204
|
-
TOKEN_URL,
|
|
205
|
-
data=data,
|
|
206
|
-
headers=headers,
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
if response.status_code != 200:
|
|
210
|
-
error_data = response.json() if response.text else {}
|
|
211
|
-
logger.error(
|
|
212
|
-
"Token exchange failed",
|
|
213
|
-
status=response.status_code,
|
|
214
|
-
error=error_data,
|
|
215
|
-
)
|
|
216
|
-
return {
|
|
217
|
-
"success": False,
|
|
218
|
-
"error": error_data.get("error_description", error_data.get("error", "Token exchange failed")),
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
token_data = response.json()
|
|
222
|
-
logger.info("Twitter OAuth token exchange successful")
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
"success": True,
|
|
226
|
-
"access_token": token_data.get("access_token"),
|
|
227
|
-
"refresh_token": token_data.get("refresh_token"),
|
|
228
|
-
"expires_in": token_data.get("expires_in"),
|
|
229
|
-
"scope": token_data.get("scope"),
|
|
230
|
-
"token_type": token_data.get("token_type", "Bearer"),
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
except httpx.HTTPError as e:
|
|
234
|
-
logger.error("HTTP error during token exchange", error=str(e))
|
|
235
|
-
return {"success": False, "error": str(e)}
|
|
236
|
-
|
|
237
|
-
async def refresh_access_token(self, refresh_token: str) -> Dict[str, Any]:
|
|
238
|
-
"""
|
|
239
|
-
Refresh an expired access token.
|
|
240
|
-
|
|
241
|
-
Args:
|
|
242
|
-
refresh_token: The refresh token from previous authorization
|
|
243
|
-
|
|
244
|
-
Returns:
|
|
245
|
-
Dict with new access_token, refresh_token, expires_in
|
|
246
|
-
"""
|
|
247
|
-
data = {
|
|
248
|
-
"grant_type": "refresh_token",
|
|
249
|
-
"refresh_token": refresh_token,
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
253
|
-
|
|
254
|
-
if self.client_secret:
|
|
255
|
-
credentials = base64.b64encode(
|
|
256
|
-
f"{self.client_id}:{self.client_secret}".encode()
|
|
257
|
-
).decode()
|
|
258
|
-
headers["Authorization"] = f"Basic {credentials}"
|
|
259
|
-
else:
|
|
260
|
-
data["client_id"] = self.client_id
|
|
261
|
-
|
|
262
|
-
try:
|
|
263
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
264
|
-
response = await client.post(
|
|
265
|
-
TOKEN_URL,
|
|
266
|
-
data=data,
|
|
267
|
-
headers=headers,
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
if response.status_code != 200:
|
|
271
|
-
error_data = response.json() if response.text else {}
|
|
272
|
-
logger.error(
|
|
273
|
-
"Token refresh failed",
|
|
274
|
-
status=response.status_code,
|
|
275
|
-
error=error_data,
|
|
276
|
-
)
|
|
277
|
-
return {
|
|
278
|
-
"success": False,
|
|
279
|
-
"error": error_data.get("error_description", "Token refresh failed"),
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
token_data = response.json()
|
|
283
|
-
logger.info("Twitter token refresh successful")
|
|
284
|
-
|
|
285
|
-
return {
|
|
286
|
-
"success": True,
|
|
287
|
-
"access_token": token_data.get("access_token"),
|
|
288
|
-
"refresh_token": token_data.get("refresh_token"),
|
|
289
|
-
"expires_in": token_data.get("expires_in"),
|
|
290
|
-
"scope": token_data.get("scope"),
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
except httpx.HTTPError as e:
|
|
294
|
-
logger.error("HTTP error during token refresh", error=str(e))
|
|
295
|
-
return {"success": False, "error": str(e)}
|
|
296
|
-
|
|
297
|
-
async def revoke_token(self, token: str, token_type: str = "access_token") -> Dict[str, Any]:
|
|
298
|
-
"""
|
|
299
|
-
Revoke an access or refresh token.
|
|
300
|
-
|
|
301
|
-
Args:
|
|
302
|
-
token: The token to revoke
|
|
303
|
-
token_type: 'access_token' or 'refresh_token'
|
|
304
|
-
|
|
305
|
-
Returns:
|
|
306
|
-
Dict with success status
|
|
307
|
-
"""
|
|
308
|
-
data = {
|
|
309
|
-
"token": token,
|
|
310
|
-
"token_type_hint": token_type,
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
314
|
-
|
|
315
|
-
if self.client_secret:
|
|
316
|
-
credentials = base64.b64encode(
|
|
317
|
-
f"{self.client_id}:{self.client_secret}".encode()
|
|
318
|
-
).decode()
|
|
319
|
-
headers["Authorization"] = f"Basic {credentials}"
|
|
320
|
-
else:
|
|
321
|
-
data["client_id"] = self.client_id
|
|
322
|
-
|
|
323
|
-
try:
|
|
324
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
325
|
-
response = await client.post(
|
|
326
|
-
REVOKE_URL,
|
|
327
|
-
data=data,
|
|
328
|
-
headers=headers,
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
# 200 = success, even if token was already revoked
|
|
332
|
-
if response.status_code == 200:
|
|
333
|
-
logger.info("Twitter token revoked successfully")
|
|
334
|
-
return {"success": True}
|
|
335
|
-
else:
|
|
336
|
-
error_data = response.json() if response.text else {}
|
|
337
|
-
return {
|
|
338
|
-
"success": False,
|
|
339
|
-
"error": error_data.get("error_description", "Revocation failed"),
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
except httpx.HTTPError as e:
|
|
343
|
-
logger.error("HTTP error during token revocation", error=str(e))
|
|
344
|
-
return {"success": False, "error": str(e)}
|
|
345
|
-
|
|
346
|
-
async def get_user_info(self, access_token: str) -> Dict[str, Any]:
|
|
347
|
-
"""
|
|
348
|
-
Get authenticated user information.
|
|
349
|
-
|
|
350
|
-
Args:
|
|
351
|
-
access_token: Valid Twitter access token
|
|
352
|
-
|
|
353
|
-
Returns:
|
|
354
|
-
Dict with user id, username, name, profile_image_url, verified
|
|
355
|
-
"""
|
|
356
|
-
try:
|
|
357
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
358
|
-
response = await client.get(
|
|
359
|
-
USER_INFO_URL,
|
|
360
|
-
params={"user.fields": "id,name,username,profile_image_url,verified"},
|
|
361
|
-
headers={"Authorization": f"Bearer {access_token}"},
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
if response.status_code != 200:
|
|
365
|
-
error_data = response.json() if response.text else {}
|
|
366
|
-
error_detail = error_data.get("detail") or error_data.get("title", "Failed to get user info")
|
|
367
|
-
return {
|
|
368
|
-
"success": False,
|
|
369
|
-
"error": error_detail,
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
data = response.json()
|
|
373
|
-
user = data.get("data", {})
|
|
374
|
-
|
|
375
|
-
return {
|
|
376
|
-
"success": True,
|
|
377
|
-
"id": user.get("id"),
|
|
378
|
-
"username": user.get("username"),
|
|
379
|
-
"name": user.get("name"),
|
|
380
|
-
"profile_image_url": user.get("profile_image_url"),
|
|
381
|
-
"verified": user.get("verified", False),
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
except httpx.HTTPError as e:
|
|
385
|
-
logger.error("HTTP error getting user info", error=str(e))
|
|
386
|
-
return {"success": False, "error": str(e)}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
def cleanup_expired_states(max_age_seconds: int = 600):
|
|
390
|
-
"""
|
|
391
|
-
Remove OAuth states older than max_age_seconds (default 10 minutes).
|
|
392
|
-
|
|
393
|
-
Authorization codes expire in 30 seconds, so states older than a few
|
|
394
|
-
minutes are definitely stale.
|
|
395
|
-
"""
|
|
396
|
-
current_time = time.time()
|
|
397
|
-
expired = [
|
|
398
|
-
state
|
|
399
|
-
for state, data in _oauth_states.items()
|
|
400
|
-
if current_time - data["created_at"] > max_age_seconds
|
|
401
|
-
]
|
|
402
|
-
for state in expired:
|
|
403
|
-
_oauth_states.pop(state, None)
|
|
404
|
-
|
|
405
|
-
if expired:
|
|
406
|
-
logger.debug(f"Cleaned up {len(expired)} expired OAuth states")
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def get_pending_state(state: str) -> Optional[Dict[str, Any]]:
|
|
410
|
-
"""Get pending OAuth state without removing it (for verification)."""
|
|
411
|
-
return _oauth_states.get(state)
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Android WebSocket Client - DEPRECATED
|
|
3
|
-
|
|
4
|
-
This module is deprecated. Use services.android instead:
|
|
5
|
-
|
|
6
|
-
from services.android import (
|
|
7
|
-
RelayWebSocketClient,
|
|
8
|
-
get_relay_client,
|
|
9
|
-
close_relay_client,
|
|
10
|
-
get_current_relay_client,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
This file re-exports from the new module for backwards compatibility.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from services.android import (
|
|
17
|
-
RelayWebSocketClient,
|
|
18
|
-
get_relay_client,
|
|
19
|
-
close_relay_client,
|
|
20
|
-
get_current_relay_client,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
# Re-export for backwards compatibility
|
|
24
|
-
__all__ = [
|
|
25
|
-
"RelayWebSocketClient",
|
|
26
|
-
"get_relay_client",
|
|
27
|
-
"close_relay_client",
|
|
28
|
-
"get_current_relay_client",
|
|
29
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|