neoagent 3.2.1-beta.0 → 3.2.1-beta.10
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/docs/agent-run-lifecycle.md +10 -0
- package/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +111 -20
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +78 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +145 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +4 -4
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +85 -40
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +11 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +73083 -72209
- package/server/routes/admin.js +1 -1
- package/server/routes/agents.js +35 -2
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +496 -166
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +121 -5
- package/server/services/ai/loop/conversation_loop.js +620 -340
- package/server/services/ai/loop/lifecycle.js +108 -0
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +48 -56
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +28 -8
- package/server/services/ai/loopPolicy.js +48 -21
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +68 -163
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +166 -0
- package/server/services/ai/providers/anthropic.js +4 -4
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +135 -97
- package/server/services/ai/providers/grok.js +6 -5
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +12 -7
- package/server/services/ai/providers/ollama.js +114 -86
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +41 -7
- package/server/services/ai/providers/openaiCodex.js +13 -4
- package/server/services/ai/providers/openrouter.js +31 -9
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/systemPrompt.js +19 -12
- package/server/services/ai/taskAnalysis.js +58 -10
- package/server/services/ai/terminal_reply.js +18 -0
- package/server/services/ai/toolEvidence.js +350 -29
- package/server/services/ai/tools.js +190 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +159 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +110 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +85 -10
- package/server/services/messaging/formatting_guides.js +7 -4
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +22 -14
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/retry.js +107 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../../../db/database');
|
|
4
|
+
const { resolveAgentId } = require('../../agents/manager');
|
|
5
|
+
const { getConnectionAccessMode } = require('../access');
|
|
6
|
+
const {
|
|
7
|
+
deleteProviderConfig,
|
|
8
|
+
getProviderConfig,
|
|
9
|
+
setProviderConfig,
|
|
10
|
+
} = require('../provider_config_store');
|
|
11
|
+
const { encryptValue } = require('../secrets');
|
|
12
|
+
const { BITWARDEN_APP, BITWARDEN_PROVIDER_KEY } = require('./constants');
|
|
13
|
+
const { buildBitwardenSnapshot } = require('./snapshot');
|
|
14
|
+
|
|
15
|
+
const DEFAULT_SERVER_URL = 'https://vault.bitwarden.com';
|
|
16
|
+
|
|
17
|
+
function text(value) {
|
|
18
|
+
return String(value || '').trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeServerUrl(value) {
|
|
22
|
+
const url = new URL(text(value) || DEFAULT_SERVER_URL);
|
|
23
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
|
|
24
|
+
throw new Error('Bitwarden server must be an HTTPS URL without embedded credentials.');
|
|
25
|
+
}
|
|
26
|
+
return url.toString().replace(/\/+$/, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseConfig(raw, existing = {}) {
|
|
30
|
+
const source = raw && typeof raw === 'object' ? raw : {};
|
|
31
|
+
return {
|
|
32
|
+
serverUrl: normalizeServerUrl(source.serverUrl || existing.serverUrl || DEFAULT_SERVER_URL),
|
|
33
|
+
email: text(source.email) || text(existing.email),
|
|
34
|
+
clientId: text(source.clientId) || text(existing.clientId),
|
|
35
|
+
clientSecret: text(source.clientSecret) || text(existing.clientSecret),
|
|
36
|
+
idleTimeoutMinutes: Math.max(5, Math.min(120, Number(
|
|
37
|
+
source.idleTimeoutMinutes || existing.idleTimeoutMinutes || 30,
|
|
38
|
+
))),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadConnection(userId, agentId) {
|
|
43
|
+
return db.prepare(
|
|
44
|
+
`SELECT * FROM integration_connections
|
|
45
|
+
WHERE user_id = ? AND agent_id = ? AND provider_key = ? AND app_key = ?
|
|
46
|
+
ORDER BY updated_at DESC, id DESC LIMIT 1`,
|
|
47
|
+
).get(userId, agentId, BITWARDEN_PROVIDER_KEY, BITWARDEN_APP.id) || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function publicConfig(config, connection, status = {}) {
|
|
51
|
+
return {
|
|
52
|
+
serverUrl: config.serverUrl || DEFAULT_SERVER_URL,
|
|
53
|
+
email: config.email || '',
|
|
54
|
+
idleTimeoutMinutes: config.idleTimeoutMinutes || 30,
|
|
55
|
+
hasClientId: Boolean(config.clientId),
|
|
56
|
+
hasClientSecret: Boolean(config.clientSecret),
|
|
57
|
+
configured: Boolean(config.email && config.clientId && config.clientSecret),
|
|
58
|
+
accountCount: connection?.status === 'connected' ? 1 : 0,
|
|
59
|
+
hasConnectedAccount: connection?.status === 'connected',
|
|
60
|
+
connectionId: connection?.id || null,
|
|
61
|
+
cliAvailable: status.cliAvailable !== false,
|
|
62
|
+
unlocked: Boolean(status.unlocked),
|
|
63
|
+
lastUsedAt: status.lastUsedAt || null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function upsertConnection(userId, agentId, config) {
|
|
68
|
+
const existing = loadConnection(userId, agentId);
|
|
69
|
+
const metadata = JSON.stringify({
|
|
70
|
+
access_mode: getConnectionAccessMode(existing),
|
|
71
|
+
idleTimeoutMinutes: config.idleTimeoutMinutes,
|
|
72
|
+
});
|
|
73
|
+
db.prepare(
|
|
74
|
+
`INSERT INTO integration_connections (
|
|
75
|
+
user_id, agent_id, provider_key, app_key, status, account_email,
|
|
76
|
+
scopes_json, credentials_json, metadata_json, last_connected_at, updated_at
|
|
77
|
+
) VALUES (?, ?, ?, ?, 'connected', ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
78
|
+
ON CONFLICT(user_id, agent_id, provider_key, app_key, account_email) DO UPDATE SET
|
|
79
|
+
status = excluded.status,
|
|
80
|
+
credentials_json = excluded.credentials_json,
|
|
81
|
+
metadata_json = excluded.metadata_json,
|
|
82
|
+
last_connected_at = excluded.last_connected_at,
|
|
83
|
+
updated_at = excluded.updated_at`,
|
|
84
|
+
).run(
|
|
85
|
+
userId,
|
|
86
|
+
agentId,
|
|
87
|
+
BITWARDEN_PROVIDER_KEY,
|
|
88
|
+
BITWARDEN_APP.id,
|
|
89
|
+
config.email,
|
|
90
|
+
JSON.stringify(['vault:read_selected']),
|
|
91
|
+
encryptValue(JSON.stringify(config)),
|
|
92
|
+
metadata,
|
|
93
|
+
);
|
|
94
|
+
if (existing && existing.account_email !== config.email) {
|
|
95
|
+
db.prepare('DELETE FROM integration_connections WHERE id = ?').run(existing.id);
|
|
96
|
+
}
|
|
97
|
+
return loadConnection(userId, agentId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createBitwardenProvider(options = {}) {
|
|
101
|
+
const app = options.app || null;
|
|
102
|
+
const cli = () => app?.locals?.bitwardenCli;
|
|
103
|
+
const broker = () => app?.locals?.credentialBroker;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
key: BITWARDEN_PROVIDER_KEY,
|
|
107
|
+
label: 'Bitwarden',
|
|
108
|
+
description: 'Fill browser logins and authenticate bounded API requests while keeping secrets hidden from the AI.',
|
|
109
|
+
icon: 'password',
|
|
110
|
+
apps: [BITWARDEN_APP],
|
|
111
|
+
connectPrompt: 'Add a Bitwarden API key, then unlock the vault when credentials are needed. The master password and vault secrets are never sent to the AI.',
|
|
112
|
+
supportsMultipleAccounts: false,
|
|
113
|
+
connectionMethod: 'user_config',
|
|
114
|
+
getApp(appId) {
|
|
115
|
+
return text(appId) === BITWARDEN_APP.id ? BITWARDEN_APP : null;
|
|
116
|
+
},
|
|
117
|
+
getToolAppId() {
|
|
118
|
+
return null;
|
|
119
|
+
},
|
|
120
|
+
getToolDefinitions() {
|
|
121
|
+
return [];
|
|
122
|
+
},
|
|
123
|
+
supportsTool() {
|
|
124
|
+
return false;
|
|
125
|
+
},
|
|
126
|
+
getEnvStatus(context = {}) {
|
|
127
|
+
const userId = Number(context.userId);
|
|
128
|
+
const agentId = Number.isInteger(userId) && userId > 0
|
|
129
|
+
? resolveAgentId(userId, context.agentId || null)
|
|
130
|
+
: null;
|
|
131
|
+
const config = agentId ? parseConfig(getProviderConfig(userId, BITWARDEN_PROVIDER_KEY, agentId)) : {};
|
|
132
|
+
const missing = ['email', 'clientId', 'clientSecret'].filter((key) => !config[key]);
|
|
133
|
+
return {
|
|
134
|
+
configured: missing.length === 0,
|
|
135
|
+
missing,
|
|
136
|
+
summary: missing.length === 0
|
|
137
|
+
? 'Bitwarden is configured. Unlock it only when a credential operation is needed.'
|
|
138
|
+
: 'Add a Bitwarden personal API key in Official Integrations.',
|
|
139
|
+
setupMode: 'user',
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
buildSnapshot(connectionRows, context = {}) {
|
|
143
|
+
return buildBitwardenSnapshot(this, connectionRows, {
|
|
144
|
+
...context,
|
|
145
|
+
credentialBindingsSummary: context.userId
|
|
146
|
+
? broker()?.summarizeBindings(context.userId, context.agentId)
|
|
147
|
+
: 'No credential bindings are configured.',
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
summarizeForModel(snapshot) {
|
|
151
|
+
if (!snapshot?.connection?.connected) {
|
|
152
|
+
return 'Bitwarden: not configured. The user can connect it in Official Integrations.';
|
|
153
|
+
}
|
|
154
|
+
const bindings = snapshot.credentialBindingsSummary || 'No credential bindings are configured.';
|
|
155
|
+
return `Bitwarden credential broker: ${bindings} Secret values are never available to the model.`;
|
|
156
|
+
},
|
|
157
|
+
getUserConfig({ userId, agentId }) {
|
|
158
|
+
const normalizedUserId = Number(userId);
|
|
159
|
+
const scopedAgentId = resolveAgentId(normalizedUserId, agentId || null);
|
|
160
|
+
const config = parseConfig(getProviderConfig(normalizedUserId, BITWARDEN_PROVIDER_KEY, scopedAgentId));
|
|
161
|
+
return publicConfig(config, loadConnection(normalizedUserId, scopedAgentId), cli()?.getStatus(normalizedUserId, scopedAgentId));
|
|
162
|
+
},
|
|
163
|
+
async saveUserConfig({ userId, agentId, config, signal }) {
|
|
164
|
+
const normalizedUserId = Number(userId);
|
|
165
|
+
if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
|
|
166
|
+
throw new Error('A valid user is required to save Bitwarden configuration.');
|
|
167
|
+
}
|
|
168
|
+
const scopedAgentId = resolveAgentId(normalizedUserId, agentId || null);
|
|
169
|
+
const existing = getProviderConfig(normalizedUserId, BITWARDEN_PROVIDER_KEY, scopedAgentId);
|
|
170
|
+
const parsed = parseConfig(config, existing);
|
|
171
|
+
if (!parsed.email || !parsed.clientId || !parsed.clientSecret) {
|
|
172
|
+
throw new Error('Bitwarden email, client ID, and client secret are required.');
|
|
173
|
+
}
|
|
174
|
+
if (!cli()) throw new Error('Bitwarden credential service is unavailable.');
|
|
175
|
+
await cli().configure(normalizedUserId, scopedAgentId, parsed, { signal });
|
|
176
|
+
setProviderConfig(normalizedUserId, BITWARDEN_PROVIDER_KEY, parsed, scopedAgentId);
|
|
177
|
+
const connection = upsertConnection(normalizedUserId, scopedAgentId, parsed);
|
|
178
|
+
return publicConfig(parsed, connection, cli().getStatus(normalizedUserId, scopedAgentId));
|
|
179
|
+
},
|
|
180
|
+
async clearUserConfig({ userId, agentId }) {
|
|
181
|
+
const normalizedUserId = Number(userId);
|
|
182
|
+
const scopedAgentId = resolveAgentId(normalizedUserId, agentId || null);
|
|
183
|
+
await cli()?.logout(normalizedUserId, scopedAgentId);
|
|
184
|
+
deleteProviderConfig(normalizedUserId, BITWARDEN_PROVIDER_KEY, scopedAgentId);
|
|
185
|
+
db.prepare(
|
|
186
|
+
'DELETE FROM integration_connections WHERE user_id = ? AND agent_id = ? AND provider_key = ?',
|
|
187
|
+
).run(normalizedUserId, scopedAgentId, BITWARDEN_PROVIDER_KEY);
|
|
188
|
+
return { cleared: true };
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
createBitwardenProvider,
|
|
195
|
+
normalizeServerUrl,
|
|
196
|
+
parseConfig,
|
|
197
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getConnectionAccessMode } = require('../access');
|
|
4
|
+
const { BITWARDEN_APP } = require('./constants');
|
|
5
|
+
|
|
6
|
+
function summarizeAccount(row, env) {
|
|
7
|
+
if (!row) {
|
|
8
|
+
return {
|
|
9
|
+
id: null,
|
|
10
|
+
status: env.configured ? 'not_connected' : 'env_not_configured',
|
|
11
|
+
connected: false,
|
|
12
|
+
accountEmail: null,
|
|
13
|
+
lastConnectedAt: null,
|
|
14
|
+
accessMode: 'read_write',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
id: row.id,
|
|
19
|
+
status: row.status,
|
|
20
|
+
connected: row.status === 'connected',
|
|
21
|
+
accountEmail: row.account_email || null,
|
|
22
|
+
lastConnectedAt: row.last_connected_at || null,
|
|
23
|
+
accessMode: getConnectionAccessMode(row),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildBitwardenSnapshot(provider, connectionRows, context = {}) {
|
|
28
|
+
const env = provider.getEnvStatus(context);
|
|
29
|
+
const row = (Array.isArray(connectionRows) ? connectionRows : [])
|
|
30
|
+
.filter((candidate) => candidate.app_key === BITWARDEN_APP.id)
|
|
31
|
+
.sort((left, right) => String(right.updated_at || '').localeCompare(String(left.updated_at || '')))[0] || null;
|
|
32
|
+
const account = summarizeAccount(row, env);
|
|
33
|
+
const accounts = row ? [account] : [];
|
|
34
|
+
const connection = {
|
|
35
|
+
status: !env.configured ? 'env_not_configured' : account.status,
|
|
36
|
+
connected: account.connected,
|
|
37
|
+
accountEmail: account.accountEmail,
|
|
38
|
+
accountCount: account.connected ? 1 : 0,
|
|
39
|
+
appCount: account.connected ? 1 : 0,
|
|
40
|
+
lastConnectedAt: account.lastConnectedAt,
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
id: provider.key,
|
|
44
|
+
label: provider.label,
|
|
45
|
+
description: provider.description,
|
|
46
|
+
icon: provider.icon,
|
|
47
|
+
apps: [{
|
|
48
|
+
...BITWARDEN_APP,
|
|
49
|
+
accounts,
|
|
50
|
+
connection,
|
|
51
|
+
availableToolCount: account.connected ? 2 : 0,
|
|
52
|
+
}],
|
|
53
|
+
env,
|
|
54
|
+
connection,
|
|
55
|
+
availableToolCount: account.connected ? 2 : 0,
|
|
56
|
+
credentialBindingsSummary: context.credentialBindingsSummary || 'No credential bindings are configured.',
|
|
57
|
+
connectPrompt: provider.connectPrompt,
|
|
58
|
+
supportsMultipleAccounts: false,
|
|
59
|
+
connectionMethod: 'user_config',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
buildBitwardenSnapshot,
|
|
65
|
+
};
|
|
@@ -158,25 +158,88 @@ function figmaUrl(path, query) {
|
|
|
158
158
|
return url.toString();
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
return
|
|
161
|
+
function expiresAtFromSeconds(expiresIn) {
|
|
162
|
+
return new Date(
|
|
163
|
+
Date.now() + Math.max(1, Number(expiresIn) || 3600) * 1000,
|
|
164
|
+
).toISOString();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function tokenExpiresSoon(credentials) {
|
|
168
|
+
const expiresAt = Date.parse(String(credentials?.expires_at || ''));
|
|
169
|
+
return Number.isFinite(expiresAt) && expiresAt <= Date.now() + 60 * 1000;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function refreshFigmaCredentials(credentials, signal) {
|
|
173
|
+
const refreshToken = String(credentials?.refresh_token || '').trim();
|
|
174
|
+
if (!refreshToken) {
|
|
175
|
+
throw new Error('Figma refresh token is missing. Reconnect this integration account.');
|
|
176
|
+
}
|
|
177
|
+
const config = resolveFigmaOAuthConfig();
|
|
178
|
+
const basic = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString(
|
|
179
|
+
'base64',
|
|
180
|
+
);
|
|
181
|
+
const token = await fetchJson(
|
|
182
|
+
'https://api.figma.com/v1/oauth/token',
|
|
183
|
+
{
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: { Authorization: `Basic ${basic}` },
|
|
186
|
+
form: {
|
|
187
|
+
grant_type: 'refresh_token',
|
|
188
|
+
refresh_token: refreshToken,
|
|
189
|
+
},
|
|
190
|
+
signal,
|
|
191
|
+
},
|
|
192
|
+
{ serviceName: 'Figma token refresh' },
|
|
193
|
+
);
|
|
194
|
+
if (!String(token?.access_token || '').trim()) {
|
|
195
|
+
throw new Error('Figma token refresh did not return an access token.');
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
...credentials,
|
|
199
|
+
access_token: token.access_token,
|
|
200
|
+
refresh_token: token.refresh_token || refreshToken,
|
|
201
|
+
token_type: token.token_type || credentials.token_type || 'bearer',
|
|
202
|
+
expires_in: token.expires_in,
|
|
203
|
+
expires_at: expiresAtFromSeconds(token.expires_in),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function figmaRequest(context, { method = 'GET', path, query, body }) {
|
|
208
|
+
const { signal } = context;
|
|
209
|
+
let credentials = context.credentials;
|
|
210
|
+
if (tokenExpiresSoon(credentials)) {
|
|
211
|
+
credentials = await refreshFigmaCredentials(credentials, signal);
|
|
212
|
+
context.updateCredentials(credentials);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const performRequest = (activeCredentials) => fetchJson(
|
|
163
216
|
figmaUrl(path, query),
|
|
164
217
|
{
|
|
165
218
|
method: String(method || 'GET').toUpperCase(),
|
|
166
|
-
headers: { Authorization: `Bearer ${
|
|
219
|
+
headers: { Authorization: `Bearer ${activeCredentials.access_token}` },
|
|
167
220
|
...(body === undefined ? {} : { json: body }),
|
|
221
|
+
signal,
|
|
168
222
|
},
|
|
169
223
|
{ serviceName: 'Figma' },
|
|
170
224
|
);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
return await performRequest(credentials);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error?.status !== 401 || !credentials.refresh_token) throw error;
|
|
230
|
+
credentials = await refreshFigmaCredentials(credentials, signal);
|
|
231
|
+
context.updateCredentials(credentials);
|
|
232
|
+
return performRequest(credentials);
|
|
233
|
+
}
|
|
171
234
|
}
|
|
172
235
|
|
|
173
|
-
async function executeFigmaTool(toolName, args,
|
|
236
|
+
async function executeFigmaTool(toolName, args, context) {
|
|
174
237
|
switch (toolName) {
|
|
175
238
|
case 'figma_get_me':
|
|
176
|
-
return { result: await figmaRequest(
|
|
239
|
+
return { result: await figmaRequest(context, { path: '/v1/me' }) };
|
|
177
240
|
case 'figma_get_file':
|
|
178
241
|
return {
|
|
179
|
-
result: await figmaRequest(
|
|
242
|
+
result: await figmaRequest(context, {
|
|
180
243
|
path: `/v1/files/${encodeURIComponent(requireText(args.file_key, 'file_key'))}`,
|
|
181
244
|
query: {
|
|
182
245
|
ids: args.ids || undefined,
|
|
@@ -186,14 +249,14 @@ async function executeFigmaTool(toolName, args, { credentials }) {
|
|
|
186
249
|
};
|
|
187
250
|
case 'figma_get_file_nodes':
|
|
188
251
|
return {
|
|
189
|
-
result: await figmaRequest(
|
|
252
|
+
result: await figmaRequest(context, {
|
|
190
253
|
path: `/v1/files/${encodeURIComponent(requireText(args.file_key, 'file_key'))}/nodes`,
|
|
191
254
|
query: { ids: requireText(args.ids, 'ids') },
|
|
192
255
|
}),
|
|
193
256
|
};
|
|
194
257
|
case 'figma_get_file_images':
|
|
195
258
|
return {
|
|
196
|
-
result: await figmaRequest(
|
|
259
|
+
result: await figmaRequest(context, {
|
|
197
260
|
path: `/v1/images/${encodeURIComponent(requireText(args.file_key, 'file_key'))}`,
|
|
198
261
|
query: {
|
|
199
262
|
ids: requireText(args.ids, 'ids'),
|
|
@@ -204,13 +267,13 @@ async function executeFigmaTool(toolName, args, { credentials }) {
|
|
|
204
267
|
};
|
|
205
268
|
case 'figma_get_comments':
|
|
206
269
|
return {
|
|
207
|
-
result: await figmaRequest(
|
|
270
|
+
result: await figmaRequest(context, {
|
|
208
271
|
path: `/v1/files/${encodeURIComponent(requireText(args.file_key, 'file_key'))}/comments`,
|
|
209
272
|
}),
|
|
210
273
|
};
|
|
211
274
|
case 'figma_post_comment':
|
|
212
275
|
return {
|
|
213
|
-
result: await figmaRequest(
|
|
276
|
+
result: await figmaRequest(context, {
|
|
214
277
|
method: 'POST',
|
|
215
278
|
path: `/v1/files/${encodeURIComponent(requireText(args.file_key, 'file_key'))}/comments`,
|
|
216
279
|
body: {
|
|
@@ -221,7 +284,7 @@ async function executeFigmaTool(toolName, args, { credentials }) {
|
|
|
221
284
|
};
|
|
222
285
|
case 'figma_api_request':
|
|
223
286
|
return {
|
|
224
|
-
result: await figmaRequest(
|
|
287
|
+
result: await figmaRequest(context, {
|
|
225
288
|
method: args.method,
|
|
226
289
|
path: requireText(args.path, 'path'),
|
|
227
290
|
query: args.query,
|
|
@@ -263,7 +326,7 @@ function createFigmaProvider() {
|
|
|
263
326
|
appId: app.id,
|
|
264
327
|
};
|
|
265
328
|
},
|
|
266
|
-
async finishOAuth({ code, app }) {
|
|
329
|
+
async finishOAuth({ code, app, signal }) {
|
|
267
330
|
const config = resolveFigmaOAuthConfig();
|
|
268
331
|
const basic = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString(
|
|
269
332
|
'base64',
|
|
@@ -280,6 +343,7 @@ function createFigmaProvider() {
|
|
|
280
343
|
code,
|
|
281
344
|
grant_type: 'authorization_code',
|
|
282
345
|
},
|
|
346
|
+
signal,
|
|
283
347
|
},
|
|
284
348
|
{ serviceName: 'Figma' },
|
|
285
349
|
);
|
|
@@ -290,6 +354,7 @@ function createFigmaProvider() {
|
|
|
290
354
|
headers: {
|
|
291
355
|
Authorization: `Bearer ${token.access_token}`,
|
|
292
356
|
},
|
|
357
|
+
signal,
|
|
293
358
|
},
|
|
294
359
|
{ serviceName: 'Figma' },
|
|
295
360
|
);
|
|
@@ -307,6 +372,7 @@ function createFigmaProvider() {
|
|
|
307
372
|
access_token: token.access_token,
|
|
308
373
|
refresh_token: token.refresh_token,
|
|
309
374
|
expires_in: token.expires_in,
|
|
375
|
+
expires_at: expiresAtFromSeconds(token.expires_in),
|
|
310
376
|
token_type: token.token_type,
|
|
311
377
|
scope: token.scope,
|
|
312
378
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const { fetchResponseText } = require('../http');
|
|
4
5
|
|
|
5
6
|
function base64UrlSha256(value) {
|
|
6
7
|
return crypto
|
|
@@ -46,15 +47,19 @@ async function githubApiRequest(auth, options = {}) {
|
|
|
46
47
|
headers['Content-Type'] = 'application/json';
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
const response = await
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
const { response, text: rawBody } = await fetchResponseText(
|
|
51
|
+
url.toString(),
|
|
52
|
+
{
|
|
53
|
+
method,
|
|
54
|
+
headers,
|
|
55
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
56
|
+
signal: options.signal || auth?.signal,
|
|
57
|
+
},
|
|
58
|
+
{ serviceName: 'GitHub API' },
|
|
59
|
+
);
|
|
54
60
|
|
|
55
61
|
let data = null;
|
|
56
62
|
if (response.status !== 204 && response.status !== 205) {
|
|
57
|
-
const rawBody = await response.text();
|
|
58
63
|
if (rawBody.trim()) {
|
|
59
64
|
try {
|
|
60
65
|
data = JSON.parse(rawBody);
|
|
@@ -6,6 +6,8 @@ const { decryptValue } = require('../secrets');
|
|
|
6
6
|
const { getConnectionAccessMode } = require('../access');
|
|
7
7
|
const { githubToolDefinitions, executeGithubTool } = require('./repos');
|
|
8
8
|
const { base64UrlSha256 } = require('./common');
|
|
9
|
+
const { fetchJson } = require('../oauth_provider');
|
|
10
|
+
const { fetchResponseText } = require('../http');
|
|
9
11
|
|
|
10
12
|
const GITHUB_ACCOUNT_IDENTITY_SCOPES = [
|
|
11
13
|
'read:user',
|
|
@@ -159,8 +161,9 @@ function summarizeAppConnection(app, connectionRows, envStatus) {
|
|
|
159
161
|
};
|
|
160
162
|
}
|
|
161
163
|
|
|
162
|
-
async function executeGithubRepoTool(toolName, args, connection) {
|
|
164
|
+
async function executeGithubRepoTool(toolName, args, connection, executionOptions = {}) {
|
|
163
165
|
const auth = await buildAuthorizedClient(connection);
|
|
166
|
+
auth.signal = executionOptions.signal || null;
|
|
164
167
|
if (!auth.token) {
|
|
165
168
|
throw new Error('GitHub access token is missing or expired. Please reconnect your GitHub account.');
|
|
166
169
|
}
|
|
@@ -326,40 +329,32 @@ function createGithubProvider() {
|
|
|
326
329
|
const url = `https://github.com/login/oauth/authorize?${params.toString()}`;
|
|
327
330
|
return { url, appId: app.id };
|
|
328
331
|
},
|
|
329
|
-
async finishOAuth({ code, codeVerifier, appKey }) {
|
|
332
|
+
async finishOAuth({ code, codeVerifier, appKey, signal }) {
|
|
330
333
|
const app = getApp(appKey);
|
|
331
334
|
if (!app) {
|
|
332
335
|
throw new Error(`Unknown GitHub app: ${appKey}`);
|
|
333
336
|
}
|
|
334
337
|
const { config } = createOAuthClient();
|
|
335
338
|
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
339
|
+
const tokenData = await fetchJson(
|
|
340
|
+
'https://github.com/login/oauth/access_token',
|
|
341
|
+
{
|
|
342
|
+
method: 'POST',
|
|
343
|
+
form: {
|
|
344
|
+
client_id: config.clientId,
|
|
345
|
+
client_secret: config.clientSecret,
|
|
346
|
+
code,
|
|
347
|
+
code_verifier: codeVerifier,
|
|
348
|
+
redirect_uri: config.redirectUri,
|
|
349
|
+
},
|
|
350
|
+
signal,
|
|
341
351
|
},
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}).toString(),
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
const tokenBody = await tokenResponse.text();
|
|
352
|
-
let tokenData = {};
|
|
353
|
-
try {
|
|
354
|
-
tokenData = tokenBody ? JSON.parse(tokenBody) : {};
|
|
355
|
-
} catch {
|
|
356
|
-
tokenData = {};
|
|
357
|
-
}
|
|
358
|
-
if (!tokenResponse.ok) {
|
|
359
|
-
throw new Error(`GitHub OAuth token exchange failed (${tokenResponse.status}): ${tokenBody || 'No response body'}`);
|
|
360
|
-
}
|
|
361
|
-
if (tokenData.error) {
|
|
362
|
-
throw new Error(`GitHub OAuth error: ${tokenData.error_description || tokenData.error}`);
|
|
352
|
+
{ serviceName: 'GitHub OAuth token exchange' },
|
|
353
|
+
);
|
|
354
|
+
if (tokenData?.error) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`GitHub OAuth error: ${tokenData.error_description || tokenData.error}`,
|
|
357
|
+
);
|
|
363
358
|
}
|
|
364
359
|
|
|
365
360
|
const accessToken = tokenData.access_token;
|
|
@@ -367,17 +362,17 @@ function createGithubProvider() {
|
|
|
367
362
|
throw new Error('GitHub OAuth did not return an access token.');
|
|
368
363
|
}
|
|
369
364
|
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
365
|
+
const userData = await fetchJson(
|
|
366
|
+
'https://api.github.com/user',
|
|
367
|
+
{
|
|
368
|
+
headers: {
|
|
369
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
370
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
371
|
+
},
|
|
372
|
+
signal,
|
|
374
373
|
},
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const errorBody = await userResponse.text().catch(() => 'Unknown error');
|
|
378
|
-
throw new Error(`GitHub user profile request failed (${userResponse.status}): ${errorBody}`);
|
|
379
|
-
}
|
|
380
|
-
const userData = await userResponse.json();
|
|
374
|
+
{ serviceName: 'GitHub user profile' },
|
|
375
|
+
);
|
|
381
376
|
const accountEmail = String(userData.login || '').trim();
|
|
382
377
|
if (!accountEmail) {
|
|
383
378
|
throw new Error('GitHub API did not return a user login.');
|
|
@@ -398,7 +393,7 @@ function createGithubProvider() {
|
|
|
398
393
|
},
|
|
399
394
|
};
|
|
400
395
|
},
|
|
401
|
-
async disconnect(connectionRow) {
|
|
396
|
+
async disconnect(connectionRow, executionOptions = {}) {
|
|
402
397
|
if (!connectionRow?.credentials_json) return;
|
|
403
398
|
try {
|
|
404
399
|
const credentials = JSON.parse(decryptValue(connectionRow.credentials_json));
|
|
@@ -415,20 +410,24 @@ function createGithubProvider() {
|
|
|
415
410
|
|
|
416
411
|
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
417
412
|
const revokeUrl = `https://api.github.com/applications/${encodeURIComponent(clientId)}/token`;
|
|
418
|
-
const revokeResponse = await
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
'
|
|
422
|
-
|
|
423
|
-
|
|
413
|
+
const { response: revokeResponse, text: revokeBody } = await fetchResponseText(
|
|
414
|
+
revokeUrl,
|
|
415
|
+
{
|
|
416
|
+
method: 'DELETE',
|
|
417
|
+
headers: {
|
|
418
|
+
'Authorization': `Basic ${basic}`,
|
|
419
|
+
'Accept': 'application/json',
|
|
420
|
+
'Content-Type': 'application/json',
|
|
421
|
+
},
|
|
422
|
+
body: JSON.stringify({
|
|
423
|
+
access_token: accessToken,
|
|
424
|
+
}),
|
|
425
|
+
signal: executionOptions.signal || null,
|
|
424
426
|
},
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}),
|
|
428
|
-
});
|
|
427
|
+
{ serviceName: 'GitHub token revocation' },
|
|
428
|
+
);
|
|
429
429
|
|
|
430
430
|
if (!revokeResponse.ok) {
|
|
431
|
-
const revokeBody = await revokeResponse.text().catch(() => 'Unknown error');
|
|
432
431
|
console.warn(
|
|
433
432
|
`[GitHub] Failed to revoke token for disconnect (connection ${connectionRow?.id || 'unknown'}): ${revokeResponse.status} ${revokeBody}`,
|
|
434
433
|
);
|
|
@@ -439,8 +438,8 @@ function createGithubProvider() {
|
|
|
439
438
|
);
|
|
440
439
|
}
|
|
441
440
|
},
|
|
442
|
-
async executeTool(toolName, args, connectionRow) {
|
|
443
|
-
return executeGithubRepoTool(toolName, args, connectionRow);
|
|
441
|
+
async executeTool(toolName, args, connectionRow, executionOptions = {}) {
|
|
442
|
+
return executeGithubRepoTool(toolName, args, connectionRow, executionOptions);
|
|
444
443
|
},
|
|
445
444
|
summarizeConnection(connectionRows) {
|
|
446
445
|
const snapshot = this.buildSnapshot(connectionRows);
|
|
@@ -496,4 +495,4 @@ module.exports = {
|
|
|
496
495
|
scopes: scopes.slice(),
|
|
497
496
|
}),
|
|
498
497
|
),
|
|
499
|
-
};
|
|
498
|
+
};
|