aimeat 1.4.7 → 1.5.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.
Files changed (135) hide show
  1. package/dist/locales/en.json +62 -4
  2. package/dist/locales/fi.json +62 -4
  3. package/dist/public/css/views/profile.css +37 -0
  4. package/dist/public/js/services/admin.js +6 -0
  5. package/dist/public/js/services/boards.js +6 -3
  6. package/dist/public/js/services/memory.js +10 -0
  7. package/dist/public/views/admin/federation-tab.js +93 -7
  8. package/dist/public/views/admin/ghii-tab.js +2 -0
  9. package/dist/public/views/admin.js +5 -9
  10. package/dist/public/views/profile/access-tab.js +160 -58
  11. package/dist/public/views/profile/agents-tab.js +13 -1
  12. package/dist/public/views/profile/boards-tab.js +12 -0
  13. package/dist/public/views/profile/data-wallet-tab.js +29 -2
  14. package/dist/public/views/profile/knowledge-tab.js +64 -2
  15. package/dist/public/views/profile/landing-page.js +9 -6
  16. package/dist/public/views/profile/memory-tab.js +92 -4
  17. package/dist/src/auth/jwt.d.ts +6 -0
  18. package/dist/src/auth/jwt.d.ts.map +1 -1
  19. package/dist/src/auth/jwt.js +6 -0
  20. package/dist/src/auth/jwt.js.map +1 -1
  21. package/dist/src/auth/middleware.d.ts +7 -0
  22. package/dist/src/auth/middleware.d.ts.map +1 -1
  23. package/dist/src/auth/middleware.js +20 -0
  24. package/dist/src/auth/middleware.js.map +1 -1
  25. package/dist/src/middleware/federation-proxy.d.ts +35 -0
  26. package/dist/src/middleware/federation-proxy.d.ts.map +1 -0
  27. package/dist/src/middleware/federation-proxy.js +92 -0
  28. package/dist/src/middleware/federation-proxy.js.map +1 -0
  29. package/dist/src/routes/actions.d.ts.map +1 -1
  30. package/dist/src/routes/actions.js +7 -2
  31. package/dist/src/routes/actions.js.map +1 -1
  32. package/dist/src/routes/admin-agents.d.ts.map +1 -1
  33. package/dist/src/routes/admin-agents.js +1 -0
  34. package/dist/src/routes/admin-agents.js.map +1 -1
  35. package/dist/src/routes/admin-features.d.ts.map +1 -1
  36. package/dist/src/routes/admin-features.js +2 -0
  37. package/dist/src/routes/admin-features.js.map +1 -1
  38. package/dist/src/routes/admin-monitoring.d.ts.map +1 -1
  39. package/dist/src/routes/admin-monitoring.js +12 -1
  40. package/dist/src/routes/admin-monitoring.js.map +1 -1
  41. package/dist/src/routes/admin.d.ts.map +1 -1
  42. package/dist/src/routes/admin.js +16 -0
  43. package/dist/src/routes/admin.js.map +1 -1
  44. package/dist/src/routes/agents.d.ts.map +1 -1
  45. package/dist/src/routes/agents.js +36 -3
  46. package/dist/src/routes/agents.js.map +1 -1
  47. package/dist/src/routes/boards.d.ts.map +1 -1
  48. package/dist/src/routes/boards.js +12 -5
  49. package/dist/src/routes/boards.js.map +1 -1
  50. package/dist/src/routes/federation-auth.d.ts +15 -0
  51. package/dist/src/routes/federation-auth.d.ts.map +1 -0
  52. package/dist/src/routes/federation-auth.js +182 -0
  53. package/dist/src/routes/federation-auth.js.map +1 -0
  54. package/dist/src/routes/federation-genesis.d.ts +2 -1
  55. package/dist/src/routes/federation-genesis.d.ts.map +1 -1
  56. package/dist/src/routes/federation-genesis.js +58 -2
  57. package/dist/src/routes/federation-genesis.js.map +1 -1
  58. package/dist/src/routes/federation-peer.d.ts.map +1 -1
  59. package/dist/src/routes/federation-peer.js +124 -15
  60. package/dist/src/routes/federation-peer.js.map +1 -1
  61. package/dist/src/routes/federation-sync.d.ts.map +1 -1
  62. package/dist/src/routes/federation-sync.js +14 -1
  63. package/dist/src/routes/federation-sync.js.map +1 -1
  64. package/dist/src/routes/federation.d.ts +2 -1
  65. package/dist/src/routes/federation.d.ts.map +1 -1
  66. package/dist/src/routes/federation.js +4 -2
  67. package/dist/src/routes/federation.js.map +1 -1
  68. package/dist/src/routes/ghii.d.ts +2 -1
  69. package/dist/src/routes/ghii.d.ts.map +1 -1
  70. package/dist/src/routes/ghii.js +88 -4
  71. package/dist/src/routes/ghii.js.map +1 -1
  72. package/dist/src/routes/libs.js +49 -7
  73. package/dist/src/routes/libs.js.map +1 -1
  74. package/dist/src/routes/memory.d.ts.map +1 -1
  75. package/dist/src/routes/memory.js +162 -0
  76. package/dist/src/routes/memory.js.map +1 -1
  77. package/dist/src/routes/owners.d.ts.map +1 -1
  78. package/dist/src/routes/owners.js +1 -0
  79. package/dist/src/routes/owners.js.map +1 -1
  80. package/dist/src/server-bootstrap/routes-loader.d.ts +2 -0
  81. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  82. package/dist/src/server-bootstrap/routes-loader.js +7 -4
  83. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  84. package/dist/src/server-bootstrap/service-init.d.ts +2 -0
  85. package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
  86. package/dist/src/server-bootstrap/service-init.js +28 -1
  87. package/dist/src/server-bootstrap/service-init.js.map +1 -1
  88. package/dist/src/server.d.ts.map +1 -1
  89. package/dist/src/server.js +1 -0
  90. package/dist/src/server.js.map +1 -1
  91. package/dist/src/services/federation-helpers.d.ts.map +1 -1
  92. package/dist/src/services/federation-helpers.js +1 -0
  93. package/dist/src/services/federation-helpers.js.map +1 -1
  94. package/dist/src/services/federation.d.ts +6 -6
  95. package/dist/src/services/federation.d.ts.map +1 -1
  96. package/dist/src/services/federation.js +57 -1
  97. package/dist/src/services/federation.js.map +1 -1
  98. package/dist/src/services/memory-replication.d.ts.map +1 -1
  99. package/dist/src/services/memory-replication.js +3 -0
  100. package/dist/src/services/memory-replication.js.map +1 -1
  101. package/dist/src/storage/interface.d.ts +17 -1
  102. package/dist/src/storage/interface.d.ts.map +1 -1
  103. package/dist/src/storage/providers/mongodb/index.d.ts +6 -3
  104. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  105. package/dist/src/storage/providers/mongodb/index.js +109 -48
  106. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  107. package/dist/src/storage/providers/sqlite/index.d.ts +6 -2
  108. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  109. package/dist/src/storage/providers/sqlite/index.js +58 -16
  110. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  111. package/dist/src/storage/providers/sqlite/repos/action.d.ts.map +1 -1
  112. package/dist/src/storage/providers/sqlite/repos/action.js +5 -4
  113. package/dist/src/storage/providers/sqlite/repos/action.js.map +1 -1
  114. package/dist/src/storage/providers/sqlite/repos/agent.d.ts.map +1 -1
  115. package/dist/src/storage/providers/sqlite/repos/agent.js +5 -4
  116. package/dist/src/storage/providers/sqlite/repos/agent.js.map +1 -1
  117. package/dist/src/storage/providers/sqlite/repos/board.d.ts.map +1 -1
  118. package/dist/src/storage/providers/sqlite/repos/board.js +3 -2
  119. package/dist/src/storage/providers/sqlite/repos/board.js.map +1 -1
  120. package/dist/src/storage/providers/sqlite/repos/storage-file.d.ts.map +1 -1
  121. package/dist/src/storage/providers/sqlite/repos/storage-file.js +4 -2
  122. package/dist/src/storage/providers/sqlite/repos/storage-file.js.map +1 -1
  123. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  124. package/dist/src/storage/providers/sqlite/schema.js +20 -0
  125. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  126. package/dist/src/storage/repositories/board.repository.d.ts +1 -1
  127. package/dist/src/storage/repositories/board.repository.d.ts.map +1 -1
  128. package/dist/src/storage/repositories/federation.repository.d.ts +5 -1
  129. package/dist/src/storage/repositories/federation.repository.d.ts.map +1 -1
  130. package/dist/src/utils/service-summary.d.ts +65 -0
  131. package/dist/src/utils/service-summary.d.ts.map +1 -0
  132. package/dist/src/utils/service-summary.js +100 -0
  133. package/dist/src/utils/service-summary.js.map +1 -0
  134. package/package.json +3 -1
  135. package/prisma/schema.prisma +33 -0
