omniwire 3.1.5 → 3.2.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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "lastCheck": 1774756238937,
2
+ "lastCheck": 1774767791264,
3
3
  "lastVersion": "3.0.1",
4
4
  "autoUpdateEnabled": true,
5
5
  "source": "auto",
package/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  **The infrastructure layer for AI agent swarms.**
21
21
 
22
- 81 MCP tools · A2A protocol · OmniMesh VPN · nftables firewall · CDP browser · cookie sync · CyberBase persistence
22
+ 82 MCP tools · A2A protocol · OmniMesh VPN · nftables firewall · CDP browser · cookie sync · 2FA TOTP · CyberBase persistence
23
23
 
24
24
  </div>
25
25
 
@@ -687,6 +687,7 @@ omniwire/
687
687
 
688
688
  | Version | Date | Changes |
689
689
  |---------|------|---------|
690
+ | **v3.2.0** | 2026-03-29 | New: `omniwire_2fa` TOTP manager — add/generate/verify/import/export 2FA codes, CyberBase + 1Password persistence, otpauth:// URI import, bulk code generation |
690
691
  | **v3.1.5** | 2026-03-29 | Fix: skip auto-audit batch entries from Obsidian vault + Canvas sync to prevent junk files |
691
692
  | **v3.1.4** | 2026-03-29 | Auto-sync CyberBase writes to Obsidian vault + Canvas mindmap, collision-avoidance grid placement, `sync-obsidian` / `sync-canvas` actions in knowledge tool |
692
693
  | **v3.1.3** | 2026-03-29 | OmniMesh WireGuard mesh manager, event bus (Webhook/WS/SSE), knowledge tool (12 actions), auto-update system, CDP rewrite (persistent Docker container, 18 actions), mesh expose/gateway, CyberBase circuit breaker + SQL hardening |
