neoagent 2.2.1-beta.7 → 2.3.0
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/index.md +1 -0
- package/docs/migration.md +238 -0
- package/lib/manager.js +99 -2
- package/lib/migrations.js +409 -0
- package/package.json +1 -1
- package/server/catalog_sources/store-bundles/skills/productivity/migration/SKILL.md +173 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +72170 -70842
- package/server/routes/auth.js +13 -5
- package/server/routes/integrations.js +22 -0
- package/server/routes/messaging.js +41 -5
- package/server/routes/settings.js +1 -0
- package/server/services/integrations/google/provider.js +20 -2
- package/server/services/integrations/manager.js +79 -8
- package/server/services/messaging/access_policy.js +703 -0
- package/server/services/messaging/access_policy.test.js +228 -0
- package/server/services/messaging/automation.js +32 -95
- package/server/services/messaging/base.js +39 -0
- package/server/services/messaging/discord.js +61 -46
- package/server/services/messaging/http_platforms.js +178 -15
- package/server/services/messaging/manager.js +136 -69
- package/server/services/messaging/telegram.js +54 -40
- package/server/services/messaging/telnyx.js +43 -14
- package/server/services/messaging/whatsapp.js +27 -0
package/server/routes/auth.js
CHANGED
|
@@ -239,18 +239,26 @@ function updateLastLogin(userId) {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
router.get('/api/auth/status', (req, res) => {
|
|
242
|
+
const count = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
|
243
|
+
const policy = getDeploymentPolicy();
|
|
244
|
+
const emailConfig = getEmailConfig();
|
|
245
|
+
const authProviderManager = getAuthProviderManager(req);
|
|
242
246
|
const currentUser = readAuthenticatedUser(req);
|
|
243
247
|
if (!currentUser) {
|
|
244
248
|
return res.json({
|
|
249
|
+
hasUser: count.count > 0,
|
|
250
|
+
registrationOpen: policy.registrationOpen || count.count === 0,
|
|
251
|
+
deploymentProfile: policy.profile,
|
|
245
252
|
authenticated: false,
|
|
246
253
|
user: null,
|
|
254
|
+
email: {
|
|
255
|
+
enabled: emailConfig.enabled,
|
|
256
|
+
configured: emailConfig.configured,
|
|
257
|
+
signupConfirmationRequired: requireSignupEmailConfirmation(),
|
|
258
|
+
},
|
|
259
|
+
providers: authProviderManager ? authProviderManager.listProviders() : [],
|
|
247
260
|
});
|
|
248
261
|
}
|
|
249
|
-
|
|
250
|
-
const count = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
|
251
|
-
const policy = getDeploymentPolicy();
|
|
252
|
-
const emailConfig = getEmailConfig();
|
|
253
|
-
const authProviderManager = getAuthProviderManager(req);
|
|
254
262
|
res.json({
|
|
255
263
|
hasUser: count.count > 0,
|
|
256
264
|
registrationOpen: policy.registrationOpen || count.count === 0,
|
|
@@ -159,6 +159,7 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
|
|
|
159
159
|
if (!session) {
|
|
160
160
|
return res.status(404).send('Connection session not found.');
|
|
161
161
|
}
|
|
162
|
+
const trustedOrigin = JSON.stringify(getTrustedPostMessageOrigin(req));
|
|
162
163
|
const statusUrl = `/api/integrations/${encodeURIComponent(req.params.provider)}/connect/${encodeURIComponent(req.params.sessionId)}/status?agentId=${encodeURIComponent(agentId)}`;
|
|
163
164
|
res.send(`
|
|
164
165
|
<html>
|
|
@@ -188,6 +189,14 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
|
|
|
188
189
|
const statusEl = document.getElementById('status');
|
|
189
190
|
const qrEl = document.getElementById('qr');
|
|
190
191
|
const statusUrl = ${JSON.stringify(statusUrl)};
|
|
192
|
+
const trustedOrigin = ${trustedOrigin};
|
|
193
|
+
const provider = ${JSON.stringify(req.params.provider)};
|
|
194
|
+
const appId = ${JSON.stringify(session.appKey)};
|
|
195
|
+
|
|
196
|
+
function notifyOpener(payload) {
|
|
197
|
+
if (!window.opener) return;
|
|
198
|
+
window.opener.postMessage(payload, trustedOrigin);
|
|
199
|
+
}
|
|
191
200
|
|
|
192
201
|
async function refresh() {
|
|
193
202
|
const response = await fetch(statusUrl, { credentials: 'same-origin' });
|
|
@@ -204,11 +213,24 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
|
|
|
204
213
|
} else if (status === 'connected') {
|
|
205
214
|
qrEl.style.display = 'none';
|
|
206
215
|
statusEl.textContent = 'Connected as ' + (data.accountEmail || 'your WhatsApp account') + '. Closing…';
|
|
216
|
+
notifyOpener({
|
|
217
|
+
type: 'integration_oauth_success',
|
|
218
|
+
provider,
|
|
219
|
+
appId,
|
|
220
|
+
connectionId: data.connectionId || null,
|
|
221
|
+
accountEmail: data.accountEmail || null,
|
|
222
|
+
});
|
|
207
223
|
setTimeout(() => window.close(), 800);
|
|
208
224
|
return;
|
|
209
225
|
} else if (status === 'failed' || status === 'logged_out' || status === 'disconnected') {
|
|
210
226
|
qrEl.style.display = 'none';
|
|
211
227
|
statusEl.textContent = data.error || ('Connection ended with status: ' + status + '.');
|
|
228
|
+
notifyOpener({
|
|
229
|
+
type: 'integration_oauth_error',
|
|
230
|
+
provider,
|
|
231
|
+
appId,
|
|
232
|
+
error: data.error || ('Connection ended with status: ' + status + '.'),
|
|
233
|
+
});
|
|
212
234
|
return;
|
|
213
235
|
} else {
|
|
214
236
|
statusEl.textContent = 'Waiting for WhatsApp to generate a QR code…';
|
|
@@ -4,6 +4,11 @@ const db = require('../db/database');
|
|
|
4
4
|
const { requireAuth } = require('../middleware/auth');
|
|
5
5
|
const { sanitizeError } = require('../utils/security');
|
|
6
6
|
const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
|
|
7
|
+
const {
|
|
8
|
+
legacyWhitelistKey,
|
|
9
|
+
normalizeAccessPolicy,
|
|
10
|
+
summarizeAccessPolicy,
|
|
11
|
+
} = require('../services/messaging/access_policy');
|
|
7
12
|
|
|
8
13
|
const PREFIXED_ENTRY_RE = /[^0-9a-z:_.@#+=\-!$*]/gi;
|
|
9
14
|
|
|
@@ -135,6 +140,37 @@ router.get('/status/:platform', (req, res) => {
|
|
|
135
140
|
res.json(manager.getPlatformStatus(req.session.userId, req.params.platform, { agentId }));
|
|
136
141
|
});
|
|
137
142
|
|
|
143
|
+
router.get('/:platform/access-policy', async (req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
const manager = req.app.locals.messagingManager;
|
|
146
|
+
const platform = String(req.params.platform || '').replace(/[^0-9a-z_+-]/gi, '');
|
|
147
|
+
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
148
|
+
if (!platform) return res.status(400).json({ error: 'platform is required' });
|
|
149
|
+
const catalog = await manager.getAccessCatalog(req.session.userId, platform, { agentId });
|
|
150
|
+
res.json(catalog);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
router.put('/:platform/access-policy', (req, res) => {
|
|
157
|
+
try {
|
|
158
|
+
const manager = req.app.locals.messagingManager;
|
|
159
|
+
const platform = String(req.params.platform || '').replace(/[^0-9a-z_+-]/gi, '');
|
|
160
|
+
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
161
|
+
if (!platform) return res.status(400).json({ error: 'platform is required' });
|
|
162
|
+
const policy = normalizeAccessPolicy(platform, req.body?.policy || req.body || {});
|
|
163
|
+
const saved = manager.setAccessPolicy(req.session.userId, platform, policy, { agentId });
|
|
164
|
+
res.json({
|
|
165
|
+
success: true,
|
|
166
|
+
policy: saved,
|
|
167
|
+
summary: summarizeAccessPolicy(platform, saved),
|
|
168
|
+
});
|
|
169
|
+
} catch (err) {
|
|
170
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
138
174
|
router.get('/:platform/devices', (req, res) => {
|
|
139
175
|
try {
|
|
140
176
|
const manager = req.app.locals.messagingManager;
|
|
@@ -167,9 +203,9 @@ router.put('/telnyx/whitelist', (req, res) => {
|
|
|
167
203
|
if (!Array.isArray(numbers)) return res.status(400).json({ error: 'numbers must be an array' });
|
|
168
204
|
const list = numbers.map(n => n.replace(/[^0-9+]/g, '')).filter(Boolean);
|
|
169
205
|
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
170
|
-
upsertAgentSetting(req.session.userId, agentId, '
|
|
206
|
+
upsertAgentSetting(req.session.userId, agentId, legacyWhitelistKey('telnyx'), list);
|
|
171
207
|
const manager = req.app.locals.messagingManager;
|
|
172
|
-
|
|
208
|
+
const policy = manager ? manager.updateTelnyxAllowedNumbers(req.session.userId, list, { agentId }) : null;
|
|
173
209
|
res.json({ success: true, numbers: list });
|
|
174
210
|
} catch (err) {
|
|
175
211
|
res.status(500).json({ error: sanitizeError(err) });
|
|
@@ -184,7 +220,7 @@ router.put('/discord/whitelist', (req, res) => {
|
|
|
184
220
|
// Keep prefixed format, strip only clearly unsafe characters
|
|
185
221
|
const list = ids.map(id => String(id).replace(/[^0-9a-z:_-]/gi, '')).filter(Boolean);
|
|
186
222
|
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
187
|
-
upsertAgentSetting(req.session.userId, agentId, '
|
|
223
|
+
upsertAgentSetting(req.session.userId, agentId, legacyWhitelistKey('discord'), list);
|
|
188
224
|
const manager = req.app.locals.messagingManager;
|
|
189
225
|
if (manager) manager.updateDiscordAllowedIds(req.session.userId, list, { agentId });
|
|
190
226
|
res.json({ success: true, ids: list });
|
|
@@ -201,7 +237,7 @@ router.put('/telegram/whitelist', (req, res) => {
|
|
|
201
237
|
// Keep prefixed format; group IDs are negative so allow minus sign
|
|
202
238
|
const list = ids.map(id => String(id).replace(/[^0-9a-z:_-]/gi, '')).filter(Boolean);
|
|
203
239
|
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
204
|
-
upsertAgentSetting(req.session.userId, agentId, '
|
|
240
|
+
upsertAgentSetting(req.session.userId, agentId, legacyWhitelistKey('telegram'), list);
|
|
205
241
|
const manager = req.app.locals.messagingManager;
|
|
206
242
|
if (manager) manager.updateTelegramAllowedIds(req.session.userId, list, { agentId });
|
|
207
243
|
res.json({ success: true, ids: list });
|
|
@@ -219,7 +255,7 @@ router.put('/:platform/whitelist', (req, res) => {
|
|
|
219
255
|
if (!platform) return res.status(400).json({ error: 'platform is required' });
|
|
220
256
|
const list = ids.map(id => String(id).replace(PREFIXED_ENTRY_RE, '')).filter(Boolean);
|
|
221
257
|
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
222
|
-
upsertAgentSetting(req.session.userId, agentId,
|
|
258
|
+
upsertAgentSetting(req.session.userId, agentId, legacyWhitelistKey(platform), list);
|
|
223
259
|
const manager = req.app.locals.messagingManager;
|
|
224
260
|
if (manager?.updateAllowedEntries) manager.updateAllowedEntries(req.session.userId, platform, list, { agentId });
|
|
225
261
|
res.json({ success: true, ids: list });
|
|
@@ -124,6 +124,7 @@ router.use(requireAuth);
|
|
|
124
124
|
function isAgentScopedSettingKey(key) {
|
|
125
125
|
return AGENT_SETTING_KEYS.has(key)
|
|
126
126
|
|| key.startsWith('platform_whitelist_')
|
|
127
|
+
|| key.startsWith('platform_access_policy_')
|
|
127
128
|
|| key === 'platform_voice_secret_telnyx';
|
|
128
129
|
}
|
|
129
130
|
|
|
@@ -144,6 +144,15 @@ function getAppScopes(appId) {
|
|
|
144
144
|
return Array.from(new Set([...GOOGLE_ACCOUNT_IDENTITY_SCOPES, ...app.scopes]));
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function getAllWorkspaceScopes() {
|
|
148
|
+
return Array.from(
|
|
149
|
+
new Set([
|
|
150
|
+
...GOOGLE_ACCOUNT_IDENTITY_SCOPES,
|
|
151
|
+
...GOOGLE_WORKSPACE_APPS.flatMap((app) => app.scopes || []),
|
|
152
|
+
]),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
function sortConnections(rows) {
|
|
148
157
|
return rows.slice().sort((left, right) => {
|
|
149
158
|
const leftEmail = String(left.account_email || '').toLowerCase();
|
|
@@ -381,7 +390,9 @@ function createGoogleWorkspaceProvider() {
|
|
|
381
390
|
const url = client.generateAuthUrl({
|
|
382
391
|
access_type: 'offline',
|
|
383
392
|
prompt: 'consent',
|
|
384
|
-
scope
|
|
393
|
+
// Request the full workspace scope bundle so one durable Google grant can
|
|
394
|
+
// back Gmail, Calendar, Drive, Docs, and Sheets for the same account.
|
|
395
|
+
scope: getAllWorkspaceScopes(),
|
|
385
396
|
include_granted_scopes: true,
|
|
386
397
|
state,
|
|
387
398
|
code_challenge: codeChallenge,
|
|
@@ -410,7 +421,14 @@ function createGoogleWorkspaceProvider() {
|
|
|
410
421
|
appId: app.id,
|
|
411
422
|
accountEmail,
|
|
412
423
|
credentials: client.credentials,
|
|
413
|
-
scopes:
|
|
424
|
+
scopes: Array.from(
|
|
425
|
+
new Set(
|
|
426
|
+
String(client.credentials.scope || '')
|
|
427
|
+
.split(/\s+/)
|
|
428
|
+
.map((scope) => scope.trim())
|
|
429
|
+
.filter(Boolean),
|
|
430
|
+
),
|
|
431
|
+
),
|
|
414
432
|
metadata: {
|
|
415
433
|
appId: app.id,
|
|
416
434
|
},
|
|
@@ -53,6 +53,58 @@ class IntegrationManager {
|
|
|
53
53
|
return merged;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
findReusableConnection(userId, agentId, providerKey, accountEmail, options = {}) {
|
|
57
|
+
const normalizedEmail = String(accountEmail || '').trim().toLowerCase();
|
|
58
|
+
if (!normalizedEmail) return null;
|
|
59
|
+
const excludedConnectionId = Number(options.excludeConnectionId);
|
|
60
|
+
const connections = this.listConnections(userId, providerKey, agentId).filter(
|
|
61
|
+
(connection) =>
|
|
62
|
+
String(connection.account_email || '').trim().toLowerCase() === normalizedEmail &&
|
|
63
|
+
connection.status === 'connected' &&
|
|
64
|
+
(!Number.isInteger(excludedConnectionId) || connection.id !== excludedConnectionId),
|
|
65
|
+
);
|
|
66
|
+
if (connections.length === 0) return null;
|
|
67
|
+
return connections.sort((left, right) =>
|
|
68
|
+
String(right.updated_at || '').localeCompare(String(left.updated_at || '')),
|
|
69
|
+
)[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
mergeWithReusableCredentials(userId, agentId, providerKey, accountEmail, credentials, options = {}) {
|
|
73
|
+
const reusableConnection = this.findReusableConnection(
|
|
74
|
+
userId,
|
|
75
|
+
agentId,
|
|
76
|
+
providerKey,
|
|
77
|
+
accountEmail,
|
|
78
|
+
options,
|
|
79
|
+
);
|
|
80
|
+
if (!reusableConnection) {
|
|
81
|
+
return credentials && typeof credentials === 'object' ? credentials : {};
|
|
82
|
+
}
|
|
83
|
+
const reusableCredentials = this.parseCredentials(
|
|
84
|
+
reusableConnection.credentials_json,
|
|
85
|
+
);
|
|
86
|
+
return this.mergeUpdatedCredentials(
|
|
87
|
+
reusableCredentials,
|
|
88
|
+
credentials,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
persistSharedCredentials(userId, agentId, providerKey, accountEmail, credentials) {
|
|
93
|
+
const normalizedEmail = String(accountEmail || '').trim();
|
|
94
|
+
if (!normalizedEmail) return;
|
|
95
|
+
db.prepare(
|
|
96
|
+
`UPDATE integration_connections
|
|
97
|
+
SET credentials_json = ?, updated_at = datetime('now')
|
|
98
|
+
WHERE user_id = ? AND agent_id = ? AND provider_key = ? AND lower(account_email) = lower(?)`,
|
|
99
|
+
).run(
|
|
100
|
+
encryptValue(JSON.stringify(credentials || {})),
|
|
101
|
+
userId,
|
|
102
|
+
agentId,
|
|
103
|
+
providerKey,
|
|
104
|
+
normalizedEmail,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
56
108
|
getProvider(providerKey) {
|
|
57
109
|
return this.registry.get(providerKey);
|
|
58
110
|
}
|
|
@@ -218,6 +270,19 @@ class IntegrationManager {
|
|
|
218
270
|
appKey: stateRow.app_key,
|
|
219
271
|
});
|
|
220
272
|
|
|
273
|
+
const mergedCredentials = this.mergeWithReusableCredentials(
|
|
274
|
+
stateRow.user_id,
|
|
275
|
+
stateRow.agent_id || resolveAgentId(stateRow.user_id, null),
|
|
276
|
+
provider.key,
|
|
277
|
+
result.accountEmail,
|
|
278
|
+
result.credentials,
|
|
279
|
+
);
|
|
280
|
+
if (!mergedCredentials.refresh_token) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`${provider.label} did not return a refresh token, so the connection would expire. Revoke the existing app grant for this provider and reconnect it so offline access is granted.`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
221
286
|
db.prepare(
|
|
222
287
|
`INSERT INTO integration_connections (
|
|
223
288
|
user_id,
|
|
@@ -246,10 +311,18 @@ class IntegrationManager {
|
|
|
246
311
|
stateRow.app_key,
|
|
247
312
|
result.accountEmail,
|
|
248
313
|
JSON.stringify(result.scopes || []),
|
|
249
|
-
encryptValue(JSON.stringify(
|
|
314
|
+
encryptValue(JSON.stringify(mergedCredentials)),
|
|
250
315
|
JSON.stringify(result.metadata || {}),
|
|
251
316
|
);
|
|
252
317
|
|
|
318
|
+
this.persistSharedCredentials(
|
|
319
|
+
stateRow.user_id,
|
|
320
|
+
stateRow.agent_id || resolveAgentId(stateRow.user_id, null),
|
|
321
|
+
provider.key,
|
|
322
|
+
result.accountEmail,
|
|
323
|
+
mergedCredentials,
|
|
324
|
+
);
|
|
325
|
+
|
|
253
326
|
const connection = db
|
|
254
327
|
.prepare(
|
|
255
328
|
`SELECT * FROM integration_connections
|
|
@@ -548,14 +621,12 @@ class IntegrationManager {
|
|
|
548
621
|
existingCredentials,
|
|
549
622
|
execution.credentials,
|
|
550
623
|
);
|
|
551
|
-
|
|
552
|
-
`UPDATE integration_connections
|
|
553
|
-
SET credentials_json = ?, updated_at = datetime('now')
|
|
554
|
-
WHERE id = ? AND user_id = ?`,
|
|
555
|
-
).run(
|
|
556
|
-
encryptValue(JSON.stringify(mergedCredentials)),
|
|
557
|
-
selection.connection.id,
|
|
624
|
+
this.persistSharedCredentials(
|
|
558
625
|
userId,
|
|
626
|
+
resolveAgentId(userId, agentId),
|
|
627
|
+
provider.key,
|
|
628
|
+
selection.connection.account_email,
|
|
629
|
+
mergedCredentials,
|
|
559
630
|
);
|
|
560
631
|
}
|
|
561
632
|
|