neoagent 3.2.1-beta.4 → 3.2.1-beta.5
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/extensions/chrome-browser/protocol.mjs +143 -1
- package/flutter_app/lib/main_controller.dart +82 -0
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/src/backend_client.dart +78 -0
- package/lib/schema_migrations.js +48 -0
- package/package.json +10 -2
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +33 -10
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +69495 -68619
- package/server/routes/integrations.js +102 -0
- package/server/services/ai/systemPrompt.js +2 -2
- package/server/services/ai/tools.js +77 -0
- package/server/services/browser/controller.js +107 -0
- package/server/services/browser/extension/protocol.js +3 -0
- package/server/services/browser/extension/provider.js +12 -0
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- 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/manager.js +1 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/manager.js +23 -0
- package/server/services/runtime/backends/local-vm.js +13 -1
- package/server/services/runtime/guest_bootstrap.js +23 -4
- package/server/services/runtime/guest_image.js +4 -3
- package/server/services/runtime/manager.js +25 -11
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
|
@@ -230,6 +230,9 @@ void _openOfficialIntegrationSetupDialog(
|
|
|
230
230
|
String providerId,
|
|
231
231
|
) {
|
|
232
232
|
switch (providerId) {
|
|
233
|
+
case 'bitwarden':
|
|
234
|
+
_showBitwardenSetupDialog(context, controller);
|
|
235
|
+
return;
|
|
233
236
|
case 'home_assistant':
|
|
234
237
|
_showHomeAssistantSetupDialog(context, controller);
|
|
235
238
|
return;
|
|
@@ -245,6 +248,587 @@ void _openOfficialIntegrationSetupDialog(
|
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
Future<void> _showBitwardenBindingDialog(
|
|
252
|
+
BuildContext context,
|
|
253
|
+
NeoAgentController controller, {
|
|
254
|
+
required int connectionId,
|
|
255
|
+
}) async {
|
|
256
|
+
List<Map<String, dynamic>> items;
|
|
257
|
+
try {
|
|
258
|
+
items = await controller.fetchBitwardenItems();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (context.mounted) {
|
|
261
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
262
|
+
SnackBar(content: Text(controller.errorMessage ?? error.toString())),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (items.isEmpty || !context.mounted) return;
|
|
268
|
+
String itemId = items.first['id']?.toString() ?? '';
|
|
269
|
+
var usageType = 'browser';
|
|
270
|
+
var saving = false;
|
|
271
|
+
var errorText = '';
|
|
272
|
+
final aliasController = TextEditingController();
|
|
273
|
+
final originController = TextEditingController(
|
|
274
|
+
text: ((items.first['origins'] as List?)?.firstOrNull)?.toString() ?? '',
|
|
275
|
+
);
|
|
276
|
+
final pathController = TextEditingController(text: '/');
|
|
277
|
+
final headerController = TextEditingController(text: 'X-API-Key');
|
|
278
|
+
var authType = 'bearer';
|
|
279
|
+
var secretField = 'login.password';
|
|
280
|
+
|
|
281
|
+
await showDialog<void>(
|
|
282
|
+
context: context,
|
|
283
|
+
barrierDismissible: false,
|
|
284
|
+
builder: (dialogContext) => StatefulBuilder(
|
|
285
|
+
builder: (dialogContext, setState) {
|
|
286
|
+
final selected = items.firstWhere(
|
|
287
|
+
(item) => item['id']?.toString() == itemId,
|
|
288
|
+
orElse: () => items.first,
|
|
289
|
+
);
|
|
290
|
+
final customFields = (selected['fields'] as List? ?? const [])
|
|
291
|
+
.whereType<Map>()
|
|
292
|
+
.map(
|
|
293
|
+
(field) => (
|
|
294
|
+
field['id']?.toString().isNotEmpty == true
|
|
295
|
+
? field['id'].toString()
|
|
296
|
+
: field['name'].toString(),
|
|
297
|
+
field['name']?.toString() ?? 'Custom field',
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
.toList();
|
|
301
|
+
final secretOptions = <(String, String)>[
|
|
302
|
+
('login.password', 'Login password'),
|
|
303
|
+
...customFields,
|
|
304
|
+
];
|
|
305
|
+
if (!secretOptions.any((entry) => entry.$1 == secretField)) {
|
|
306
|
+
secretField = secretOptions.first.$1;
|
|
307
|
+
}
|
|
308
|
+
return AlertDialog(
|
|
309
|
+
title: const Text('Add credential binding'),
|
|
310
|
+
content: SizedBox(
|
|
311
|
+
width: 520,
|
|
312
|
+
child: SingleChildScrollView(
|
|
313
|
+
child: Column(
|
|
314
|
+
mainAxisSize: MainAxisSize.min,
|
|
315
|
+
children: <Widget>[
|
|
316
|
+
DropdownButtonFormField<String>(
|
|
317
|
+
initialValue: itemId,
|
|
318
|
+
decoration: const InputDecoration(
|
|
319
|
+
labelText: 'Bitwarden item',
|
|
320
|
+
border: OutlineInputBorder(),
|
|
321
|
+
),
|
|
322
|
+
items: items
|
|
323
|
+
.map(
|
|
324
|
+
(item) => DropdownMenuItem(
|
|
325
|
+
value: item['id']?.toString(),
|
|
326
|
+
child: Text(item['name']?.toString() ?? 'Untitled'),
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
.toList(),
|
|
330
|
+
onChanged: saving
|
|
331
|
+
? null
|
|
332
|
+
: (value) => setState(() {
|
|
333
|
+
itemId = value ?? itemId;
|
|
334
|
+
final item = items.firstWhere(
|
|
335
|
+
(candidate) =>
|
|
336
|
+
candidate['id']?.toString() == itemId,
|
|
337
|
+
);
|
|
338
|
+
final origins = item['origins'] as List?;
|
|
339
|
+
if (origins?.isNotEmpty == true) {
|
|
340
|
+
originController.text = origins!.first.toString();
|
|
341
|
+
}
|
|
342
|
+
}),
|
|
343
|
+
),
|
|
344
|
+
const SizedBox(height: 12),
|
|
345
|
+
TextField(
|
|
346
|
+
controller: aliasController,
|
|
347
|
+
decoration: const InputDecoration(
|
|
348
|
+
labelText: 'Agent-visible name',
|
|
349
|
+
hintText: 'Work account',
|
|
350
|
+
border: OutlineInputBorder(),
|
|
351
|
+
),
|
|
352
|
+
),
|
|
353
|
+
const SizedBox(height: 12),
|
|
354
|
+
DropdownButtonFormField<String>(
|
|
355
|
+
initialValue: usageType,
|
|
356
|
+
decoration: const InputDecoration(
|
|
357
|
+
labelText: 'Use for',
|
|
358
|
+
border: OutlineInputBorder(),
|
|
359
|
+
),
|
|
360
|
+
items: const [
|
|
361
|
+
DropdownMenuItem(
|
|
362
|
+
value: 'browser',
|
|
363
|
+
child: Text('Browser login'),
|
|
364
|
+
),
|
|
365
|
+
DropdownMenuItem(
|
|
366
|
+
value: 'http',
|
|
367
|
+
child: Text('API request'),
|
|
368
|
+
),
|
|
369
|
+
],
|
|
370
|
+
onChanged: saving
|
|
371
|
+
? null
|
|
372
|
+
: (value) =>
|
|
373
|
+
setState(() => usageType = value ?? usageType),
|
|
374
|
+
),
|
|
375
|
+
const SizedBox(height: 12),
|
|
376
|
+
TextField(
|
|
377
|
+
controller: originController,
|
|
378
|
+
keyboardType: TextInputType.url,
|
|
379
|
+
decoration: InputDecoration(
|
|
380
|
+
labelText: usageType == 'browser'
|
|
381
|
+
? 'Allowed HTTPS origin'
|
|
382
|
+
: 'API HTTPS origin',
|
|
383
|
+
border: const OutlineInputBorder(),
|
|
384
|
+
),
|
|
385
|
+
),
|
|
386
|
+
if (usageType == 'http') ...[
|
|
387
|
+
const SizedBox(height: 12),
|
|
388
|
+
TextField(
|
|
389
|
+
controller: pathController,
|
|
390
|
+
decoration: const InputDecoration(
|
|
391
|
+
labelText: 'Allowed path prefix',
|
|
392
|
+
border: OutlineInputBorder(),
|
|
393
|
+
),
|
|
394
|
+
),
|
|
395
|
+
const SizedBox(height: 12),
|
|
396
|
+
DropdownButtonFormField<String>(
|
|
397
|
+
initialValue: authType,
|
|
398
|
+
decoration: const InputDecoration(
|
|
399
|
+
labelText: 'Authentication',
|
|
400
|
+
border: OutlineInputBorder(),
|
|
401
|
+
),
|
|
402
|
+
items: const [
|
|
403
|
+
DropdownMenuItem(
|
|
404
|
+
value: 'bearer',
|
|
405
|
+
child: Text('Bearer token'),
|
|
406
|
+
),
|
|
407
|
+
DropdownMenuItem(
|
|
408
|
+
value: 'basic',
|
|
409
|
+
child: Text('Basic authentication'),
|
|
410
|
+
),
|
|
411
|
+
DropdownMenuItem(
|
|
412
|
+
value: 'header',
|
|
413
|
+
child: Text('Custom header'),
|
|
414
|
+
),
|
|
415
|
+
],
|
|
416
|
+
onChanged: saving
|
|
417
|
+
? null
|
|
418
|
+
: (value) =>
|
|
419
|
+
setState(() => authType = value ?? authType),
|
|
420
|
+
),
|
|
421
|
+
const SizedBox(height: 12),
|
|
422
|
+
DropdownButtonFormField<String>(
|
|
423
|
+
initialValue: secretField,
|
|
424
|
+
decoration: const InputDecoration(
|
|
425
|
+
labelText: 'Secret field',
|
|
426
|
+
border: OutlineInputBorder(),
|
|
427
|
+
),
|
|
428
|
+
items: secretOptions
|
|
429
|
+
.map(
|
|
430
|
+
(entry) => DropdownMenuItem(
|
|
431
|
+
value: entry.$1,
|
|
432
|
+
child: Text(entry.$2),
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
.toList(),
|
|
436
|
+
onChanged: saving
|
|
437
|
+
? null
|
|
438
|
+
: (value) => setState(
|
|
439
|
+
() => secretField = value ?? secretField,
|
|
440
|
+
),
|
|
441
|
+
),
|
|
442
|
+
if (authType == 'header') ...[
|
|
443
|
+
const SizedBox(height: 12),
|
|
444
|
+
TextField(
|
|
445
|
+
controller: headerController,
|
|
446
|
+
decoration: const InputDecoration(
|
|
447
|
+
labelText: 'Header name',
|
|
448
|
+
border: OutlineInputBorder(),
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
],
|
|
452
|
+
],
|
|
453
|
+
if (errorText.isNotEmpty) ...[
|
|
454
|
+
const SizedBox(height: 12),
|
|
455
|
+
Text(errorText, style: TextStyle(color: _danger)),
|
|
456
|
+
],
|
|
457
|
+
],
|
|
458
|
+
),
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
actions: [
|
|
462
|
+
TextButton(
|
|
463
|
+
onPressed: saving
|
|
464
|
+
? null
|
|
465
|
+
: () => Navigator.of(dialogContext).pop(),
|
|
466
|
+
child: const Text('Cancel'),
|
|
467
|
+
),
|
|
468
|
+
FilledButton(
|
|
469
|
+
onPressed: saving
|
|
470
|
+
? null
|
|
471
|
+
: () async {
|
|
472
|
+
setState(() {
|
|
473
|
+
saving = true;
|
|
474
|
+
errorText = '';
|
|
475
|
+
});
|
|
476
|
+
try {
|
|
477
|
+
await controller.createCredentialBinding(
|
|
478
|
+
<String, dynamic>{
|
|
479
|
+
'connectionId': connectionId,
|
|
480
|
+
'alias': aliasController.text.trim(),
|
|
481
|
+
'usageType': usageType,
|
|
482
|
+
'itemId': itemId,
|
|
483
|
+
if (usageType == 'browser')
|
|
484
|
+
'origins': <String>[originController.text.trim()],
|
|
485
|
+
if (usageType == 'http') ...<String, dynamic>{
|
|
486
|
+
'origin': originController.text.trim(),
|
|
487
|
+
'pathPrefix': pathController.text.trim(),
|
|
488
|
+
'methods': const [
|
|
489
|
+
'GET',
|
|
490
|
+
'POST',
|
|
491
|
+
'PUT',
|
|
492
|
+
'PATCH',
|
|
493
|
+
'DELETE',
|
|
494
|
+
],
|
|
495
|
+
'authType': authType,
|
|
496
|
+
'secretField': secretField,
|
|
497
|
+
if (authType == 'basic')
|
|
498
|
+
'usernameField': 'login.username',
|
|
499
|
+
if (authType == 'header')
|
|
500
|
+
'headerName': headerController.text.trim(),
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
);
|
|
504
|
+
if (dialogContext.mounted) {
|
|
505
|
+
Navigator.of(dialogContext).pop();
|
|
506
|
+
}
|
|
507
|
+
} catch (_) {
|
|
508
|
+
setState(() {
|
|
509
|
+
errorText =
|
|
510
|
+
controller.errorMessage ??
|
|
511
|
+
'Could not create binding.';
|
|
512
|
+
saving = false;
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
child: Text(saving ? 'Adding...' : 'Add binding'),
|
|
517
|
+
),
|
|
518
|
+
],
|
|
519
|
+
);
|
|
520
|
+
},
|
|
521
|
+
),
|
|
522
|
+
);
|
|
523
|
+
aliasController.dispose();
|
|
524
|
+
originController.dispose();
|
|
525
|
+
pathController.dispose();
|
|
526
|
+
headerController.dispose();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
Future<void> _showBitwardenSetupDialog(
|
|
530
|
+
BuildContext context,
|
|
531
|
+
NeoAgentController controller,
|
|
532
|
+
) async {
|
|
533
|
+
Map<String, dynamic> config;
|
|
534
|
+
List<Map<String, dynamic>> bindings;
|
|
535
|
+
try {
|
|
536
|
+
config = await controller.getOfficialIntegrationConfig('bitwarden');
|
|
537
|
+
bindings = await controller.fetchCredentialBindings();
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (context.mounted) {
|
|
540
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
541
|
+
SnackBar(content: Text(controller.errorMessage ?? error.toString())),
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
var unlocked = config['unlocked'] == true;
|
|
547
|
+
var busy = false;
|
|
548
|
+
var errorText = '';
|
|
549
|
+
final serverController = TextEditingController(
|
|
550
|
+
text: config['serverUrl']?.toString() ?? 'https://vault.bitwarden.com',
|
|
551
|
+
);
|
|
552
|
+
final emailController = TextEditingController(
|
|
553
|
+
text: config['email']?.toString() ?? '',
|
|
554
|
+
);
|
|
555
|
+
final clientIdController = TextEditingController();
|
|
556
|
+
final clientSecretController = TextEditingController();
|
|
557
|
+
final masterPasswordController = TextEditingController();
|
|
558
|
+
final timeoutController = TextEditingController(
|
|
559
|
+
text: (config['idleTimeoutMinutes'] ?? 30).toString(),
|
|
560
|
+
);
|
|
561
|
+
if (!context.mounted) return;
|
|
562
|
+
await showDialog<void>(
|
|
563
|
+
context: context,
|
|
564
|
+
barrierDismissible: false,
|
|
565
|
+
builder: (dialogContext) => StatefulBuilder(
|
|
566
|
+
builder: (dialogContext, setState) => AlertDialog(
|
|
567
|
+
title: const Text('Bitwarden credential broker'),
|
|
568
|
+
content: SizedBox(
|
|
569
|
+
width: 600,
|
|
570
|
+
child: SingleChildScrollView(
|
|
571
|
+
child: Column(
|
|
572
|
+
mainAxisSize: MainAxisSize.min,
|
|
573
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
574
|
+
children: [
|
|
575
|
+
Text(
|
|
576
|
+
'NeoAgent receives only opaque binding IDs. Your master password, vault session, usernames, passwords, and API tokens stay outside the AI context.',
|
|
577
|
+
style: TextStyle(color: _textSecondary),
|
|
578
|
+
),
|
|
579
|
+
const SizedBox(height: 16),
|
|
580
|
+
TextField(
|
|
581
|
+
controller: serverController,
|
|
582
|
+
decoration: const InputDecoration(
|
|
583
|
+
labelText: 'Bitwarden server',
|
|
584
|
+
border: OutlineInputBorder(),
|
|
585
|
+
),
|
|
586
|
+
),
|
|
587
|
+
const SizedBox(height: 12),
|
|
588
|
+
TextField(
|
|
589
|
+
controller: emailController,
|
|
590
|
+
keyboardType: TextInputType.emailAddress,
|
|
591
|
+
decoration: const InputDecoration(
|
|
592
|
+
labelText: 'Account email',
|
|
593
|
+
border: OutlineInputBorder(),
|
|
594
|
+
),
|
|
595
|
+
),
|
|
596
|
+
const SizedBox(height: 12),
|
|
597
|
+
TextField(
|
|
598
|
+
controller: clientIdController,
|
|
599
|
+
decoration: InputDecoration(
|
|
600
|
+
labelText: config['hasClientId'] == true
|
|
601
|
+
? 'Replacement personal API client ID'
|
|
602
|
+
: 'Personal API client ID',
|
|
603
|
+
border: const OutlineInputBorder(),
|
|
604
|
+
),
|
|
605
|
+
),
|
|
606
|
+
const SizedBox(height: 12),
|
|
607
|
+
TextField(
|
|
608
|
+
controller: clientSecretController,
|
|
609
|
+
obscureText: true,
|
|
610
|
+
decoration: InputDecoration(
|
|
611
|
+
labelText: config['hasClientSecret'] == true
|
|
612
|
+
? 'Replacement personal API client secret'
|
|
613
|
+
: 'Personal API client secret',
|
|
614
|
+
border: const OutlineInputBorder(),
|
|
615
|
+
),
|
|
616
|
+
),
|
|
617
|
+
const SizedBox(height: 20),
|
|
618
|
+
Text(
|
|
619
|
+
unlocked ? 'Vault unlocked' : 'Vault locked',
|
|
620
|
+
style: TextStyle(color: unlocked ? _success : _textSecondary),
|
|
621
|
+
),
|
|
622
|
+
const SizedBox(height: 8),
|
|
623
|
+
Row(
|
|
624
|
+
children: [
|
|
625
|
+
Expanded(
|
|
626
|
+
child: TextField(
|
|
627
|
+
controller: masterPasswordController,
|
|
628
|
+
obscureText: true,
|
|
629
|
+
enabled: !busy && !unlocked,
|
|
630
|
+
decoration: const InputDecoration(
|
|
631
|
+
labelText: 'Master password',
|
|
632
|
+
border: OutlineInputBorder(),
|
|
633
|
+
),
|
|
634
|
+
),
|
|
635
|
+
),
|
|
636
|
+
const SizedBox(width: 8),
|
|
637
|
+
SizedBox(
|
|
638
|
+
width: 92,
|
|
639
|
+
child: TextField(
|
|
640
|
+
controller: timeoutController,
|
|
641
|
+
keyboardType: TextInputType.number,
|
|
642
|
+
enabled: !busy && !unlocked,
|
|
643
|
+
decoration: const InputDecoration(
|
|
644
|
+
labelText: 'Minutes',
|
|
645
|
+
border: OutlineInputBorder(),
|
|
646
|
+
),
|
|
647
|
+
),
|
|
648
|
+
),
|
|
649
|
+
const SizedBox(width: 8),
|
|
650
|
+
FilledButton(
|
|
651
|
+
onPressed:
|
|
652
|
+
busy || (!unlocked && config['configured'] != true)
|
|
653
|
+
? null
|
|
654
|
+
: () async {
|
|
655
|
+
setState(() => busy = true);
|
|
656
|
+
try {
|
|
657
|
+
if (unlocked) {
|
|
658
|
+
await controller.lockBitwarden();
|
|
659
|
+
unlocked = false;
|
|
660
|
+
} else {
|
|
661
|
+
await controller.unlockBitwarden(
|
|
662
|
+
masterPasswordController.text,
|
|
663
|
+
idleTimeoutMinutes:
|
|
664
|
+
int.tryParse(timeoutController.text) ??
|
|
665
|
+
30,
|
|
666
|
+
);
|
|
667
|
+
masterPasswordController.clear();
|
|
668
|
+
unlocked = true;
|
|
669
|
+
}
|
|
670
|
+
setState(() {});
|
|
671
|
+
} catch (_) {
|
|
672
|
+
setState(
|
|
673
|
+
() => errorText =
|
|
674
|
+
controller.errorMessage ??
|
|
675
|
+
'Vault operation failed.',
|
|
676
|
+
);
|
|
677
|
+
} finally {
|
|
678
|
+
setState(() => busy = false);
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
child: Text(unlocked ? 'Lock' : 'Unlock'),
|
|
682
|
+
),
|
|
683
|
+
],
|
|
684
|
+
),
|
|
685
|
+
const SizedBox(height: 20),
|
|
686
|
+
Row(
|
|
687
|
+
children: [
|
|
688
|
+
const Expanded(
|
|
689
|
+
child: Text(
|
|
690
|
+
'Credential bindings',
|
|
691
|
+
style: TextStyle(fontWeight: FontWeight.w600),
|
|
692
|
+
),
|
|
693
|
+
),
|
|
694
|
+
FilledButton.tonalIcon(
|
|
695
|
+
onPressed:
|
|
696
|
+
!unlocked || config['connectionId'] == null || busy
|
|
697
|
+
? null
|
|
698
|
+
: () async {
|
|
699
|
+
await _showBitwardenBindingDialog(
|
|
700
|
+
dialogContext,
|
|
701
|
+
controller,
|
|
702
|
+
connectionId: (config['connectionId'] as num)
|
|
703
|
+
.toInt(),
|
|
704
|
+
);
|
|
705
|
+
bindings = await controller
|
|
706
|
+
.fetchCredentialBindings();
|
|
707
|
+
setState(() {});
|
|
708
|
+
},
|
|
709
|
+
icon: const Icon(Icons.add_rounded),
|
|
710
|
+
label: const Text('Add'),
|
|
711
|
+
),
|
|
712
|
+
],
|
|
713
|
+
),
|
|
714
|
+
if (bindings.isEmpty)
|
|
715
|
+
Padding(
|
|
716
|
+
padding: const EdgeInsets.only(top: 8),
|
|
717
|
+
child: Text(
|
|
718
|
+
'No bindings yet.',
|
|
719
|
+
style: TextStyle(color: _textSecondary),
|
|
720
|
+
),
|
|
721
|
+
)
|
|
722
|
+
else
|
|
723
|
+
...bindings.map(
|
|
724
|
+
(binding) => ListTile(
|
|
725
|
+
contentPadding: EdgeInsets.zero,
|
|
726
|
+
title: Text(binding['alias']?.toString() ?? 'Credential'),
|
|
727
|
+
subtitle: Text(binding['usageType']?.toString() ?? ''),
|
|
728
|
+
trailing: IconButton(
|
|
729
|
+
tooltip: 'Delete binding',
|
|
730
|
+
icon: const Icon(Icons.delete_outline_rounded),
|
|
731
|
+
onPressed: busy
|
|
732
|
+
? null
|
|
733
|
+
: () async {
|
|
734
|
+
await controller.deleteCredentialBinding(
|
|
735
|
+
binding['id'].toString(),
|
|
736
|
+
);
|
|
737
|
+
bindings = await controller
|
|
738
|
+
.fetchCredentialBindings();
|
|
739
|
+
setState(() {});
|
|
740
|
+
},
|
|
741
|
+
),
|
|
742
|
+
),
|
|
743
|
+
),
|
|
744
|
+
if (errorText.isNotEmpty)
|
|
745
|
+
Padding(
|
|
746
|
+
padding: const EdgeInsets.only(top: 12),
|
|
747
|
+
child: Text(errorText, style: TextStyle(color: _danger)),
|
|
748
|
+
),
|
|
749
|
+
],
|
|
750
|
+
),
|
|
751
|
+
),
|
|
752
|
+
),
|
|
753
|
+
actions: [
|
|
754
|
+
if (config['configured'] == true)
|
|
755
|
+
TextButton(
|
|
756
|
+
onPressed: busy
|
|
757
|
+
? null
|
|
758
|
+
: () async {
|
|
759
|
+
setState(() => busy = true);
|
|
760
|
+
try {
|
|
761
|
+
await controller.clearOfficialIntegrationConfig(
|
|
762
|
+
'bitwarden',
|
|
763
|
+
);
|
|
764
|
+
if (dialogContext.mounted) {
|
|
765
|
+
Navigator.of(dialogContext).pop();
|
|
766
|
+
}
|
|
767
|
+
} catch (_) {
|
|
768
|
+
setState(() {
|
|
769
|
+
errorText =
|
|
770
|
+
controller.errorMessage ??
|
|
771
|
+
'Could not disconnect Bitwarden.';
|
|
772
|
+
busy = false;
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
child: const Text('Disconnect'),
|
|
777
|
+
),
|
|
778
|
+
TextButton(
|
|
779
|
+
onPressed: busy ? null : () => Navigator.of(dialogContext).pop(),
|
|
780
|
+
child: const Text('Close'),
|
|
781
|
+
),
|
|
782
|
+
FilledButton(
|
|
783
|
+
onPressed: busy
|
|
784
|
+
? null
|
|
785
|
+
: () async {
|
|
786
|
+
setState(() {
|
|
787
|
+
busy = true;
|
|
788
|
+
errorText = '';
|
|
789
|
+
});
|
|
790
|
+
try {
|
|
791
|
+
await controller.saveOfficialIntegrationConfig(
|
|
792
|
+
'bitwarden',
|
|
793
|
+
config: <String, dynamic>{
|
|
794
|
+
'serverUrl': serverController.text.trim(),
|
|
795
|
+
'email': emailController.text.trim(),
|
|
796
|
+
if (clientIdController.text.trim().isNotEmpty)
|
|
797
|
+
'clientId': clientIdController.text.trim(),
|
|
798
|
+
if (clientSecretController.text.isNotEmpty)
|
|
799
|
+
'clientSecret': clientSecretController.text,
|
|
800
|
+
'idleTimeoutMinutes':
|
|
801
|
+
int.tryParse(timeoutController.text) ?? 30,
|
|
802
|
+
},
|
|
803
|
+
);
|
|
804
|
+
config = await controller.getOfficialIntegrationConfig(
|
|
805
|
+
'bitwarden',
|
|
806
|
+
);
|
|
807
|
+
setState(() {});
|
|
808
|
+
} catch (_) {
|
|
809
|
+
setState(
|
|
810
|
+
() => errorText =
|
|
811
|
+
controller.errorMessage ??
|
|
812
|
+
'Could not save Bitwarden setup.',
|
|
813
|
+
);
|
|
814
|
+
} finally {
|
|
815
|
+
setState(() => busy = false);
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
child: Text(busy ? 'Saving...' : 'Save setup'),
|
|
819
|
+
),
|
|
820
|
+
],
|
|
821
|
+
),
|
|
822
|
+
),
|
|
823
|
+
);
|
|
824
|
+
serverController.dispose();
|
|
825
|
+
emailController.dispose();
|
|
826
|
+
clientIdController.dispose();
|
|
827
|
+
clientSecretController.dispose();
|
|
828
|
+
masterPasswordController.dispose();
|
|
829
|
+
timeoutController.dispose();
|
|
830
|
+
}
|
|
831
|
+
|
|
248
832
|
Future<void> _showNeoRecallSetupDialog(
|
|
249
833
|
BuildContext context,
|
|
250
834
|
NeoAgentController controller,
|
|
@@ -305,7 +889,8 @@ Future<void> _showNeoRecallSetupDialog(
|
|
|
305
889
|
if (dialogContext.mounted) Navigator.of(dialogContext).pop();
|
|
306
890
|
} catch (_) {
|
|
307
891
|
setState(() {
|
|
308
|
-
errorText =
|
|
892
|
+
errorText =
|
|
893
|
+
controller.errorMessage ?? 'Could not save NeoRecall setup.';
|
|
309
894
|
busy = false;
|
|
310
895
|
});
|
|
311
896
|
}
|
|
@@ -380,11 +965,13 @@ Future<void> _showNeoRecallSetupDialog(
|
|
|
380
965
|
),
|
|
381
966
|
actions: <Widget>[
|
|
382
967
|
TextButton(
|
|
383
|
-
onPressed: () =>
|
|
968
|
+
onPressed: () =>
|
|
969
|
+
Navigator.of(context).pop(false),
|
|
384
970
|
child: const Text('Cancel'),
|
|
385
971
|
),
|
|
386
972
|
FilledButton(
|
|
387
|
-
onPressed: () =>
|
|
973
|
+
onPressed: () =>
|
|
974
|
+
Navigator.of(context).pop(true),
|
|
388
975
|
child: const Text('Disconnect'),
|
|
389
976
|
),
|
|
390
977
|
],
|
|
@@ -397,11 +984,17 @@ Future<void> _showNeoRecallSetupDialog(
|
|
|
397
984
|
errorText = '';
|
|
398
985
|
});
|
|
399
986
|
try {
|
|
400
|
-
await controller.clearOfficialIntegrationConfig(
|
|
401
|
-
|
|
987
|
+
await controller.clearOfficialIntegrationConfig(
|
|
988
|
+
'neorecall',
|
|
989
|
+
);
|
|
990
|
+
if (dialogContext.mounted) {
|
|
991
|
+
Navigator.of(dialogContext).pop();
|
|
992
|
+
}
|
|
402
993
|
} catch (_) {
|
|
403
994
|
setState(() {
|
|
404
|
-
errorText =
|
|
995
|
+
errorText =
|
|
996
|
+
controller.errorMessage ??
|
|
997
|
+
'Could not disconnect NeoRecall.';
|
|
405
998
|
busy = false;
|
|
406
999
|
});
|
|
407
1000
|
}
|
|
@@ -413,11 +1006,15 @@ Future<void> _showNeoRecallSetupDialog(
|
|
|
413
1006
|
child: const Text('Close'),
|
|
414
1007
|
),
|
|
415
1008
|
TextButton(
|
|
416
|
-
onPressed: busy
|
|
1009
|
+
onPressed: busy
|
|
1010
|
+
? null
|
|
1011
|
+
: () => save(setState, dialogContext, connect: false),
|
|
417
1012
|
child: const Text('Save Only'),
|
|
418
1013
|
),
|
|
419
1014
|
FilledButton(
|
|
420
|
-
onPressed: busy
|
|
1015
|
+
onPressed: busy
|
|
1016
|
+
? null
|
|
1017
|
+
: () => save(setState, dialogContext, connect: true),
|
|
421
1018
|
child: Text(
|
|
422
1019
|
busy
|
|
423
1020
|
? 'Working...'
|
|
@@ -1544,6 +2141,7 @@ class _OfficialIntegrationIcon extends StatelessWidget {
|
|
|
1544
2141
|
'neorecall' => const Color(0xFFD98AA6),
|
|
1545
2142
|
'google' => const Color(0xFF4285F4),
|
|
1546
2143
|
'home_assistant' => const Color(0xFF41BDF5),
|
|
2144
|
+
'password' => const Color(0xFF175DDC),
|
|
1547
2145
|
'trello' => const Color(0xFF0C66E4),
|
|
1548
2146
|
_ => _accent,
|
|
1549
2147
|
};
|
|
@@ -1552,6 +2150,7 @@ class _OfficialIntegrationIcon extends StatelessWidget {
|
|
|
1552
2150
|
'neorecall' => 'R',
|
|
1553
2151
|
'google' => 'G',
|
|
1554
2152
|
'home_assistant' => 'H',
|
|
2153
|
+
'password' => 'B',
|
|
1555
2154
|
'trello' => 'T',
|
|
1556
2155
|
_ => item.label.isNotEmpty ? item.label[0] : '?',
|
|
1557
2156
|
};
|