@@ -3660,7 +3660,237 @@ echo "port-knock configured: ${ports.join(' -> ')} -> port ${target}"`;
3660
3660
  }
3661
3661
  return fail('invalid action');
3662
3662
  });
3663
- // --- Tool 53: omniwire_knowledge ---
3663
+ // --- Tool 53: omniwire_2fa ---
3664
+ // TOTP 2FA manager — stores secrets in CyberBase + 1Password, generates codes on demand
3665
+ const twoFaStore = new Map();
3666
+ function base32Decode(encoded) {
3667
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
3668
+ const stripped = encoded.replace(/[\s=-]/g, '').toUpperCase();
3669
+ let bits = '';
3670
+ for (const c of stripped) {
3671
+ const idx = alphabet.indexOf(c);
3672
+ if (idx === -1)
3673
+ continue;
3674
+ bits += idx.toString(2).padStart(5, '0');
3675
+ }
3676
+ const bytes = [];
3677
+ for (let i = 0; i + 8 <= bits.length; i += 8)
3678
+ bytes.push(parseInt(bits.slice(i, i + 8), 2));
3679
+ return Buffer.from(bytes);
3680
+ }
3681
+ function generateTOTP(secret, options) {
3682
+ const period = options?.period ?? 30;
3683
+ const digits = options?.digits ?? 6;
3684
+ const algo = options?.algorithm ?? 'sha1';
3685
+ const now = options?.time ?? Math.floor(Date.now() / 1000);
3686
+ const counter = Math.floor(now / period);
3687
+ const counterBuf = Buffer.alloc(8);
3688
+ counterBuf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);
3689
+ counterBuf.writeUInt32BE(counter & 0xFFFFFFFF, 4);
3690
+ const keyBuf = base32Decode(secret);
3691
+ const hmac = require('crypto').createHmac(algo, keyBuf).update(counterBuf).digest();
3692
+ const offset = hmac[hmac.length - 1] & 0x0f;
3693
+ const code = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % (10 ** digits);
3694
+ return code.toString().padStart(digits, '0');
3695
+ }
3696
+ server.tool('omniwire_2fa', 'TOTP 2FA manager — add/generate/list/delete/verify/export 2FA secrets. Stores encrypted in CyberBase + 1Password. Generate codes for any stored service instantly.', {
3697
+ action: z.enum(['add', 'generate', 'list', 'delete', 'verify', 'export', 'import', 'bulk-generate']).describe('Action: add=store secret, generate=get current code, list=show all, delete=remove, verify=check code, export=dump all, import=parse otpauth URI, bulk-generate=codes for all'),
3698
+ name: z.string().optional().describe('Service name (e.g., "github", "discord", "aws")'),
3699
+ secret: z.string().optional().describe('Base32-encoded TOTP secret (for add) or otpauth:// URI (for import)'),
3700
+ issuer: z.string().optional().describe('Issuer name (e.g., "GitHub", "Discord")'),
3701
+ code: z.string().optional().describe('6-digit code to verify (for verify action)'),
3702
+ algorithm: z.enum(['sha1', 'sha256', 'sha512']).optional().describe('HMAC algorithm (default: sha1)'),
3703
+ digits: z.number().optional().describe('Code length (default: 6)'),
3704
+ period: z.number().optional().describe('Time step in seconds (default: 30)'),
3705
+ format: z.enum(['json', 'uri', 'table']).optional().describe('Export format (default: table)'),
3706
+ node: z.string().optional().describe('Node for 1Password sync'),
3707
+ background: z.boolean().optional().describe('Run in background'),
3708
+ }, async (args) => {
3709
+ const { action, name, secret, issuer, code: verifyCode, algorithm, digits, period, format, node: targetNode } = args;
3710
+ const nodeId = targetNode ?? 'contabo';
3711
+ // Load from CyberBase on first access if memory is empty
3712
+ if (twoFaStore.size === 0 && cbManager) {
3713
+ try {
3714
+ const r = await cbManager.exec('contabo', pgExec(`SELECT key, value FROM knowledge WHERE source_tool = 'omniwire' AND key LIKE '2fa:%' LIMIT 100`));
3715
+ if (r.code === 0 && r.stdout) {
3716
+ for (const line of r.stdout.split('\n')) {
3717
+ const match = line.match(/^\s*2fa:(\S+)\s*\|\s*(.+)/);
3718
+ if (match) {
3719
+ try {
3720
+ const parsed = JSON.parse(match[2].trim());
3721
+ const data = parsed.data ? JSON.parse(parsed.data) : parsed;
3722
+ twoFaStore.set(match[1], data);
3723
+ }
3724
+ catch { /* skip malformed */ }
3725
+ }
3726
+ }
3727
+ }
3728
+ }
3729
+ catch { /* CyberBase offline, continue with memory */ }
3730
+ }
3731
+ switch (action) {
3732
+ case 'add': {
3733
+ if (!name)
3734
+ return fail('name required');
3735
+ if (!secret)
3736
+ return fail('secret required');
3737
+ const entry = {
3738
+ secret: secret.replace(/\s/g, '').toUpperCase(),
3739
+ issuer: issuer ?? name,
3740
+ algorithm: algorithm ?? 'sha1',
3741
+ digits: digits ?? 6,
3742
+ period: period ?? 30,
3743
+ addedAt: new Date().toISOString(),
3744
+ };
3745
+ twoFaStore.set(name, entry);
3746
+ // Persist to CyberBase (secret stored as JSON, no raw secret in key)
3747
+ cb('2fa', name, JSON.stringify(entry));
3748
+ // Sync to 1Password
3749
+ if (cbManager) {
3750
+ const opCmd = `op item get "OmniWire 2FA - ${name}" --vault "CyberBase" >/dev/null 2>&1 && op item edit "OmniWire 2FA - ${name}" --vault "CyberBase" "notesPlain=${entry.secret}" 2>/dev/null || op item create --category=SecureNote --vault="CyberBase" --title="OmniWire 2FA - ${name}" "notesPlain=${entry.secret}" 2>/dev/null`;
3751
+ cbManager.exec(nodeId, opCmd).catch(() => { });
3752
+ }
3753
+ const currentCode = generateTOTP(entry.secret, { period: entry.period, digits: entry.digits, algorithm: entry.algorithm });
3754
+ return okBrief(`2FA added: ${name} (${entry.issuer})\nCurrent code: ${currentCode}\nStored in: memory + CyberBase + 1Password`);
3755
+ }
3756
+ case 'generate': {
3757
+ if (!name) {
3758
+ // Generate for all
3759
+ if (twoFaStore.size === 0)
3760
+ return fail('no 2FA secrets stored — use add first');
3761
+ const lines = ['Service | Code | Expires in'];
3762
+ const now = Math.floor(Date.now() / 1000);
3763
+ for (const [n, e] of twoFaStore) {
3764
+ const c = generateTOTP(e.secret, { period: e.period, digits: e.digits, algorithm: e.algorithm });
3765
+ const remaining = e.period - (now % e.period);
3766
+ lines.push(`${n} | ${c} | ${remaining}s`);
3767
+ }
3768
+ return okBrief(lines.join('\n'));
3769
+ }
3770
+ const entry = twoFaStore.get(name);
3771
+ if (!entry)
3772
+ return fail(`${name} not found — use list to see available`);
3773
+ const now = Math.floor(Date.now() / 1000);
3774
+ const currentCode = generateTOTP(entry.secret, { period: entry.period, digits: entry.digits, algorithm: entry.algorithm });
3775
+ const remaining = entry.period - (now % entry.period);
3776
+ return okBrief(`${name}: ${currentCode} (expires in ${remaining}s)`);
3777
+ }
3778
+ case 'bulk-generate': {
3779
+ if (twoFaStore.size === 0)
3780
+ return fail('no 2FA secrets stored');
3781
+ const lines = ['Service | Code | Expires'];
3782
+ const now = Math.floor(Date.now() / 1000);
3783
+ for (const [n, e] of twoFaStore) {
3784
+ const c = generateTOTP(e.secret, { period: e.period, digits: e.digits, algorithm: e.algorithm });
3785
+ const remaining = e.period - (now % e.period);
3786
+ lines.push(`${n} | ${c} | ${remaining}s`);
3787
+ }
3788
+ return okBrief(lines.join('\n'));
3789
+ }
3790
+ case 'list': {
3791
+ if (twoFaStore.size === 0)
3792
+ return okBrief('No 2FA secrets stored');
3793
+ const lines = ['Service | Issuer | Algorithm | Digits | Period | Added'];
3794
+ for (const [n, e] of twoFaStore) {
3795
+ lines.push(`${n} | ${e.issuer} | ${e.algorithm} | ${e.digits} | ${e.period}s | ${e.addedAt.split('T')[0]}`);
3796
+ }
3797
+ return okBrief(lines.join('\n'));
3798
+ }
3799
+ case 'delete': {
3800
+ if (!name)
3801
+ return fail('name required');
3802
+ if (!twoFaStore.has(name))
3803
+ return fail(`${name} not found`);
3804
+ twoFaStore.delete(name);
3805
+ // Remove from CyberBase
3806
+ if (cbManager) {
3807
+ const sql = `DELETE FROM knowledge WHERE source_tool = 'omniwire' AND key = '2fa:${sqlEscape(name)}'`;
3808
+ CB_QUEUE.push(sql);
3809
+ if (!cbDraining)
3810
+ drainCb();
3811
+ // Remove from 1Password
3812
+ cbManager.exec(nodeId, `op item delete "OmniWire 2FA - ${name}" --vault "CyberBase" 2>/dev/null`).catch(() => { });
3813
+ }
3814
+ return okBrief(`2FA deleted: ${name}`);
3815
+ }
3816
+ case 'verify': {
3817
+ if (!name)
3818
+ return fail('name required');
3819
+ if (!verifyCode)
3820
+ return fail('code required');
3821
+ const entry = twoFaStore.get(name);
3822
+ if (!entry)
3823
+ return fail(`${name} not found`);
3824
+ const now = Math.floor(Date.now() / 1000);
3825
+ // Check current window ± 1 step
3826
+ for (const offset of [0, -1, 1]) {
3827
+ const expected = generateTOTP(entry.secret, {
3828
+ period: entry.period, digits: entry.digits, algorithm: entry.algorithm,
3829
+ time: now + (offset * entry.period)
3830
+ });
3831
+ if (verifyCode === expected)
3832
+ return okBrief(`VALID — ${name} code matches (window: ${offset === 0 ? 'current' : offset < 0 ? 'previous' : 'next'})`);
3833
+ }
3834
+ return fail(`INVALID — ${name} code does not match`);
3835
+ }
3836
+ case 'import': {
3837
+ if (!secret)
3838
+ return fail('otpauth:// URI required');
3839
+ // Parse otpauth://totp/Issuer:Account?secret=XXX&issuer=YYY&algorithm=SHA1&digits=6&period=30
3840
+ const url = new URL(secret);
3841
+ if (url.protocol !== 'otpauth:')
3842
+ return fail('invalid URI — must start with otpauth://');
3843
+ const label = decodeURIComponent(url.pathname.replace(/^\/\/totp\//, ''));
3844
+ const parsedSecret = url.searchParams.get('secret');
3845
+ if (!parsedSecret)
3846
+ return fail('no secret in URI');
3847
+ const parsedIssuer = url.searchParams.get('issuer') ?? label.split(':')[0] ?? 'Unknown';
3848
+ const parsedAlgo = (url.searchParams.get('algorithm') ?? 'SHA1').toLowerCase();
3849
+ const parsedDigits = parseInt(url.searchParams.get('digits') ?? '6');
3850
+ const parsedPeriod = parseInt(url.searchParams.get('period') ?? '30');
3851
+ const entryName = name ?? label.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
3852
+ const entry = {
3853
+ secret: parsedSecret.toUpperCase(),
3854
+ issuer: parsedIssuer,
3855
+ algorithm: parsedAlgo,
3856
+ digits: parsedDigits,
3857
+ period: parsedPeriod,
3858
+ addedAt: new Date().toISOString(),
3859
+ };
3860
+ twoFaStore.set(entryName, entry);
3861
+ cb('2fa', entryName, JSON.stringify(entry));
3862
+ const currentCode = generateTOTP(entry.secret, { period: entry.period, digits: entry.digits, algorithm: entry.algorithm });
3863
+ return okBrief(`Imported: ${entryName} (${parsedIssuer})\nCurrent code: ${currentCode}`);
3864
+ }
3865
+ case 'export': {
3866
+ if (twoFaStore.size === 0)
3867
+ return fail('no 2FA secrets stored');
3868
+ const fmt = format ?? 'table';
3869
+ if (fmt === 'json') {
3870
+ const data = {};
3871
+ for (const [n, e] of twoFaStore)
3872
+ data[n] = { ...e };
3873
+ return okBrief(JSON.stringify(data, null, 2));
3874
+ }
3875
+ if (fmt === 'uri') {
3876
+ const lines = [];
3877
+ for (const [n, e] of twoFaStore) {
3878
+ lines.push(`otpauth://totp/${encodeURIComponent(e.issuer)}:${encodeURIComponent(n)}?secret=${e.secret}&issuer=${encodeURIComponent(e.issuer)}&algorithm=${e.algorithm.toUpperCase()}&digits=${e.digits}&period=${e.period}`);
3879
+ }
3880
+ return okBrief(lines.join('\n'));
3881
+ }
3882
+ // table
3883
+ const lines = ['Service | Issuer | Secret (last 4) | Algorithm'];
3884
+ for (const [n, e] of twoFaStore) {
3885
+ lines.push(`${n} | ${e.issuer} | ...${e.secret.slice(-4)} | ${e.algorithm}`);
3886
+ }
3887
+ return okBrief(lines.join('\n'));
3888
+ }
3889
+ default:
3890
+ return fail('invalid action');
3891
+ }
3892
+ });
3893
+ // --- Tool 54: omniwire_knowledge ---
3664
3894
  server.tool('omniwire_knowledge', 'CyberBase knowledge base — CRUD, search, and health management for the unified PostgreSQL knowledge store. Auto-syncs all writes to Obsidian vault + Canvas mindmap. Supports text search, semantic/vector search, categories, bulk operations, and explicit sync-obsidian/sync-canvas actions.', {
3665
3895
  action: z.enum(['get', 'set', 'delete', 'search', 'semantic-search', 'list', 'stats', 'health', 'categories', 'bulk-set', 'export', 'vacuum', 'sync-obsidian', 'sync-canvas']).describe('Action'),
3666
3896
  category: z.string().optional().describe('Knowledge category (e.g., tools, vulns, infra, notes)'),