@@ -1,58 +1,160 @@
1
- /**
2
- * @file access-tab.js
3
- * @description Profile tab displaying session info, public key, owner key,
4
- * and MCP endpoint details for the current user.
5
- * @version-history
6
- * v1.0.0 — 2026-03-16 — Initial access tab
7
- * v1.1.0 — 2026-03-17 — Replace inline styles with CSS classes
8
- */
9
- import { h } from 'preact';
10
- import { useState } from 'preact/hooks';
11
- import htm from 'htm';
12
- const html = htm.bind(h);
13
- import { t } from '/js/i18n.js';
14
- import { escHtml, copyToClipboard } from '/js/utils.js';
15
- import { getNodeUrl } from '/js/services/auth.js';
16
-
17
- export default function AccessTab({ session, showToast }) {
18
- const NODE_URL = getNodeUrl();
19
- const ownerKey = typeof localStorage !== 'undefined' ? localStorage.getItem('aimeat_owner_key') : null;
20
- const [keyBlurred, setKeyBlurred] = useState(true);
21
-
22
- return html`
23
- <div class="section-title">${t('profile.access.title')}</div>
24
- <div class="section-desc">${t('profile.access.desc')}</div>
25
-
26
- <h3 class="card-h3">\u{1F4BB} ${t('profile.access.session')}</h3>
27
- <div class="card">
28
- <div class="mem-item"><span class="mem-key">${t('profile.access.owner')}</span><span>${escHtml(session.owner || '-')}</span></div>
29
- <div class="mem-item"><span class="mem-key">${t('profile.access.ghii')}</span><span>${escHtml(session.ghii || '-')}</span></div>
30
- <div class="mem-item"><span class="mem-key">${t('profile.access.agentGaii')}</span><span>${escHtml(session.gaii || '-')}</span></div>
31
- <div class="mem-item"><span class="mem-key">${t('profile.access.node')}</span><span>${escHtml(NODE_URL)}</span></div>
32
- <div class="mem-item"><span class="mem-key">${t('profile.access.jwtValid')}</span><span>${session.valid ? html`<span class="badge badge-success">${t('profile.access.yes')}</span>` : html`<span class="badge badge-danger">${t('profile.access.expired')}</span>`}</span></div>
33
- </div>
34
-
35
- <h3 class="card-h3 mt-section">\u{1F510} ${t('profile.access.publicKey')}</h3>
36
- <div class="card"><div class="access-mono">${escHtml(session.publicKey || 'N/A')}</div></div>
37
-
38
- ${ownerKey && html`
39
- <h3 class="card-h3 mt-section">\u{1F5DD}\uFE0F ${t('profile.access.ownerKey')}</h3>
40
- <div class="card access-card-warn" onClick=${() => copyToClipboard(ownerKey).then(() => showToast(t('profile.access.keyCopied')))}>
41
- <div class="flex-between">
42
- <div class="access-mono ${keyBlurred ? 'access-key-blur' : 'access-key-blur revealed'}"
43
- onMouseEnter=${() => setKeyBlurred(false)} onMouseLeave=${() => setKeyBlurred(true)}>
44
- ${escHtml(ownerKey)}
45
- </div>
46
- <span class="badge badge-warn">${t('profile.access.hoverReveal')}</span>
47
- </div>
48
- <div class="access-warn-text">\u26A0 ${t('profile.access.keepSafe')}</div>
49
- </div>
50
- `}
51
-
52
- <h3 class="card-h3 mt-section">\u{1F517} ${t('profile.access.mcpEndpoint')}</h3>
53
- <div class="card">
54
- <div class="access-mcp-url">${escHtml(NODE_URL + '/v1/mcp')}</div>
55
- <div class="access-mcp-desc">${t('profile.access.mcpDesc')}</div>
56
- </div>
57
- `;
58
- }
1
+ /**
2
+ * @file access-tab.js
3
+ * @description Profile tab displaying session info, public key, owner key,
4
+ * MCP endpoint details, and federation access management for the current user.
5
+ * @version-history
6
+ * v1.0.0 — 2026-03-16 — Initial access tab
7
+ * v1.1.0 — 2026-03-17 — Replace inline styles with CSS classes
8
+ * v1.2.0 — 2026-05-21 — Add Federation Access section for auth consent management
9
+ */
10
+ import { h } from 'preact';
11
+ import { useState, useEffect } from 'preact/hooks';
12
+ import htm from 'htm';
13
+ const html = htm.bind(h);
14
+ import { t } from '/js/i18n.js';
15
+ import { escHtml, copyToClipboard } from '/js/utils.js';
16
+ import { getNodeUrl } from '/js/services/auth.js';
17
+ import { apiGet, apiPost, apiDelete } from '/js/api.js';
18
+
19
+ export default function AccessTab({ session, showToast }) {
20
+ const NODE_URL = getNodeUrl();
21
+ const ownerKey = typeof localStorage !== 'undefined' ? localStorage.getItem('aimeat_owner_key') : null;
22
+ const [keyBlurred, setKeyBlurred] = useState(true);
23
+ const [authConsents, setAuthConsents] = useState([]);
24
+ const [newNodeId, setNewNodeId] = useState('');
25
+ const [fedLoading, setFedLoading] = useState(false);
26
+
27
+ // Load auth consents on mount
28
+ useEffect(() => {
29
+ apiGet('/v1/consent').then(data => {
30
+ const all = data.data?.consents || [];
31
+ setAuthConsents(all.filter(c => c.scope === 'auth' && c.status === 'active'));
32
+ }).catch(() => {});
33
+ }, []);
34
+
35
+ async function addAuthNode() {
36
+ if (!newNodeId.trim()) return;
37
+ setFedLoading(true);
38
+ try {
39
+ await apiPost('/v1/consent', {
40
+ data_pattern: '_identity',
41
+ recipient: 'node:' + newNodeId.trim(),
42
+ scope: 'auth',
43
+ purpose: 'federation_login',
44
+ });
45
+ setNewNodeId('');
46
+ const data = await apiGet('/v1/consent');
47
+ setAuthConsents((data.data?.consents || []).filter(c => c.scope === 'auth' && c.status === 'active'));
48
+ showToast(t('profile.access.fedGranted'));
49
+ } catch (e) {
50
+ showToast(e.message);
51
+ } finally {
52
+ setFedLoading(false);
53
+ }
54
+ }
55
+
56
+ async function removeAuthConsent(id) {
57
+ try {
58
+ await apiDelete('/v1/consent/' + id);
59
+ setAuthConsents(prev => prev.filter(c => c.id !== id));
60
+ showToast(t('profile.access.fedRevoked'));
61
+ } catch (e) {
62
+ showToast(e.message);
63
+ }
64
+ }
65
+
66
+ async function toggleAllowAll() {
67
+ const wildcard = authConsents.find(c => c.recipient === '*');
68
+ if (wildcard) {
69
+ await removeAuthConsent(wildcard.id);
70
+ } else {
71
+ try {
72
+ await apiPost('/v1/consent', {
73
+ data_pattern: '_identity',
74
+ recipient: '*',
75
+ scope: 'auth',
76
+ purpose: 'federation_login_all',
77
+ });
78
+ const data = await apiGet('/v1/consent');
79
+ setAuthConsents((data.data?.consents || []).filter(c => c.scope === 'auth' && c.status === 'active'));
80
+ } catch (e) {
81
+ showToast(e.message);
82
+ }
83
+ }
84
+ }
85
+
86
+ return html`
87
+ <div class="section-title">${t('profile.access.title')}</div>
88
+ <div class="section-desc">${t('profile.access.desc')}</div>
89
+
90
+ <h3 class="card-h3">\u{1F4BB} ${t('profile.access.session')}</h3>
91
+ <div class="card">
92
+ <div class="mem-item"><span class="mem-key">${t('profile.access.owner')}</span><span>${escHtml(session.owner || '-')}</span></div>
93
+ <div class="mem-item"><span class="mem-key">${t('profile.access.ghii')}</span><span>${escHtml(session.ghii || '-')}</span></div>
94
+ <div class="mem-item"><span class="mem-key">${t('profile.access.agentGaii')}</span><span>${escHtml(session.gaii || '-')}</span></div>
95
+ <div class="mem-item"><span class="mem-key">${t('profile.access.node')}</span><span>${escHtml(NODE_URL)}</span></div>
96
+ <div class="mem-item"><span class="mem-key">${t('profile.access.jwtValid')}</span><span>${session.valid ? html`<span class="badge badge-success">${t('profile.access.yes')}</span>` : html`<span class="badge badge-danger">${t('profile.access.expired')}</span>`}</span></div>
97
+ </div>
98
+
99
+ <h3 class="card-h3 mt-section">\u{1F510} ${t('profile.access.publicKey')}</h3>
100
+ <div class="card"><div class="access-mono">${escHtml(session.publicKey || 'N/A')}</div></div>
101
+
102
+ ${ownerKey && html`
103
+ <h3 class="card-h3 mt-section">\u{1F5DD}️ ${t('profile.access.ownerKey')}</h3>
104
+ <div class="card access-card-warn" onClick=${() => copyToClipboard(ownerKey).then(() => showToast(t('profile.access.keyCopied')))}>
105
+ <div class="flex-between">
106
+ <div class="access-mono ${keyBlurred ? 'access-key-blur' : 'access-key-blur revealed'}"
107
+ onMouseEnter=${() => setKeyBlurred(false)} onMouseLeave=${() => setKeyBlurred(true)}>
108
+ ${escHtml(ownerKey)}
109
+ </div>
110
+ <span class="badge badge-warn">${t('profile.access.hoverReveal')}</span>
111
+ </div>
112
+ <div class="access-warn-text">⚠ ${t('profile.access.keepSafe')}</div>
113
+ </div>
114
+ `}
115
+
116
+ <h3 class="card-h3 mt-section">\u{1F517} ${t('profile.access.mcpEndpoint')}</h3>
117
+ <div class="card">
118
+ <div class="access-mcp-url">${escHtml(NODE_URL + '/v1/mcp')}</div>
119
+ <div class="access-mcp-desc">${t('profile.access.mcpDesc')}</div>
120
+ </div>
121
+
122
+ <h3 class="card-h3 mt-section">\u{1F310} ${t('profile.access.fedTitle')}</h3>
123
+ <div class="section-desc">${t('profile.access.fedDesc')}</div>
124
+
125
+ <div class="card">
126
+ <div class="mem-item">
127
+ <label class="flex-row">
128
+ <input type="checkbox"
129
+ checked=${authConsents.some(c => c.recipient === '*')}
130
+ onChange=${toggleAllowAll} />
131
+ ${t('profile.access.fedAllowAll')}
132
+ </label>
133
+ ${authConsents.some(c => c.recipient === '*') && html`
134
+ <span class="badge badge-warn">${t('profile.access.fedAllowAllWarn')}</span>
135
+ `}
136
+ </div>
137
+
138
+ ${authConsents.filter(c => c.recipient !== '*').length === 0 && !authConsents.some(c => c.recipient === '*') && html`
139
+ <p class="adm-text-dim">${t('profile.access.fedNoConsents')}</p>
140
+ `}
141
+ ${authConsents.filter(c => c.recipient !== '*').map(c => html`
142
+ <div class="mem-item">
143
+ <span class="mem-key">${escHtml(c.recipient.replace('node:', ''))}</span>
144
+ <button class="btn-ghost btn-danger" onClick=${() => removeAuthConsent(c.id)}>
145
+ ${t('profile.access.fedRemove')}
146
+ </button>
147
+ </div>
148
+ `)}
149
+
150
+ <div class="mem-item access-fed-add">
151
+ <input type="text" class="input-field input-sm" placeholder=${t('profile.access.fedNodeId')}
152
+ value=${newNodeId} onInput=${e => setNewNodeId(e.target.value)}
153
+ onKeyDown=${e => e.key === 'Enter' && addAuthNode()} />
154
+ <button class="btn-primary" onClick=${addAuthNode} disabled=${fedLoading}>
155
+ ${t('profile.access.fedAddNode')}
156
+ </button>
157
+ </div>
158
+ </div>
159
+ `;
160
+ }
@@ -14,7 +14,7 @@ const html = htm.bind(h);
14
14
  import { t } from '/js/i18n.js';
