@synoikia/core 0.5.2 → 0.7.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 (182) hide show
  1. package/dist/app.d.ts +10 -8
  2. package/dist/app.js +14 -4
  3. package/dist/app.js.map +1 -1
  4. package/dist/approvals/grants.d.ts +34 -0
  5. package/dist/approvals/grants.js +79 -0
  6. package/dist/approvals/grants.js.map +1 -0
  7. package/dist/approvals/links.d.ts +2 -5
  8. package/dist/approvals/links.js +2 -5
  9. package/dist/approvals/links.js.map +1 -1
  10. package/dist/approvals/service.d.ts +24 -16
  11. package/dist/approvals/service.js +24 -6
  12. package/dist/approvals/service.js.map +1 -1
  13. package/dist/audit-query.d.ts +1 -5
  14. package/dist/audit-query.js +1 -5
  15. package/dist/audit-query.js.map +1 -1
  16. package/dist/auth/mcp-auth.d.ts +11 -17
  17. package/dist/auth/mcp-auth.js +14 -13
  18. package/dist/auth/mcp-auth.js.map +1 -1
  19. package/dist/auth/mcp-tokens.d.ts +4 -4
  20. package/dist/auth/mcp-tokens.js +11 -4
  21. package/dist/auth/mcp-tokens.js.map +1 -1
  22. package/dist/auth/oauth.d.ts +7 -18
  23. package/dist/auth/oauth.js +18 -18
  24. package/dist/auth/oauth.js.map +1 -1
  25. package/dist/auth/oidc.d.ts +1 -4
  26. package/dist/auth/oidc.js +3 -9
  27. package/dist/auth/oidc.js.map +1 -1
  28. package/dist/auth/sessions.d.ts +9 -4
  29. package/dist/auth/sessions.js +18 -2
  30. package/dist/auth/sessions.js.map +1 -1
  31. package/dist/auth/throttle.d.ts +2 -8
  32. package/dist/auth/throttle.js +2 -8
  33. package/dist/auth/throttle.js.map +1 -1
  34. package/dist/auth/totp.d.ts +1 -4
  35. package/dist/auth/totp.js +1 -4
  36. package/dist/auth/totp.js.map +1 -1
  37. package/dist/auth/users.d.ts +1 -4
  38. package/dist/auth/users.js +3 -8
  39. package/dist/auth/users.js.map +1 -1
  40. package/dist/catalog/groups.d.ts +6 -18
  41. package/dist/catalog/groups.js +7 -23
  42. package/dist/catalog/groups.js.map +1 -1
  43. package/dist/catalog/registry.d.ts +1 -4
  44. package/dist/catalog/registry.js +1 -4
  45. package/dist/catalog/registry.js.map +1 -1
  46. package/dist/catalog/rules.d.ts +6 -18
  47. package/dist/catalog/rules.js +3 -9
  48. package/dist/catalog/rules.js.map +1 -1
  49. package/dist/catalog/sync.d.ts +2 -17
  50. package/dist/catalog/sync.js +5 -20
  51. package/dist/catalog/sync.js.map +1 -1
  52. package/dist/cli.js +2 -9
  53. package/dist/cli.js.map +1 -1
  54. package/dist/config/env.js +3 -8
  55. package/dist/config/env.js.map +1 -1
  56. package/dist/crypto/index.js +1 -4
  57. package/dist/crypto/index.js.map +1 -1
  58. package/dist/crypto/rotate.d.ts +2 -5
  59. package/dist/crypto/rotate.js.map +1 -1
  60. package/dist/db/index.d.ts +1 -4
  61. package/dist/db/index.js +1 -4
  62. package/dist/db/index.js.map +1 -1
  63. package/dist/db/schema.d.ts +44 -0
  64. package/dist/db/schema.js +7 -9
  65. package/dist/db/schema.js.map +1 -1
  66. package/dist/events.d.ts +1 -4
  67. package/dist/events.js.map +1 -1
  68. package/dist/gate/access.d.ts +3 -20
  69. package/dist/gate/access.js +3 -20
  70. package/dist/gate/access.js.map +1 -1
  71. package/dist/gate/attestation.d.ts +2 -6
  72. package/dist/gate/attestation.js +2 -6
  73. package/dist/gate/attestation.js.map +1 -1
  74. package/dist/gate/match.d.ts +5 -13
  75. package/dist/gate/match.js +5 -21
  76. package/dist/gate/match.js.map +1 -1
  77. package/dist/gate/pipeline.d.ts +30 -23
  78. package/dist/gate/pipeline.js +68 -11
  79. package/dist/gate/pipeline.js.map +1 -1
  80. package/dist/gate/preapproval.d.ts +3 -10
  81. package/dist/gate/preapproval.js +2 -5
  82. package/dist/gate/preapproval.js.map +1 -1
  83. package/dist/gate/rate-limit.d.ts +4 -14
  84. package/dist/gate/rate-limit.js +4 -14
  85. package/dist/gate/rate-limit.js.map +1 -1
  86. package/dist/gate/redact.d.ts +10 -26
  87. package/dist/gate/redact.js +12 -26
  88. package/dist/gate/redact.js.map +1 -1
  89. package/dist/http/admin/auth.d.ts +2 -8
  90. package/dist/http/admin/auth.js +20 -5
  91. package/dist/http/admin/auth.js.map +1 -1
  92. package/dist/http/admin/instances.js +19 -1
  93. package/dist/http/admin/instances.js.map +1 -1
  94. package/dist/http/admin/system.js +2 -0
  95. package/dist/http/admin/system.js.map +1 -1
  96. package/dist/http/common.d.ts +2 -5
  97. package/dist/http/common.js +2 -5
  98. package/dist/http/common.js.map +1 -1
  99. package/dist/http/mcp/approval-card.d.ts +6 -0
  100. package/dist/http/mcp/approval-card.js +160 -0
  101. package/dist/http/mcp/approval-card.js.map +1 -0
  102. package/dist/http/mcp/approval-routes.d.ts +2 -6
  103. package/dist/http/mcp/approval-routes.js +33 -8
  104. package/dist/http/mcp/approval-routes.js.map +1 -1
  105. package/dist/http/mcp/endpoint.d.ts +4 -16
  106. package/dist/http/mcp/endpoint.js +108 -28
  107. package/dist/http/mcp/endpoint.js.map +1 -1
  108. package/dist/http/mcp/oauth-routes.d.ts +3 -10
  109. package/dist/http/mcp/oauth-routes.js +15 -9
  110. package/dist/http/mcp/oauth-routes.js.map +1 -1
  111. package/dist/http/mcp/pages.d.ts +8 -1
  112. package/dist/http/mcp/pages.js +21 -2
  113. package/dist/http/mcp/pages.js.map +1 -1
  114. package/dist/http/mcp-app.d.ts +1 -4
  115. package/dist/http/mcp-app.js +1 -4
  116. package/dist/http/mcp-app.js.map +1 -1
  117. package/dist/instances/connection.d.ts +5 -14
  118. package/dist/instances/connection.js +6 -18
  119. package/dist/instances/connection.js.map +1 -1
  120. package/dist/instances/manager.d.ts +10 -18
  121. package/dist/instances/manager.js +33 -19
  122. package/dist/instances/manager.js.map +1 -1
  123. package/dist/instances/settings.d.ts +2 -0
  124. package/dist/instances/settings.js +4 -5
  125. package/dist/instances/settings.js.map +1 -1
  126. package/dist/lenient.d.ts +3 -9
  127. package/dist/lenient.js +3 -9
  128. package/dist/lenient.js.map +1 -1
  129. package/dist/lock.d.ts +4 -10
  130. package/dist/lock.js +5 -15
  131. package/dist/lock.js.map +1 -1
  132. package/dist/log.d.ts +2 -5
  133. package/dist/log.js +2 -5
  134. package/dist/log.js.map +1 -1
  135. package/dist/maintenance.js +2 -5
  136. package/dist/maintenance.js.map +1 -1
  137. package/dist/notify/service.d.ts +2 -5
  138. package/dist/notify/service.js +2 -5
  139. package/dist/notify/service.js.map +1 -1
  140. package/dist/plugins/default-repo.d.ts +1 -4
  141. package/dist/plugins/default-repo.js +1 -4
  142. package/dist/plugins/default-repo.js.map +1 -1
  143. package/dist/plugins/discovery.d.ts +2 -5
  144. package/dist/plugins/discovery.js +2 -5
  145. package/dist/plugins/discovery.js.map +1 -1
  146. package/dist/plugins/minisign.d.ts +2 -6
  147. package/dist/plugins/minisign.js.map +1 -1
  148. package/dist/plugins/process.d.ts +1 -4
  149. package/dist/plugins/process.js +1 -4
  150. package/dist/plugins/process.js.map +1 -1
  151. package/dist/plugins/repos.d.ts +4 -16
  152. package/dist/plugins/repos.js +7 -25
  153. package/dist/plugins/repos.js.map +1 -1
  154. package/dist/plugins/supervisor.d.ts +3 -9
  155. package/dist/plugins/supervisor.js +5 -5
  156. package/dist/plugins/supervisor.js.map +1 -1
  157. package/dist/runtime/executions.d.ts +68 -0
  158. package/dist/runtime/executions.js +158 -0
  159. package/dist/runtime/executions.js.map +1 -0
  160. package/dist/runtime/index.d.ts +7 -10
  161. package/dist/runtime/index.js +116 -31
  162. package/dist/runtime/index.js.map +1 -1
  163. package/dist/sandbox/index.d.ts +3 -10
  164. package/dist/sandbox/index.js +2 -3
  165. package/dist/sandbox/index.js.map +1 -1
  166. package/dist/settings.d.ts +3 -4
  167. package/dist/settings.js +9 -4
  168. package/dist/settings.js.map +1 -1
  169. package/dist/testing/contract.d.ts +2 -7
  170. package/dist/testing/contract.js.map +1 -1
  171. package/dist/testing/fake-http.d.ts +1 -5
  172. package/dist/testing/fake-http.js.map +1 -1
  173. package/dist/testing/index.d.ts +14 -12
  174. package/dist/testing/index.js +21 -6
  175. package/dist/testing/index.js.map +1 -1
  176. package/dist/testing/repository.d.ts +2 -7
  177. package/dist/testing/repository.js +3 -12
  178. package/dist/testing/repository.js.map +1 -1
  179. package/drizzle/0012_sensitive_result.sql +7 -0
  180. package/drizzle/meta/0012_snapshot.json +2368 -0
  181. package/drizzle/meta/_journal.json +7 -0
  182. package/package.json +2 -2