15
15
  import { escHtml, timeAgo, copyToClipboard } from '/js/utils.js';
16
16
  import { Spinner } from './shared.js';
17
- import { apiGet, apiPost } from '/js/api.js';
17
+ import { apiGet, apiPost, apiPatch } from '/js/api.js';
18
18
  import { listAgents, updateAgentScopes, deleteAgent } from '/js/services/agents.js';
19
19
  import { getNodeUrl } from '/js/services/auth.js';
20
20
  import { useConfirm } from '/components/Modal.js';
@@ -314,6 +314,13 @@ export default function AgentsTab({ session, showToast, onStats }) {
314
314
  } catch { /* ignore */ }
315
315
  }
316
316
 
317
+ async function toggleFederate(agent) {
318
+ try {
319
+ await apiPatch(`/v1/agents/${encodeURIComponent(agent.name)}/federate`, { federate: !agent.federate });
320
+ loadData();
321
+ } catch (e) { showToast(e.message || t('profile.unknownError'), true); }
322
+ }
323
+
317
324
  if (!agents) return html`<${Spinner} text=${t('profile.agents.loadingAgents')} />`;
318
325
 
319
326
  return html`
@@ -417,6 +424,11 @@ export default function AgentsTab({ session, showToast, onStats }) {
417
424
  <div class="card-title">${escHtml(a.display_name || a.name)}</div>
418
425
  </div>
419
426
  <span class="badge badge-info">${escHtml(a.name)}</span>
427
+ <span class="${a.federate ? 'badge badge-success' : 'badge badge-muted'}"
428
+ onClick=${(e) => { e.stopPropagation(); toggleFederate(a); }}
429
+ title=${t('profile.federateTooltip')}>
430
+ ${a.federate ? t('profile.federated') : t('profile.notFederated')}
431
+ </span>
420
432
  </div>
421
433
  <div class="card-subtitle mt-xs">
422
434
  ${t('profile.agents.trust')}: ${a.trust_score ?? '-'} \u2502
@@ -118,6 +118,13 @@ export default function BoardsTab({ session, showToast }) {
118
118
  } catch (e) { showToast(e.message || t('profile.error'), true); }
119
119
  }
120
120
 
121
+ async function toggleBoardFederate(boardId, value) {
122
+ try {
123
+ await boardsService.updateBoardVisibility(boardId, undefined, value);
124
+ loadMyData();
125
+ } catch (e) { showToast(e.message || t('profile.error'), true); }
126
+ }
127
+
121
128
  async function handleDeleteBoard(boardId) {
122
129
  confirm(t('profile.boards.confirmDeleteBoard') || 'Delete this board and all its posts?', async () => {
123
130
  try {
@@ -230,6 +237,11 @@ export default function BoardsTab({ session, showToast }) {
230
237
  title="${t('knowledge.visibility.' + vis)} \u2192 ${t('knowledge.visibility.' + nextVis)}"
231
238
  style="background:${visBg[vis]};color:${visColor[vis]};border-color:${visColor[vis]}"
232
239
  >${t('knowledge.visibility.' + vis)} \u25BE</button>
240
+ ${vis === 'public' && html`<span class="${b.federate ? 'badge badge-success' : 'badge badge-muted'}"
241
+ onClick=${(e) => { e.stopPropagation(); toggleBoardFederate(bid, !b.federate); }}
242
+ title=${t('profile.federateTooltip')}>
243
+ ${b.federate ? t('profile.federated') : t('profile.notFederated')}
244
+ </span>`}
233
245
  <button class="btn-danger-solid btn-sm" onClick=${(e) => { e.stopPropagation(); handleDeleteBoard(bid); }}>${t('profile.boards.deleteBoard')}</button>
234
246
  </div>
235
247
  </div>
@@ -27,6 +27,7 @@ export default function DataWalletTab({ session, showToast }) {
27
27
  const [permSummary, setPermSummary] = useState(null);
28
28
  const [showConsentForm, setShowConsentForm] = useState(false);
29
29
  const [consentFilter, setConsentFilter] = useState('');
30
+ const [scopeFilter, setScopeFilter] = useState('all');
30
31
  const [selectedConsents, setSelectedConsents] = useState(new Set());
31
32
 
32
33
  useEffect(() => {
@@ -108,6 +109,10 @@ export default function DataWalletTab({ session, showToast }) {
108
109
  }
109
110
 
110
111
  const filteredConsents = consents?.filter(c => {
112
+ // Scope filter
113
+ if (scopeFilter === 'federation' && c.scope !== 'federation') return false;
114
+ if (scopeFilter === 'auth' && c.scope !== 'auth') return false;
115
+ // Text filter
111
116
  if (!consentFilter) return true;
112
117
  const q = consentFilter.toLowerCase();
113
118
  const recip = (c.recipient_gaii || c.recipient || '').toLowerCase();
@@ -218,11 +223,25 @@ export default function DataWalletTab({ session, showToast }) {
218
223
  `}