@@ -0,0 +1,160 @@
1
+ /** The approval card (design §5.7): an MCP Apps view that hosts render for `execute` and `resume` results.
2
+ * It shows state and opens the approval page; it never decides. Static, no external origins. */
3
+ export const APPROVAL_CARD_URI = 'ui://synoikia/approval';
4
+ export const MCP_APP_MIME = 'text/html;profile=mcp-app';
5
+ export const MCP_APPS_EXTENSION = 'io.modelcontextprotocol/ui';
6
+ export const APPROVAL_CARD_HTML = `<!doctype html>
7
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <style>
9
+ :root { color-scheme: light; --bg:#eee4d3; --line:#d9c9ab; --ink:#241b12; --muted:#5c5044; --brand:#b5502f; --brand-strong:#8f3d22;
10
+ --on-brand:#fff; --ok:#3f6b45; --ok-bg:#dfe9dc; --warn:#8a6514; --warn-bg:#f3e6c4; --no:#9a2f2f; --no-bg:#f0dcda;
11
+ --sans:"IBM Plex Sans", system-ui, -apple-system, sans-serif; --mono:"IBM Plex Mono", ui-monospace, monospace; }
12
+ :root[data-theme="dark"] { color-scheme: dark; --bg:#1d1914; --line:#3a3026; --ink:#f2e9db; --muted:#b8ab98; --brand:#d97250;
13
+ --brand-strong:#e88a63; --on-brand:#14110e; --ok:#8fbf8f; --ok-bg:#1f2a1f; --warn:#e0b860; --warn-bg:#2c2414; --no:#e8685f; --no-bg:#2e1c1b; }
14
+ * { box-sizing: border-box; }
15
+ html, body { margin: 0; background: transparent; color: var(--ink); font: 14px/1.45 var(--sans); }
16
+ .card { border: 1px solid var(--line); border-radius: 12px; background: var(--bg); overflow: hidden; }
17
+ .hd { padding: 8px 12px; border-bottom: 1px solid var(--line); font-size: 11px; font-weight: 600; letter-spacing: .06em;
18
+ text-transform: uppercase; color: var(--muted); }
19
+ .bd { padding: 12px; display: grid; gap: 8px; }
20
+ .t { font-weight: 600; font-size: 15px; overflow-wrap: anywhere; }
21
+ .op { font: 12px/1.4 var(--mono); color: var(--muted); overflow-wrap: anywhere; }
22
+ .ft { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; }
23
+ .pill { font-size: 12px; font-weight: 600; padding: 3px 9px; border-radius: 999px; }
24
+ .pending { background: var(--warn-bg); color: var(--warn); } .ok { background: var(--ok-bg); color: var(--ok); }
25
+ .no { background: var(--no-bg); color: var(--no); }
26
+ button { font: 600 13px/1.2 var(--sans); padding: 7px 14px; border-radius: 8px; border: 1px solid var(--brand); cursor: pointer;
27
+ background: var(--brand); color: var(--on-brand); }
28
+ button:hover { background: var(--brand-strong); }
29
+ button.quiet { background: transparent; color: var(--no); border-color: var(--line); padding: 4px 10px; font-size: 12px; }
30
+ .grant { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 8px 12px; border: 1px solid var(--line);
31
+ border-radius: 10px; background: var(--ok-bg); color: var(--ok); font-size: 12.5px; }
32
+ .grant button { margin-left: auto; }
33
+ .note { font-size: 12px; color: var(--muted); }
34
+ [hidden] { display: none !important; }
35
+ </style></head>
36
+ <body>
37
+ <div id="root" hidden></div>
38
+ <script>
39
+ (() => {
40
+ const root = document.getElementById('root');
41
+ let nextId = 1;
42
+ const waiting = new Map();
43
+ const send = (msg) => window.parent.postMessage(Object.assign({ jsonrpc: '2.0' }, msg), '*');
44
+ const request = (method, params) =>
45
+ new Promise((resolve, reject) => {
46
+ const id = nextId++;
47
+ waiting.set(id, { resolve, reject });
48
+ send({ id, method, params });
49
+ });
50
+ const notify = (method, params) => send({ method, params: params || {} });
51
+ const resize = () => notify('ui/notifications/size-changed', { height: root.hidden ? 0 : document.body.scrollHeight });
52
+ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => '&#' + c.charCodeAt(0) + ';');
53
+ const payloadOf = (res) => {
54
+ if (res && res.structuredContent) return res.structuredContent;
55
+ const text = res && Array.isArray(res.content) ? res.content.find((c) => c.type === 'text') : null;
56
+ try { return text ? JSON.parse(text.text) : null; } catch { return null; }
57
+ };
58
+ const timeLeft = (iso) => {
59
+ const ms = new Date(iso).getTime() - Date.now();
60
+ if (!(ms > 0)) return 'expired';
61
+ const m = Math.floor(ms / 60000), s = Math.floor((ms % 60000) / 1000);
62
+ return 'expires in ' + m + ':' + String(s).padStart(2, '0');
63
+ };
64
+
65
+ let state = null;
66
+ let poll = null;
67
+ let told = false;
68
+
69
+ function render() {
70
+ if (!state || (!state.approval && !state.grant)) { root.hidden = true; resize(); return; }
71
+ root.hidden = false;
72
+ let html = '';
73
+ if (state.approval) {
74
+ const a = state.approval;
75
+ const st = state.status || 'pending';
76
+ const pill = st === 'pending'
77
+ ? '<span class="pill pending" id="left">● ' + esc(timeLeft(a.expiresAt)) + '</span>'
78
+ : st === 'approved' || (st === 'done' && state.decision === 'approved')
79
+ ? '<span class="pill ok">✓ Approved</span>'
80
+ : st === 'running' ? '<span class="pill ok">Running</span>'
81
+ : '<span class="pill no">✕ ' + esc(st === 'done' ? (state.decision || 'finished').replace('_', ' ') : st.replace('_', ' ')) + '</span>';
82
+ html += '<div class="card"><div class="hd">Synoikia · approval needed</div><div class="bd">' +
83
+ '<div class="t">' + esc(a.summary) + '</div><div class="op">' + esc(a.operationKey) + '</div>' +
84
+ '<div class="ft">' + pill + (st === 'pending' ? '<button id="open" type="button">Review and approve ↗</button>' : '') + '</div>' +
85
+ (st === 'pending' ? '<div class="note">Approve or deny on the Synoikia page. This chat continues by itself after you decide.</div>' : '') +
86
+ '</div></div>';
87
+ }
88
+ if (state.grant) {
89
+ html += '<div class="grant" style="margin-top:' + (state.approval ? '8px' : '0') + '">✓ Session approval active until ' +
90
+ esc(new Date(state.grant.expiresAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })) +
91
+ ' <button class="quiet" id="revoke" type="button">Revoke</button></div>';
92
+ }
93
+ root.innerHTML = html;
94
+ const open = document.getElementById('open');
95
+ if (open) open.onclick = () => request('ui/open-link', { url: state.approval.url }).catch(() => {});
96
+ const revoke = document.getElementById('revoke');
97
+ if (revoke) revoke.onclick = () =>
98
+ request('tools/call', { name: 'session_grant_revoke', arguments: { grantId: state.grant.id } })
99
+ .then(() => { state.grant = null; render(); }).catch(() => {});
100
+ resize();
101
+ }
102
+
103
+ async function tell(text) {
104
+ if (told) return;
105
+ told = true;
106
+ try { await request('ui/message', { role: 'user', content: [{ type: 'text', text }] }); }
107
+ catch {
108
+ try { await request('ui/message', { role: 'user', content: { type: 'text', text } }); }
109
+ catch { const n = document.createElement('div'); n.className = 'note'; n.textContent = 'Say "continue" in the chat to get the result.'; root.appendChild(n); resize(); }
110
+ }
111
+ }
112
+
113
+ async function check() {
114
+ if (!state || !state.executionId || !state.approval) return;
115
+ let s;
116
+ try { s = payloadOf(await request('tools/call', { name: 'approval_status', arguments: { executionId: state.executionId } })); }
117
+ catch { return; }
118
+ if (!s || !s.state) return;
119
+ if (s.state === 'pending') { const left = document.getElementById('left'); if (left) left.textContent = '● ' + timeLeft(state.approval.expiresAt); return; }
120
+ state.status = s.state; state.decision = s.decision || s.state;
121
+ clearInterval(poll); poll = null;
122
+ render();
123
+ const approved = s.state === 'approved' || s.state === 'running' || (s.state === 'done' && s.decision === 'approved');
124
+ tell(approved ? 'Approved, continue.' : 'The approval was ' + String(state.decision).replace('_', ' ') + '. Continue.');
125
+ }
126
+
127
+ function onResult(res) {
128
+ const p = payloadOf(res) || {};
129
+ state = { executionId: p.executionId, approval: p.status === 'awaiting_approval' ? p.approval : null, grant: p.sessionGrant || null, status: 'pending' };
130
+ told = false;
131
+ render();
132
+ if (poll) clearInterval(poll);
133
+ if (state.approval) poll = setInterval(check, 2000);
134
+ }
135
+
136
+ window.addEventListener('message', (ev) => {
137
+ if (ev.source !== window.parent) return;
138
+ const m = ev.data;
139
+ if (!m || m.jsonrpc !== '2.0') return;
140
+ if (m.id != null && waiting.has(m.id) && !m.method) {
141
+ const w = waiting.get(m.id); waiting.delete(m.id);
142
+ if (m.error) w.reject(m.error); else w.resolve(m.result);
143
+ return;
144
+ }
145
+ if (m.method === 'ui/notifications/tool-result') onResult(m.params);
146
+ if (m.method === 'ui/notifications/host-context-changed' && m.params && m.params.theme)
147
+ document.documentElement.dataset.theme = m.params.theme;
148
+ });
149
+
150
+ request('ui/initialize', { appInfo: { name: 'synoikia-approval', version: '1' }, appCapabilities: {}, protocolVersion: '2026-01-26' })
151
+ .then((r) => {
152
+ const theme = r && r.hostContext && r.hostContext.theme;
153
+ document.documentElement.dataset.theme = theme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
154
+ notify('ui/notifications/initialized');
155
+ })
156
+ .catch(() => {});
157
+ })();
158
+ </script>
159
+ </body></html>`;
160
+ //# sourceMappingURL=approval-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-card.js","sourceRoot":"","sources":["../../../src/http/mcp/approval-card.ts"],"names":[],"mappings":"AAAA;gGACgG;AAEhG,MAAM,CAAC,MAAM,iBAAiB,GAAG,wBAAwB,CAAC;AAC1D,MAAM,CAAC,MAAM,YAAY,GAAG,2BAA2B,CAAC;AACxD,MAAM,CAAC,MAAM,kBAAkB,GAAG,4BAA4B,CAAC;AAE/D,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAyJnB,CAAC"}
@@ -1,9 +1,5 @@
1
1
  import type { Hono } from 'hono';