219
224
 
220
225
  ${consents && consents.length > 0 && html`
221
- <div class="flex-row mb-half">
226
+ <div class="flex-row mb-half dw-filter-row">
222
227
  <input type="text" class="input-field dw-filter-input"
223
228
  placeholder=${t('permissions.filterPlaceholder')}
224
229
  value=${consentFilter}
225
230
  onInput=${(e) => { setConsentFilter(e.target.value); setSelectedConsents(new Set()); }} />
231
+ <div class="dw-scope-filters">
232
+ <button class="btn-sm ${scopeFilter === 'all' ? 'btn-primary' : 'btn-outline'}"
233
+ onClick=${() => { setScopeFilter('all'); setSelectedConsents(new Set()); }}>
234
+ ${t('wallet.consents.filterAll')}
235
+ </button>
236
+ <button class="btn-sm ${scopeFilter === 'federation' ? 'btn-primary' : 'btn-outline'}"
237
+ onClick=${() => { setScopeFilter('federation'); setSelectedConsents(new Set()); }}>
238
+ ${t('wallet.consents.filterFederation')}
239
+ </button>
240
+ <button class="btn-sm ${scopeFilter === 'auth' ? 'btn-primary' : 'btn-outline'}"
241
+ onClick=${() => { setScopeFilter('auth'); setSelectedConsents(new Set()); }}>
242
+ ${t('wallet.consents.filterAuth')}
243
+ </button>
244
+ </div>
226
245
  ${selectedConsents.size > 0 && html`
227
246
  <button class="btn-danger text-meta" onClick=${() => handleBulkRevoke(selectedConsents)}>
228
247
  ${t('permissions.revokeSelected')} (${selectedConsents.size})
@@ -266,7 +285,15 @@ export default function DataWalletTab({ session, showToast }) {
266
285
  <td><span class="dw-code-accent">${escHtml(c.data_pattern || c.pattern || '-')}</span></td>
267
286
  <td class="dw-recipient-cell">${recipientBadge(c.recipient_gaii || c.recipient)} <span class="text-meta">${escHtml(c.recipient_gaii || c.recipient || '-')}</span></td>
268
287
  <td>${escHtml(c.purpose || '-')}</td>
269
- <td>${isExpired ? html`<span class="badge badge-muted">expired</span>` : html`<span class="badge badge-success">active</span>`} ${escHtml(c.scope || '-')}</td>
288
+ <td>
289
+ ${isExpired ? html`<span class="badge badge-muted">expired</span>` : html`<span class="badge badge-success">active</span>`}
290
+ ${' '}
291
+ ${c.scope === 'federation'
292
+ ? html`<span class="badge badge-info">${t('wallet.consents.scopeFederation')}</span>`
293
+ : c.scope === 'auth'
294
+ ? html`<span class="badge badge-warn">${t('wallet.consents.scopeAuth')}</span>`
295
+ : html`<span>${escHtml(c.scope || '-')}</span>`}
296
+ </td>
270
297
  <td class="text-meta">${c.granted_at ? new Date(c.granted_at).toLocaleDateString() : '-'}</td>
271
298
  <td class="text-meta">
272
299
  ${expSoon && html`<span class="dw-expiring-icon" title=${t('permissions.expiringWarning')}>\u26A0\uFE0F</span>`}
@@ -25,7 +25,8 @@ import htm from 'htm';
25
25
  const html = htm.bind(h);
26
26
  import { t } from '/js/i18n.js';
27
27
  import { escHtml } from '/js/utils.js';
28
- import { apiGet } from '/js/api.js';
28
+ import { apiGet, apiPost, apiDelete } from '/js/api.js';
29
+ import { listConsents, grantConsent, revokeConsent } from '/js/services/consent.js';
29
30
  import { Spinner } from './shared.js';
30
31
  import { useConfirm } from '/components/Modal.js';
31
32
  import * as knowledgeService from '/js/services/knowledge.js';
@@ -46,6 +47,10 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
46
47
  const [deleting, setDeleting] = useState(null);
47
48
  const [savingSharing, setSavingSharing] = useState(null);
48
49
 
50
+ /* ── Federation consent state ── */
51
+ const [fedConsents, setFedConsents] = useState({}); // { packageName: consentId }
52
+ const [togglingFed, setTogglingFed] = useState(null);
53
+
49
54
  /* ── Discovery state ── */
50
55
  const [discovered, setDiscovered] = useState([]);
51
56
  const [discoverLoading, setDiscoverLoading] = useState(false);
@@ -77,11 +82,58 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
77
82
 
78
83
  useEffect(() => { loadPackages(); }, [loadPackages]);
79
84
 
85
+ /* ── Load federation consents for knowledge packages ── */
86
+ const loadFedConsents = useCallback(async () => {
87
+ try {
88
+ const allConsents = await listConsents();
89
+ const map = {};
90
+ for (const c of allConsents) {
91
+ if (c.scope === 'federation' && (c.data_pattern || c.pattern || '').startsWith('packages/')) {
92
+ const pat = c.data_pattern || c.pattern || '';
93
+ // Extract package name from pattern like "packages/{name}/*"
94
+ const match = pat.match(/^packages\/([^/]+)\//);
95
+ if (match) {
96
+ map[match[1]] = c.id || c.consent_id;
97
+ }
98
+ }
99
+ }
100
+ setFedConsents(map);
101
+ } catch { /* ignore */ }
102
+ }, []);
103
+
104
+ useEffect(() => { loadFedConsents(); }, [loadFedConsents]);
105
+
106
+ /* ── Toggle federation consent for a package ── */
107
+ const toggleFederation = useCallback(async (pkg) => {
108
+ const manifest = pkg.value;
109
+ const packageId = manifest?.id || pkg.key.split('/')[1] || pkg.key;
110
+ setTogglingFed(pkg.key);
111
+ try {
112
+ if (fedConsents[packageId]) {
113
+ // Revoke existing federation consent
114
+ await revokeConsent(fedConsents[packageId]);
115
+ showToast(t('knowledge.unfederateSuccess'));
116
+ } else {
117
+ // Grant federation consent
118
+ await grantConsent({
119
+ data_pattern: `packages/${packageId}/*`,
120
+ recipient: '*',
121
+ scope: 'federation',
122
+ purpose: 'knowledge_sharing',
123
+ });
124
+ showToast(t('knowledge.federateSuccess'));
125
+ }
126
+ await loadFedConsents();
127
+ } catch (err) {
128
+ showToast(err.message || t('profile.error'), true);
129
+ } finally { setTogglingFed(null); }
130
+ }, [fedConsents, showToast, loadFedConsents]);
131
+
80
132
  // Live update listener
81
133
  const loadRef = useRef(loadPackages);
82
134
  loadRef.current = loadPackages;
83
135
  useEffect(() => {
84
- const handler = () => loadRef.current();
136
+ const handler = () => { loadRef.current(); loadFedConsents(); };
85
137
  window.addEventListener('aimeat-live-update', handler);
86
138
  return () => window.removeEventListener('aimeat-live-update', handler);
87
139
  }, []);
@@ -581,6 +633,16 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
581
633
  <span class="kpkg-badge kpkg-badge-type">${t('knowledge.contentTypes.' + (manifest.content_type || 'document')) || (manifest.content_type || 'document').toUpperCase()}</span>
582
634
  <span class="kpkg-badge kpkg-badge-synthesis">${t('knowledge.synthesis.' + (manifest.synthesis?.level || 'original'))}</span>
583
635
  <span class="kpkg-badge kpkg-badge-${manifest.maturity || 'draft'}">${t('knowledge.maturity.' + (manifest.maturity || 'draft'))}</span>
636
+ ${(() => {
637
+ const pkgId = manifest?.id || pkg.key.split('/')[1] || pkg.key;
638
+ const isFed = !!fedConsents[pkgId];
639
+ return html`<button
640
+ class="badge ${isFed ? 'badge-success' : 'badge-muted'} kpkg-fed-toggle"
641
+ title=${t('profile.federateTooltip')}
642
+ disabled=${togglingFed === pkg.key}
643
+ onClick=${(e) => { e.stopPropagation(); toggleFederation(pkg); }}
644
+ >${togglingFed === pkg.key ? '...' : isFed ? t('knowledge.federated') : t('knowledge.notFederated')}</button>`;
645
+ })()}
584
646
  </div>
585
647
  <div class="kpkg-card-tags">
586
648
  ${(manifest.tags || []).map(tag => html`<span class="kpkg-tag" key=${tag}>${escHtml(tag)}</span>`)}
@@ -264,12 +264,15 @@ function ProfileCard({ tier, stats, session, onEditProfile, onChangePassword })
264
264
  </div>
265
265
  <div class="pf-lp-ghii">${escHtml(session.ghii || '')}</div>
266
266
  <div class="pf-lp-node">${t('profile.node')}: ${escHtml(NODE_URL)}</div>
267
- ${isExperienced && typeof stats.nodes === 'number' && stats.nodes > 0 && html`
268
- <div class="pf-federation-badge">
269
- <span class="pf-fed-dot"></span>
270
- ${t('profile.landing.federationBadge').replace('{count}', String(stats.nodes))}
271
- </div>
272
- `}
267
+ ${typeof stats.nodes === 'number' && stats.nodes > 0
268
+ ? html`<div class="pf-federation-badge">
269
+ <span class="pf-fed-dot"></span>
270
+ ${t('profile.federation.statusConnected').replace('{count}', String(stats.nodes))}
271
+ </div>`
272
+ : html`<div class="pf-federation-badge pf-federation-standalone">
273
+ ${t('profile.federation.statusStandalone')}
274
+ </div>`
275
+ }
273
276
  </div>
274
277
  </div>
275
278
  <div class="pf-lp-stats">
@@ -16,7 +16,7 @@ import { Spinner, recipientBadge, VisibilityPill } from './shared.js';
16
16
  import * as memoryService from '/js/services/memory.js';
17
17
  import AuthImage from '/js/components/auth-image.js';
18
18
  import { listAgents } from '/js/services/agents.js';
19
- import { getKeyPermissions } from '/js/services/consent.js';
19
+ import { getKeyPermissions, listConsents, grantConsent, revokeConsent } from '/js/services/consent.js';
20
20
  import { getNodeUrl } from '/js/services/auth.js';
21
21
  import TagCloud from '/js/components/tag-cloud.js';
22
22
  import TagEditor from '/js/components/tag-editor.js';
@@ -38,18 +38,66 @@ export default function MemoryTab({ session, showToast, onStats }) {
38
38
  const [editingFileTags, setEditingFileTags] = useState(null);
39
39
  const [agents, setAgents] = useState([]);
40
40
  const [selectedAgent, setSelectedAgent] = useState('');
41
+ const [fedConsents, setFedConsents] = useState({}); // { memoryKey: consentId }
42
+ const [togglingFed, setTogglingFed] = useState(null);
41
43
 
42
44
  useEffect(() => {
43
- if (session) { loadAgents(); loadMemories(); loadFiles(); }
45
+ if (session) { loadAgents(); loadMemories(); loadFiles(); loadFedConsents(); }
44
46
  }, [session]);
45
47
 
46
48
  useEffect(() => {
47
49
  if (session) { loadMemories(); }
48
50
  }, [selectedAgent]);
49
51
 
52
+ async function loadFedConsents() {
53
+ try {
54
+ const allConsents = await listConsents();
55
+ const map = {};
56
+ for (const c of allConsents) {
57
+ if (c.scope === 'federation') {
58
+ const pat = c.data_pattern || c.pattern || '';
59
+ // Direct key match (no wildcard) or exact pattern
60
+ if (pat && !pat.includes('*')) {
61
+ map[pat] = c.id || c.consent_id;
62
+ }
63
+ }
64
+ }
65
+ setFedConsents(map);
66
+ } catch { /* ignore */ }
67
+ }
68
+
69
+ async function handleShareToFederation(key) {
70
+ setTogglingFed(key);
71
+ try {
72
+ await grantConsent({
73
+ data_pattern: key,
74
+ recipient: '*',
75
+ scope: 'federation',
76
+ purpose: 'federation_sharing',
77
+ });
78
+ showToast(t('profile.memory.shareSuccess'));
79
+ await loadFedConsents();
80
+ } catch (e) {
81
+ showToast(e.message || t('profile.error'), true);
82
+ } finally { setTogglingFed(null); }
83
+ }
84
+
85
+ async function handleStopSharing(key) {
86
+ const consentId = fedConsents[key];
87
+ if (!consentId) return;
88
+ setTogglingFed(key);
89
+ try {
90
+ await revokeConsent(consentId);
91
+ showToast(t('profile.memory.unshareSuccess'));
92
+ await loadFedConsents();
93
+ } catch (e) {
94
+ showToast(e.message || t('profile.error'), true);
95
+ } finally { setTogglingFed(null); }
96
+ }
97
+
50
98
  // Live update listener — refresh both memories and files
51
- const liveRef = useRef(() => { loadMemories(); loadFiles(); });
52
- liveRef.current = () => { loadMemories(); loadFiles(); };
99
+ const liveRef = useRef(() => { loadMemories(); loadFiles(); loadFedConsents(); });
100
+ liveRef.current = () => { loadMemories(); loadFiles(); loadFedConsents(); };
53
101
  useEffect(() => {
54
102
  const handler = () => liveRef.current();
55
103
  window.addEventListener('aimeat-live-update', handler);
@@ -268,6 +316,7 @@ export default function MemoryTab({ session, showToast, onStats }) {
268
316
  >${t('knowledge.visibility.' + vis)} ▾</button>`;
269
317
  })()}
270
318
  <span class="shield-icon" title=${t('permissions.sharingRules')} onClick=${(e) => { e.stopPropagation(); loadKeyPerms(m.key); }}>\u{1F6E1}\uFE0F</span>
319
+ ${fedConsents[m.key] && html`<span class="badge badge-success pf-fed-badge">${t('profile.memory.syncedToFederation')}</span>`}
271
320
  </div>
272
321
  ${expandedMem === m.key && html`
273
322
  <div class="mem-detail">
@@ -304,6 +353,24 @@ export default function MemoryTab({ session, showToast, onStats }) {
304
353
  <div class="mem-actions">
305
354
  <button class="btn-sm" onClick=${() => setEditModal({ key: m.key, value: typeof m.value === 'object' ? JSON.stringify(m.value, null, 2) : String(m.value || ''), visibility: m.visibility || 'private', version: m.version })}>${t('profile.memory.editBtn')}</button>
306
355
  <button class="btn-danger" onClick=${() => handleDeleteMemory(m.key)}>${t('profile.memory.deleteBtn')}</button>
356
+ ${fedConsents[m.key]
357
+ ? html`<button class="btn-outline btn-sm" disabled=${togglingFed === m.key}
358
+ onClick=${() => handleStopSharing(m.key)}>
359
+ ${togglingFed === m.key ? '...' : t('profile.memory.stopSharing')}
360
+ </button>`
361
+ : html`<button class="btn-ghost btn-sm" disabled=${togglingFed === m.key}
362
+ onClick=${() => handleShareToFederation(m.key)}>
363
+ ${togglingFed === m.key ? '...' : t('profile.memory.shareToFederation')}
364
+ </button>`
365
+ }
366
+ ${session.federated && html`
367
+ <button class="btn-ghost" onClick=${() => doPull(m.key)} title=${t('profile.memory.pullFromHome')}>
368
+ ↓ ${t('profile.memory.pullFromHome')}
369
+ </button>
370
+ <button class="btn-ghost" onClick=${() => doPush(m.key)} title=${t('profile.memory.pushToHome')}>
371
+ ↑ ${t('profile.memory.pushToHome')}
372
+ </button>
373
+ `}
307
374
  </div>
308
375
  </div>
309
376
  `}
@@ -392,10 +459,31 @@ export default function MemoryTab({ session, showToast, onStats }) {
392
459
  }`;
393
460
  };
394
461
 
462
+ async function doPull(key) {
463
+ try {
464
+ await memoryService.pullFromHome(key);
465
+ showToast(t('profile.memory.pullSuccess'));
466
+ loadMemories();
467
+ } catch (e) { showToast(e.message, true); }
468
+ }
469
+
470
+ async function doPush(key) {
471
+ try {
472
+ await memoryService.pushToHome(key);
473
+ showToast(t('profile.memory.pushSuccess'));
474
+ } catch (e) { showToast(e.message, true); }
475
+ }
476
+
395
477
  return html`
396
478
  <div class="section-title">${t('profile.memory.title')}</div>
397
479
  <div class="section-desc">${t('profile.memory.desc')}</div>
398
480
 
481
+ ${session.federated && html`
482
+ <div class="alert alert-info">
483
+ <span class="alert-msg">${t('profile.memory.federatedSession')}</span>
484
+ </div>
485
+ `}
486
+
399
487
  ${agents.length > 1 && html`
400
488
  <div class="agent-selector mb-half">
401
489
  <label class="text-meta pf-mr-half">${t('profile.memory.agent') || 'Agent'}:</label>
@@ -11,6 +11,9 @@ export interface JWTPayload {
11
11
  roles: string[];
12
12
  scopes?: string[];
13
13
  mcp_client?: string;
14
+ federated?: boolean;
15
+ homeNode?: string;
16
+ homeUrl?: string;
14
17
  }
15
18
  /** Generate a unique session ID for JWT tracking. */
16
19
  export declare function generateSessionId(): string;
@@ -25,6 +28,9 @@ export interface VerifiedToken {
25
28
  anonymous?: boolean;
26
29
  sessionId?: string;
27
30
  mcp_client?: string;
31
+ federated?: boolean;
32
+ homeNode?: string;
33
+ homeUrl?: string;
28
34
  }
29
35
  export declare function verifyJWT(token: string): Promise<VerifiedToken | null>;
30
36
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/auth/jwt.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAQvD,wBAAgB,iBAAiB,IAAI;IAAE,UAAU,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAGnF;AAED,wBAAsB,YAAY,CAAC,eAAe,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnG;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qDAAqD;AACrD,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqB3G;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAoB5E;AAkBD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmB5D;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjF;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiB/D"}
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/auth/jwt.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAQvD,wBAAgB,iBAAiB,IAAI;IAAE,UAAU,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAGnF;AAED,wBAAsB,YAAY,CAAC,eAAe,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnG;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qDAAqD;AACrD,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwB3G;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAuB5E;AAkBD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmB5D;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjF;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiB/D"}