2
2
  import type { AppContext } from '../../app.js';
3
- /**
4
- * The approval page (design §5.3). The MCP client is asked to open it (URL-mode elicitation); the
5
- * link only opens this page. Deciding needs a signed-in user with TOTP enrolled, a TOTP proof in this
6
- * session (a fresh one for locked operations) and a CSRF-protected POST, so neither the client that
7
- * holds the link nor a prefetch can decide anything.
8
- */
3
+ /** The approval page (design §5.3): deciding needs a signed-in user with a TOTP proof in this browser
4
+ * (fresh for locked ops) and a CSRF-protected POST, so neither the link holder nor a prefetch decides. */
9
5
  export declare function registerApprovalRoutes(app: Hono, ctx: AppContext): void;
@@ -6,12 +6,21 @@ import { checkUiCsrf, recordTotpProof, renderLogin, sinceTotpProof, uiCsrf, uiSe
6
6
  import { approvalPage, errorPage } from './pages.js';
7
7
  /** A locked operation needs a TOTP code proved this recently (design §5.3). */
8
8
  const LOCKED_TOTP_MAX_AGE_MS = 5 * 60_000;
9
- /**
10
- * The approval page (design §5.3). The MCP client is asked to open it (URL-mode elicitation); the
11
- * link only opens this page. Deciding needs a signed-in user with TOTP enrolled, a TOTP proof in this
12
- * session (a fresh one for locked operations) and a CSRF-protected POST, so neither the client that
13
- * holds the link nor a prefetch can decide anything.
14
- */
9
+ /** "Approve for this session" lengths (design §5.8), capped by the endpoint's `sessionGrantMaxHours`. */
10
+ function grantOptions(now, maxHours) {
11
+ if (maxHours <= 0)
12
+ return [];
13
+ const opts = [1, 4]
14
+ .filter((h) => h <= maxHours)
15
+ .map((h) => ({ value: String(h), label: `${h} h`, until: new Date(now.getTime() + h * 3_600_000) }));
16
+ const midnight = new Date(now);
17
+ midnight.setHours(24, 0, 0, 0);
18
+ if (midnight.getTime() - now.getTime() <= maxHours * 3_600_000)
19
+ opts.push({ value: 'midnight', label: 'until midnight', until: midnight });
20
+ return opts;
21
+ }
22
+ /** The approval page (design §5.3): deciding needs a signed-in user with a TOTP proof in this browser
23
+ * (fresh for locked ops) and a CSRF-protected POST, so neither the link holder nor a prefetch decides. */
15
24
  export function registerApprovalRoutes(app, ctx) {
16
25
  const gone = (c) => errorPage(c, 'Link expired', 'This approval request has expired or was already decided.', 404);
17
26
  const load = (c, token) => {
@@ -30,6 +39,10 @@ export function registerApprovalRoutes(app, ctx) {
30
39
  .get();
31
40
  return row ? { link, ...row } : null;
32
41
  };
42
+ /** Locked and typed-confirmation operations never get a session grant. */
43
+ const grantsFor = (data) => data.op.locked || data.approval.confirmLiteral
44
+ ? []
45
+ : grantOptions(ctx.now(), ctx.instances.settingsOf(data.instance).sessionGrantMaxHours);
33
46
  /** Every approval needs a TOTP proof in this session (OIDC sign-ins have none); locked ops a fresh one. */
34
47
  const needsTotp = (session, data) => {
35
48
  const since = sinceTotpProof(ctx, session);
@@ -51,6 +64,8 @@ export function registerApprovalRoutes(app, ctx) {
51
64
  expiresAt: data.approval.expiresAt,
52
65
  confirmLiteral: data.approval.confirmLiteral,
53
66
  needsTotp: needsTotp(session, data),
67
+ grantOptions: grantsFor(data).map(({ value, label }) => ({ value, label })),
68
+ client: data.approval.clientId,
54
69
  status: data.approval.status,
55
70
  error,
56
71
  }, error ? 400 : 200);
@@ -79,9 +94,15 @@ export function registerApprovalRoutes(app, ctx) {
79
94
  return noTotp(c);
80
95
  if (!checkUiCsrf(c, body.csrf))
81
96
  return render(c, token, data, session, 'The form expired; try again.');
82
- const approve = body.decision === 'approve';
97
+ const approve = body.decision === 'approve' || body.decision === 'approve_session';
83
98
  if (!approve && body.decision !== 'deny')
84
99
  return render(c, token, data, session, 'Choose approve or deny.');
100
+ let sessionGrantUntil;
101
+ if (body.decision === 'approve_session') {
102
+ sessionGrantUntil = grantsFor(data).find((o) => o.value === body.grant)?.until;
103
+ if (!sessionGrantUntil)
104
+ return render(c, token, data, session, 'Choose a session length.');
105
+ }
85
106
  if (approve && needsTotp(session, data)) {
86
107
  const user = session.user;
87
108
  if (ctx.throttle.lockedFor(user.username, 'mcp') > 0)
@@ -104,6 +125,7 @@ export function registerApprovalRoutes(app, ctx) {
104
125
  approve,
105
126
  confirm: typeof body.confirm === 'string' ? body.confirm : undefined,
106
127
  decidedBy: session.user.username,
128
+ sessionGrantUntil,
107
129
  });
108
130
  }
109
131
  catch (err) {
@@ -111,7 +133,10 @@ export function registerApprovalRoutes(app, ctx) {
111
133
  return render(c, token, data, session, err.message);
112
134
  throw err;
113
135
  }
114
- return errorPage(c, approve ? 'Approved' : 'Denied', `${data.op.key} on /${data.instance.slug} was ${approve ? 'approved' : 'denied'}. You can close this page.`, 200);
136
+ const granted = sessionGrantUntil
137
+ ? ` Ask operations of this client on /${data.instance.slug} run without asking until ${sessionGrantUntil.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}.`
138
+ : '';
139
+ return errorPage(c, approve ? 'Approved' : 'Denied', `${data.op.key} on /${data.instance.slug} was ${approve ? 'approved' : 'denied'}.${granted} You can close this page; your chat continues by itself.`, 200);
115
140
  });
116
141
  }
117
142
  //# sourceMappingURL=approval-routes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"approval-routes.js","sourceRoot":"","sources":["../../../src/http/mcp/approval-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"approval-routes.js","sourceRoot":"","sources":["../../../src/http/mcp/approval-routes.ts"],"names":[],"mappings":"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"}
@@ -4,17 +4,9 @@ import type { Config } from '../../config/env.js';
4
4
  import type { OAuthService } from '../../auth/oauth.js';
5
5
  /** Open MCP sessions per authenticated principal; a new one past the cap closes that principal's oldest. */
6
6
  export declare const MAX_SESSIONS_PER_PRINCIPAL = 16;
7
- /**
8
- * DNS-rebinding defence (MCP transport spec): a browser page can make its own domain resolve to this
9
- * server, but it can't change the Host it sends, nor the Origin. Host must be a name this server was
10
- * configured for (PUBLIC_MCP_URL, MCP_ALLOWED_HOSTS) or something that can't be rebound (localhost, an
11
- * IP literal); an Origin, when present, must be one of those too.
12
- */
7
+ /** DNS-rebinding defence: Host (and Origin, when sent) must be a configured name, localhost or an IP. */
13
8
  export declare function hostAllowed(config: Config, hostHeader: string | undefined): boolean;
14
- /**
15
- * One refusal line per key (level, client IP, status) per minute, so a flood of refused requests
16
- * can't flood the log. The next line after a window reports how many were dropped.
17
- */
9
+ /** At most one refusal log line per key per minute; the next one reports how many were dropped. */
18
10
  export declare class LogThrottle {
19
11
  private readonly windows;
20
12
  /** The number of lines dropped since the last one for this key, or null to drop this one too. */
@@ -34,12 +26,8 @@ export declare class McpEndpoints {
34
26
  private buildServer;
35
27
  /** The request carries a bearer or OAuth access token that is live somewhere on this server. */
36
28
  private holdsLiveCredential;
37
- /**
38
- * Why a request was refused, on the server log only (the client gets the generic answer). Requests
39
- * that presented a token-shaped credential are `warn`: someone is trying to connect and failing.
40
- * Bare probes (scanners, a client's first unauthenticated step of OAuth discovery) are `debug`. The
41
- * reason is a quoted field, never part of the message: it can carry client-controlled text.
42
- */
29
+ /** Logs why a request was refused (the client gets the generic answer): `warn` for token-shaped
30
+ * credentials, `debug` for bare probes; the reason is a quoted field, never the message. */
43
31
  private refused;
44
32
  /** A 4xx/5xx from the MCP transport itself (bad JSON-RPC, wrong Accept header, …), with its message. */
45
33
  private logTransportError;
@@ -3,15 +3,14 @@ import { isIP } from 'node:net';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
5
5
  import { z } from 'zod';
6
- import { authenticateMcp, effectiveAuthMode, publicMcpBase } from '../../auth/mcp-auth.js';
6
+ import { authenticateMcp, effectiveAuthMode, principalLive, publicMcpBase } from '../../auth/mcp-auth.js';
7
7
  import { TOKEN_PREFIX } from '../../auth/mcp-tokens.js';
8
8
  import { ACCESS_PREFIX } from '../../auth/oauth.js';
9
- import { executeCode, searchCode } from '../../runtime/index.js';
9
+ import { executeCode, resumeExecution, searchCode } from '../../runtime/index.js';
10
10
  import { clientIp } from '../common.js';
11
- /**
12
- * `/{slug}` Streamable HTTP endpoints (design §2.2): one McpServer + transport per MCP session,
13
- * bound to its instance and authenticated principal, exposing exactly `search` and `execute`.
14
- */
11
+ import { APPROVAL_CARD_HTML, APPROVAL_CARD_URI, MCP_APP_MIME, MCP_APPS_EXTENSION } from './approval-card.js';
12
+ /** `/{slug}` Streamable HTTP endpoints (design §2.2): one McpServer per session, bound to its instance
13
+ * and principal, exposing `search`, `execute` and `resume`, plus the approval card's app-only tools. */
15
14
  const SESSION_IDLE_MS = 30 * 60_000;
16
15
  /** Open MCP sessions per authenticated principal; a new one past the cap closes that principal's oldest. */
17
16
  export const MAX_SESSIONS_PER_PRINCIPAL = 16;
@@ -21,7 +20,30 @@ const jsonRpcError = (code, message) => ({
21
20
  id: null,
22
21
  error: { code, message },
23
22
  });
24
- function toolText(result) {
23
+ function toolText(result, extras) {
24
+ const sessionGrant = extras?.grant && { id: extras.grant.id, expiresAt: extras.grant.expiresAt.toISOString() };
25
+ if (!result.ok && 'status' in result) {
26
+ // Parked (design §5.6): not an error; the agent shows the link and calls resume.
27
+ const body = {
28
+ status: result.status,
29
+ executionId: result.executionId,
30
+ ...(result.approval
31
+ ? {
32
+ approval: {
33
+ url: `${extras?.publicBase ?? ''}${result.approval.path}`,
34
+ operationKey: result.approval.operationKey,
35
+ summary: result.approval.summary,
36
+ expiresAt: result.approval.expiresAt.toISOString(),
37
+ },
38
+ }
39
+ : {}),
40
+ next: result.status === 'awaiting_approval'
41
+ ? 'Show approval.url to the user. After they approve or deny it, call resume with this executionId.'
42
+ : 'Call resume with this executionId.',
43
+ ...(sessionGrant ? { sessionGrant } : {}),
44
+ };
45
+ return { structuredContent: body, content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] };
46
+ }
25
47
  if (!result.ok) {
26
48
  return {
27
49
  isError: true,
@@ -42,7 +64,16 @@ function toolText(result) {
42
64
  ...(result.truncated ? { truncated: true } : {}),
43
65
  ...(result.logs.length ? { logs: result.logs } : {}),
44
66
  };
45
- return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] };
67
+ return {
68
+ ...(sessionGrant ? { structuredContent: { sessionGrant } } : {}),
69
+ content: [{ type: 'text', text: JSON.stringify(body, null, 2) }],
70
+ };
71
+ }
72
+ function describeResume(manifest) {
73
+ return [
74
+ `Continue an execute call on ${manifest.name} that is waiting for approval, and get its result.`,
75
+ 'Pass the `executionId` from the `awaiting_approval` result. It waits up to 45 seconds; if the human has not decided yet, it returns `awaiting_approval` again with the same link. If the script reaches another approval, it returns the new link.',
76
+ ].join('\n');
46
77
  }
47
78
  function describeSearch(manifest) {
48
79
  const ops = manifest.labels.operations.toLowerCase();
@@ -68,16 +99,12 @@ function describeExecute(manifest) {
68
99
  const fns = manifest.binding.functions.map((f) => `${ns}.${f}(…)`).join(', ');
69
100
  return [
70
101
  `Run code against ${manifest.name}. \`code\` is the body of an async JavaScript function; \`await\` ${fns} and return a JSON-serializable result.`,
71
- 'Reads run immediately. Writes either run straight away or pause until a human approves them on an approval page the client is asked to open (see `approval` in catalog entries); denials and other refusals throw an Error with `err.code` (e.g. OPERATION_DISABLED, PERMISSION_DENIED, UPSTREAM_ERROR) that your code can catch.',
102
+ 'Reads run immediately. Writes either run straight away or wait until a human approves them on an approval page (see `approval` in catalog entries); denials and other refusals throw an Error with `err.code` (e.g. OPERATION_DISABLED, PERMISSION_DENIED, UPSTREAM_ERROR) that your code can catch.',
103
+ 'If the result has `status: "awaiting_approval"`, the run is paused on the server: show `approval.url` to the user, and after they decide, call `resume` with its `executionId` (do not run the code again). Each write runs once.',
72
104
  'Calls run one at a time. There is no network, filesystem or timer access. Secrets in results are redacted.',
73
105
  ].join('\n');
74
106
  }
75
- /**
76
- * DNS-rebinding defence (MCP transport spec): a browser page can make its own domain resolve to this
77
- * server, but it can't change the Host it sends, nor the Origin. Host must be a name this server was
78
- * configured for (PUBLIC_MCP_URL, MCP_ALLOWED_HOSTS) or something that can't be rebound (localhost, an
79
- * IP literal); an Origin, when present, must be one of those too.
80
- */
107
+ /** DNS-rebinding defence: Host (and Origin, when sent) must be a configured name, localhost or an IP. */
81
108
  export function hostAllowed(config, hostHeader) {
82
109
  if (!hostHeader)
83
110
  return false;
@@ -113,10 +140,7 @@ function presentsCredential(c) {
113
140
  }
114
141
  const THROTTLE_WINDOW_MS = 60_000;
115
142
  const THROTTLE_MAX_KEYS = 1000;
116
- /**
117
- * One refusal line per key (level, client IP, status) per minute, so a flood of refused requests
118
- * can't flood the log. The next line after a window reports how many were dropped.
119
- */
143
+ /** At most one refusal log line per key per minute; the next one reports how many were dropped. */
120
144
  export class LogThrottle {
121
145
  windows = new Map();
122
146
  /** The number of lines dropped since the last one for this key, or null to drop this one too. */
@@ -175,7 +199,8 @@ export class McpEndpoints {
175
199
  const rt = () => this.ctx.instances.runtime(instanceId);
176
200
  const manifest = rt().manifest;
177
201
  const server = new McpServer({ name: `synoikia/${rt().slug}`, version: SERVER_VERSION }, {
178
- instructions: `${manifest.name} via Synoikia. Use search to discover operations, then execute to call them.`,
202
+ instructions: `${manifest.name} via Synoikia. Use search to discover operations, then execute to call them. When execute returns awaiting_approval, show the link and call resume after the user decides.`,
203
+ capabilities: { extensions: { [MCP_APPS_EXTENSION]: {} } },
179
204
  });
180
205
  // Approval prompts (design §5.3): URL mode sends the human to our approval page; form mode is only
181
206
  // used where the endpoint opted in. `elicitation: {}` from older clients means form support.
@@ -209,6 +234,12 @@ export class McpEndpoints {
209
234
  mcpSessionId: sessionId,
210
235
  principal: { ceiling: identity.access },
211
236
  prompts: prompts(),
237
+ parkable: true,
238
+ stillAuthorized: () => principalLive(this.ctx, identity.principal, instanceId),
239
+ });
240
+ const extras = () => ({
241
+ publicBase,
242
+ grant: this.ctx.grants.active(instanceId, identity.principal),
212
243
  });
213
244
  const run = async (fn, code, sessionId, request) => {
214
245
  try {
@@ -223,8 +254,12 @@ export class McpEndpoints {
223
254
  }
224
255
  // A cancelled request or a closed session ends the run: nothing it queued may run afterwards.
225
256
  const signal = AbortSignal.any([request, closed]);
226
- return toolText(await fn(this.ctx.gateDeps(), rt(), caller(sessionId), code, signal));
257
+ const result = await fn(this.ctx.gateDeps(), rt(), caller(sessionId), code, signal);
258
+ return toolText(result, extras());
227
259
  };
260
+ const ui = { ui: { resourceUri: APPROVAL_CARD_URI } };
261
+ const appOnly = { ui: { resourceUri: APPROVAL_CARD_URI, visibility: ['app'] } };
262
+ const executionId = z.string().uuid().describe('The executionId from an awaiting_approval result.');
228
263
  const input = {
229
264
  code: z
230
265
  .string()
@@ -244,7 +279,52 @@ export class McpEndpoints {
244
279
  inputSchema: input,
245
280
  // Hints for clients that confirm tool calls themselves; the gate never relies on them.
246
281
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
282
+ _meta: ui,
247
283
  }, ({ code }, extra) => run(executeCode, code, extra.sessionId, extra.signal));
284
+ server.registerTool('resume', {
285
+ title: `Resume on ${manifest.name}`,
286
+ description: describeResume(manifest),
287
+ inputSchema: { executionId },
288
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
289
+ _meta: ui,
290
+ }, async ({ executionId: id }, extra) => {
291
+ const signal = AbortSignal.any([extra.signal, closed]);
292
+ return toolText(await resumeExecution(this.ctx.gateDeps(), rt(), caller(extra.sessionId), id, signal), extras());
293
+ });
294
+ // App-only (design §5.7): the approval card polls state and can end a grant; neither can approve.
295
+ server.registerTool('approval_status', {
296
+ title: 'Approval status',
297
+ description: 'State of a parked execution, for the approval card.',
298
+ inputSchema: { executionId },
299
+ annotations: { readOnlyHint: true, openWorldHint: false },
300
+ _meta: appOnly,
301
+ }, ({ executionId: id }) => {
302
+ const status = this.ctx.executions.status(id, identity.principal, instanceId);
303
+ const body = status ?? { state: 'not_found' };
304
+ return { structuredContent: { ...body }, content: [{ type: 'text', text: JSON.stringify(body) }] };
305
+ });
306
+ server.registerTool('session_grant_revoke', {
307
+ title: 'Revoke session approval',
308
+ description: 'Ends the session approval of this client on this endpoint, for the approval card.',
309
+ inputSchema: { grantId: z.string().uuid() },
310
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
311
+ _meta: appOnly,
312
+ }, ({ grantId }) => {
313
+ const grant = this.ctx.grants.get(grantId);
314
+ const mine = grant && grant.instanceId === instanceId && grant.principal === identity.principal;
315
+ const revoked = !!mine && this.ctx.grants.revoke(grantId, { actorKind: 'mcp_client', actorId: identity.label });
316
+ return { content: [{ type: 'text', text: JSON.stringify({ revoked }) }] };
317
+ });
318
+ server.registerResource('approval-card', APPROVAL_CARD_URI, { title: 'Synoikia approval card', mimeType: MCP_APP_MIME }, () => ({
319
+ contents: [
320
+ {
321
+ uri: APPROVAL_CARD_URI,
322
+ mimeType: MCP_APP_MIME,
323
+ text: APPROVAL_CARD_HTML,
324
+ _meta: { ui: { csp: { connectDomains: [], resourceDomains: [] }, prefersBorder: false } },
325
+ },
326
+ ],
327
+ }));
248
328
  return server;
249
329
  }
250
330
  /** The request carries a bearer or OAuth access token that is live somewhere on this server. */
@@ -254,12 +334,8 @@ export class McpEndpoints {
254
334
  return false;
255
335
  return !!this.ctx.tokens.verify(token) || !!this.oauth.verifyAccess(token, '');
256
336
  }
257
- /**
258
- * Why a request was refused, on the server log only (the client gets the generic answer). Requests
259
- * that presented a token-shaped credential are `warn`: someone is trying to connect and failing.
260
- * Bare probes (scanners, a client's first unauthenticated step of OAuth discovery) are `debug`. The
261
- * reason is a quoted field, never part of the message: it can carry client-controlled text.
262
- */
337
+ /** Logs why a request was refused (the client gets the generic answer): `warn` for token-shaped
338
+ * credentials, `debug` for bare probes; the reason is a quoted field, never the message. */
263
339
  refused(c, slug, status, reason, fields = {}) {
264
340
  const ip = clientIp(c, this.ctx.config.TRUST_PROXY);
265
341
  const level = presentsCredential(c) ? 'warn' : 'debug';
@@ -395,7 +471,11 @@ export class McpEndpoints {
395
471
  await server.close().catch(() => undefined);
396
472
  }
397
473
  else {
398
- this.ctx.log.info('MCP session opened', { slug, client, ua: c.req.header('user-agent') });
474
+ // Which approval path this client gets (design §5.3): elicitation modes and the MCP Apps card.
475
+ const caps = server.server.getClientCapabilities();
476
+ const elicitation = caps?.elicitation ? (caps.elicitation.url ? 'url' : 'form') : 'none';
477
+ const apps = !!caps?.extensions?.[MCP_APPS_EXTENSION];
478
+ this.ctx.log.info('MCP session opened', { slug, client, ua: c.req.header('user-agent'), elicitation, apps });
399
479
  }
400
480
  return response;
401
481
  }