@suveren/gateway 0.4.0 → 0.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.
- package/bin/suveren-gateway.js +59 -0
- package/content/integrations/calendar.json +116 -22
- package/content/integrations/gmail.json +3 -0
- package/dist/control-plane/index.mjs +67 -7
- package/dist/mcp-server/http.mjs +98 -17
- package/dist/ui/assets/index-DedRWpBs.js +105 -0
- package/dist/ui/index.html +1 -1
- package/lib/autostart-templates.d.mts +37 -0
- package/lib/autostart-templates.mjs +208 -0
- package/node_modules/jose/README.md +13 -13
- package/node_modules/jose/dist/types/index.d.ts +6 -5
- package/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts +17 -11
- package/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts +7 -19
- package/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts +17 -11
- package/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts +7 -19
- package/node_modules/jose/dist/types/jwe/general/decrypt.d.ts +18 -13
- package/node_modules/jose/dist/types/jwe/general/encrypt.d.ts +20 -21
- package/node_modules/jose/dist/types/jwk/embedded.d.ts +8 -12
- package/node_modules/jose/dist/types/jwk/thumbprint.d.ts +0 -15
- package/node_modules/jose/dist/types/jwks/local.d.ts +12 -21
- package/node_modules/jose/dist/types/jwks/remote.d.ts +34 -165
- package/node_modules/jose/dist/types/jws/compact/sign.d.ts +2 -13
- package/node_modules/jose/dist/types/jws/compact/verify.d.ts +17 -13
- package/node_modules/jose/dist/types/jws/flattened/sign.d.ts +2 -13
- package/node_modules/jose/dist/types/jws/flattened/verify.d.ts +17 -13
- package/node_modules/jose/dist/types/jws/general/sign.d.ts +16 -17
- package/node_modules/jose/dist/types/jws/general/verify.d.ts +18 -15
- package/node_modules/jose/dist/types/jwt/decrypt.d.ts +17 -11
- package/node_modules/jose/dist/types/jwt/encrypt.d.ts +10 -34
- package/node_modules/jose/dist/types/jwt/sign.d.ts +2 -13
- package/node_modules/jose/dist/types/jwt/unsecured.d.ts +4 -13
- package/node_modules/jose/dist/types/jwt/verify.d.ts +14 -13
- package/node_modules/jose/dist/types/key/export.d.ts +0 -14
- package/node_modules/jose/dist/types/key/generate_key_pair.d.ts +7 -14
- package/node_modules/jose/dist/types/key/generate_secret.d.ts +16 -14
- package/node_modules/jose/dist/types/key/import.d.ts +18 -28
- package/node_modules/jose/dist/types/types.d.ts +242 -223
- package/node_modules/jose/dist/types/util/base64url.d.ts +11 -4
- package/node_modules/jose/dist/types/util/decode_jwt.d.ts +1 -9
- package/node_modules/jose/dist/types/util/decode_protected_header.d.ts +0 -8
- package/node_modules/jose/dist/types/util/errors.d.ts +128 -121
- package/node_modules/jose/dist/webapi/jwe/compact/decrypt.js +3 -21
- package/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +5 -153
- package/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +16 -111
- package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +15 -3
- package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +33 -48
- package/node_modules/jose/dist/webapi/jwk/embedded.js +5 -3
- package/node_modules/jose/dist/webapi/jwks/local.js +27 -47
- package/node_modules/jose/dist/webapi/jwks/remote.js +3 -3
- package/node_modules/jose/dist/webapi/jws/compact/sign.js +5 -2
- package/node_modules/jose/dist/webapi/jws/compact/verify.js +3 -15
- package/node_modules/jose/dist/webapi/jws/flattened/sign.js +7 -57
- package/node_modules/jose/dist/webapi/jws/flattened/verify.js +3 -89
- package/node_modules/jose/dist/webapi/jws/general/sign.js +18 -5
- package/node_modules/jose/dist/webapi/jws/general/verify.js +21 -3
- package/node_modules/jose/dist/webapi/jwt/decrypt.js +4 -4
- package/node_modules/jose/dist/webapi/jwt/sign.js +2 -3
- package/node_modules/jose/dist/webapi/jwt/unsecured.js +4 -3
- package/node_modules/jose/dist/webapi/jwt/verify.js +5 -5
- package/node_modules/jose/dist/webapi/key/export.js +34 -1
- package/node_modules/jose/dist/webapi/key/generate_key_pair.js +32 -82
- package/node_modules/jose/dist/webapi/key/import.js +8 -8
- package/node_modules/jose/dist/webapi/lib/asn1.js +29 -65
- package/node_modules/jose/dist/webapi/lib/buffer_utils.js +1 -0
- package/node_modules/jose/dist/webapi/lib/content_encryption.js +20 -77
- package/node_modules/jose/dist/webapi/lib/crypto_key.js +13 -127
- package/node_modules/jose/dist/webapi/lib/helpers.js +23 -0
- package/node_modules/jose/dist/webapi/lib/jwe_algorithms.js +143 -0
- package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +181 -0
- package/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +109 -0
- package/node_modules/jose/dist/webapi/lib/jwk_to_key.js +11 -96
- package/node_modules/jose/dist/webapi/lib/jws_algorithms.js +74 -0
- package/node_modules/jose/dist/webapi/lib/jws_sign.js +68 -0
- package/node_modules/jose/dist/webapi/lib/jws_verify.js +108 -0
- package/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +9 -8
- package/node_modules/jose/dist/webapi/lib/key.js +170 -0
- package/node_modules/jose/dist/webapi/lib/key_algorithm.js +16 -0
- package/node_modules/jose/dist/webapi/lib/key_descriptor.js +7 -0
- package/node_modules/jose/dist/webapi/lib/key_management.js +186 -29
- package/node_modules/jose/dist/webapi/lib/{validate_crit.js → options.js} +18 -0
- package/node_modules/jose/dist/webapi/lib/signing.js +20 -52
- package/node_modules/jose/dist/webapi/util/base64url.js +11 -3
- package/node_modules/jose/dist/webapi/util/decode_jwt.js +2 -2
- package/node_modules/jose/dist/webapi/util/decode_protected_header.js +5 -15
- package/node_modules/jose/dist/webapi/util/errors.js +1 -1
- package/node_modules/jose/package.json +2 -4
- package/package.json +2 -1
- package/profiles/email/0.4.profile.json +2 -1
- package/dist/ui/assets/index-BDQY7Z3H.js +0 -105
- package/node_modules/jose/dist/webapi/lib/aesgcmkw.js +0 -15
- package/node_modules/jose/dist/webapi/lib/aeskw.js +0 -25
- package/node_modules/jose/dist/webapi/lib/check_key_type.js +0 -122
- package/node_modules/jose/dist/webapi/lib/ecdhes.js +0 -52
- package/node_modules/jose/dist/webapi/lib/key_to_jwk.js +0 -34
- package/node_modules/jose/dist/webapi/lib/normalize_key.js +0 -166
- package/node_modules/jose/dist/webapi/lib/pbes2kw.js +0 -42
- package/node_modules/jose/dist/webapi/lib/rsaes.js +0 -24
- package/node_modules/jose/dist/webapi/lib/validate_algorithms.js +0 -10
package/bin/suveren-gateway.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { spawn, spawnSync } from 'node:child_process';
|
|
14
|
+
import { createConnection } from 'node:net';
|
|
14
15
|
import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'node:fs';
|
|
15
16
|
import { homedir, platform } from 'node:os';
|
|
16
17
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -60,6 +61,24 @@ async function start(args) {
|
|
|
60
61
|
process.exit(1);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
// The PID file only knows about instances THIS CLI started. A gateway
|
|
65
|
+
// started by the login service — or from another shell — leaves none, so the
|
|
66
|
+
// check above passes, we spawn, the child dies instantly with EADDRINUSE,
|
|
67
|
+
// and its error goes only to the log. The user gets "started (pid N)" for a
|
|
68
|
+
// process that is already dead. Ask the port, not just the file.
|
|
69
|
+
if (await isPortListening(SUVEREN_PORT)) {
|
|
70
|
+
console.error(`Port ${SUVEREN_PORT} is already in use.`);
|
|
71
|
+
console.error(``);
|
|
72
|
+
console.error(`Something is already serving there — most likely a gateway started by the`);
|
|
73
|
+
console.error(`login service or from another terminal, which leaves no PID file for this`);
|
|
74
|
+
console.error(`CLI to find.`);
|
|
75
|
+
console.error(``);
|
|
76
|
+
console.error(` Check what it is: suveren-gateway status`);
|
|
77
|
+
console.error(` Stop a login service: suveren-gateway service uninstall`);
|
|
78
|
+
console.error(` Or use another port: SUVEREN_CP_PORT=3410 suveren-gateway start`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
ensureDataDir();
|
|
64
83
|
|
|
65
84
|
if (detach) {
|
|
@@ -71,6 +90,24 @@ async function start(args) {
|
|
|
71
90
|
});
|
|
72
91
|
writeFileSync(PID_FILE, String(child.pid), 'utf8');
|
|
73
92
|
child.unref();
|
|
93
|
+
|
|
94
|
+
// "started" must mean SERVING. A detached child that dies on startup takes
|
|
95
|
+
// its error to the log, so reporting success on spawn alone hands the user
|
|
96
|
+
// a pid that no longer exists and a browser tab that will not connect.
|
|
97
|
+
const serving = await waitForListening(SUVEREN_PORT, 20_000);
|
|
98
|
+
if (!serving) {
|
|
99
|
+
safeUnlink(PID_FILE);
|
|
100
|
+
console.error(`suveren-gateway failed to start — nothing is listening on ${SUVEREN_PORT}.`);
|
|
101
|
+
console.error(``);
|
|
102
|
+
console.error(` Log: ${LOG_FILE}`);
|
|
103
|
+
console.error(` Last lines:`);
|
|
104
|
+
try {
|
|
105
|
+
const tail = readFileSync(LOG_FILE, 'utf8').trimEnd().split('\n').slice(-8);
|
|
106
|
+
for (const line of tail) console.error(` ${line}`);
|
|
107
|
+
} catch { /* no log yet */ }
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
74
111
|
console.log(`suveren-gateway started (pid ${child.pid})`);
|
|
75
112
|
console.log(``);
|
|
76
113
|
console.log(` → Open in your browser: http://localhost:${SUVEREN_PORT}`);
|
|
@@ -532,6 +569,28 @@ function serviceImplFor(os) {
|
|
|
532
569
|
}
|
|
533
570
|
}
|
|
534
571
|
|
|
572
|
+
/** Is something accepting TCP connections on this port? */
|
|
573
|
+
function isPortListening(port, timeoutMs = 1_000) {
|
|
574
|
+
return new Promise(resolve => {
|
|
575
|
+
const socket = createConnection({ host: '127.0.0.1', port: Number(port) });
|
|
576
|
+
const done = (result) => { socket.destroy(); resolve(result); };
|
|
577
|
+
socket.setTimeout(timeoutMs);
|
|
578
|
+
socket.once('connect', () => done(true));
|
|
579
|
+
socket.once('timeout', () => done(false));
|
|
580
|
+
socket.once('error', () => done(false));
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Poll until the port accepts connections, or give up. */
|
|
585
|
+
async function waitForListening(port, timeoutMs) {
|
|
586
|
+
const deadline = Date.now() + timeoutMs;
|
|
587
|
+
while (Date.now() < deadline) {
|
|
588
|
+
if (await isPortListening(port, 500)) return true;
|
|
589
|
+
await sleep(300);
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
535
594
|
/** Run a command capturing output, never throwing. */
|
|
536
595
|
function runQuiet(bin, args) {
|
|
537
596
|
const r = spawnSync(bin, args, { encoding: 'utf8' });
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
"description": "List, create, update, and delete events in Google Calendar",
|
|
6
6
|
"icon": "calendar",
|
|
7
7
|
"profile": "calendar",
|
|
8
|
+
"readPolicy": {
|
|
9
|
+
"defaultAgeDays": 30
|
|
10
|
+
},
|
|
8
11
|
"npmPackage": "@humanagencyp/google-calendar-mcp",
|
|
9
12
|
"mcp": {
|
|
10
13
|
"command": "google-calendar-mcp",
|
|
@@ -12,8 +15,18 @@
|
|
|
12
15
|
},
|
|
13
16
|
"credentials": {
|
|
14
17
|
"fields": [
|
|
15
|
-
{
|
|
16
|
-
|
|
18
|
+
{
|
|
19
|
+
"key": "clientId",
|
|
20
|
+
"label": "Client ID",
|
|
21
|
+
"type": "text",
|
|
22
|
+
"placeholder": "From Google Cloud Console"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"key": "clientSecret",
|
|
26
|
+
"label": "Client Secret",
|
|
27
|
+
"type": "password",
|
|
28
|
+
"placeholder": "OAuth2 client secret"
|
|
29
|
+
}
|
|
17
30
|
],
|
|
18
31
|
"envMapping": {
|
|
19
32
|
"GOOGLE_CLIENT_ID": "clientId",
|
|
@@ -30,9 +43,15 @@
|
|
|
30
43
|
"https://www.googleapis.com/auth/calendar.events",
|
|
31
44
|
"https://www.googleapis.com/auth/calendar.readonly"
|
|
32
45
|
],
|
|
33
|
-
"credentialKeys": {
|
|
46
|
+
"credentialKeys": {
|
|
47
|
+
"clientId": "clientId",
|
|
48
|
+
"clientSecret": "clientSecret"
|
|
49
|
+
},
|
|
34
50
|
"tokenStorage": "refreshToken",
|
|
35
|
-
"extraParams": {
|
|
51
|
+
"extraParams": {
|
|
52
|
+
"access_type": "offline",
|
|
53
|
+
"prompt": "consent"
|
|
54
|
+
}
|
|
36
55
|
},
|
|
37
56
|
"contextDiscovery": {
|
|
38
57
|
"allowed_calendars": {
|
|
@@ -53,52 +72,103 @@
|
|
|
53
72
|
"toolGating": {
|
|
54
73
|
"default": {
|
|
55
74
|
"executionMapping": {},
|
|
56
|
-
"staticExecution": {
|
|
75
|
+
"staticExecution": {
|
|
76
|
+
"duration": 0,
|
|
77
|
+
"lookahead": 0
|
|
78
|
+
}
|
|
57
79
|
},
|
|
58
80
|
"overrides": {
|
|
59
81
|
"list_calendars": {
|
|
60
82
|
"category": "read",
|
|
61
|
-
"read": {
|
|
83
|
+
"read": {
|
|
84
|
+
"resourceBound": "allowed_calendars",
|
|
85
|
+
"resultResourcePath": "id"
|
|
86
|
+
}
|
|
62
87
|
},
|
|
63
88
|
"list_events": {
|
|
64
89
|
"category": "read",
|
|
65
|
-
"read": {
|
|
90
|
+
"read": {
|
|
91
|
+
"resourceBound": "allowed_calendars",
|
|
92
|
+
"resourceArg": "calendarId",
|
|
93
|
+
"resourceDefault": "primary",
|
|
94
|
+
"ageField": "read_age_days",
|
|
95
|
+
"ageFloorArg": "timeMin",
|
|
96
|
+
"ageFloorFormat": "iso"
|
|
97
|
+
}
|
|
66
98
|
},
|
|
67
99
|
"get_event": {
|
|
68
100
|
"category": "read",
|
|
69
|
-
"read": {
|
|
101
|
+
"read": {
|
|
102
|
+
"resourceBound": "allowed_calendars",
|
|
103
|
+
"resourceArg": "calendarId",
|
|
104
|
+
"resourceDefault": "primary",
|
|
105
|
+
"ageField": "read_age_days",
|
|
106
|
+
"resultDatePath": [
|
|
107
|
+
"start.dateTime",
|
|
108
|
+
"start.date"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
70
111
|
},
|
|
71
112
|
"free_busy": {
|
|
72
113
|
"category": "read",
|
|
73
|
-
"read": {
|
|
114
|
+
"read": {
|
|
115
|
+
"resourceBound": "allowed_calendars",
|
|
116
|
+
"resourceArrayArg": "calendarIds",
|
|
117
|
+
"resourceDefault": "primary",
|
|
118
|
+
"ageField": "read_age_days",
|
|
119
|
+
"ageFloorArg": "timeMin",
|
|
120
|
+
"ageFloorFormat": "iso"
|
|
121
|
+
}
|
|
74
122
|
},
|
|
75
123
|
"create_event": {
|
|
76
124
|
"executionMapping": {
|
|
77
125
|
"duration_minutes": "duration",
|
|
78
126
|
"lookahead_days": "lookahead",
|
|
79
127
|
"attendees": [
|
|
80
|
-
{
|
|
81
|
-
|
|
128
|
+
{
|
|
129
|
+
"field": "allowed_attendees",
|
|
130
|
+
"transform": "join"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"field": "allowed_domains",
|
|
134
|
+
"transform": "join_domains"
|
|
135
|
+
}
|
|
82
136
|
],
|
|
83
137
|
"calendarId": "allowed_calendars"
|
|
84
138
|
},
|
|
85
|
-
"staticExecution": {
|
|
139
|
+
"staticExecution": {
|
|
140
|
+
"action_type": "booking"
|
|
141
|
+
}
|
|
86
142
|
},
|
|
87
143
|
"update_event": {
|
|
88
144
|
"executionMapping": {
|
|
89
145
|
"attendees": [
|
|
90
|
-
{
|
|
91
|
-
|
|
146
|
+
{
|
|
147
|
+
"field": "allowed_attendees",
|
|
148
|
+
"transform": "join"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"field": "allowed_domains",
|
|
152
|
+
"transform": "join_domains"
|
|
153
|
+
}
|
|
92
154
|
],
|
|
93
155
|
"calendarId": "allowed_calendars"
|
|
94
156
|
},
|
|
95
|
-
"staticExecution": {
|
|
157
|
+
"staticExecution": {
|
|
158
|
+
"action_type": "booking",
|
|
159
|
+
"duration": 0,
|
|
160
|
+
"lookahead": 0
|
|
161
|
+
}
|
|
96
162
|
},
|
|
97
163
|
"delete_event": {
|
|
98
164
|
"executionMapping": {
|
|
99
165
|
"calendarId": "allowed_calendars"
|
|
100
166
|
},
|
|
101
|
-
"staticExecution": {
|
|
167
|
+
"staticExecution": {
|
|
168
|
+
"action_type": "delete",
|
|
169
|
+
"duration": 0,
|
|
170
|
+
"lookahead": 0
|
|
171
|
+
}
|
|
102
172
|
}
|
|
103
173
|
}
|
|
104
174
|
},
|
|
@@ -108,22 +178,46 @@
|
|
|
108
178
|
"description": "List and read events, check free/busy. No create, update, or delete.",
|
|
109
179
|
"risk": "low",
|
|
110
180
|
"mode": "automatic",
|
|
111
|
-
"bounds": {
|
|
112
|
-
|
|
181
|
+
"bounds": {
|
|
182
|
+
"booking_daily_max": "0",
|
|
183
|
+
"booking_duration_max": "0",
|
|
184
|
+
"lookahead_days_max": "0"
|
|
185
|
+
},
|
|
186
|
+
"context": {
|
|
187
|
+
"allowed_calendars": "primary",
|
|
188
|
+
"allowed_attendees": "",
|
|
189
|
+
"allowed_domains": ""
|
|
190
|
+
},
|
|
113
191
|
"intent": "Allow the agent to read the primary calendar (only) to help with scheduling suggestions. No create or modify. To read other calendars, add them to allowed_calendars.",
|
|
114
192
|
"ttl": 2592000,
|
|
115
|
-
"tags": [
|
|
193
|
+
"tags": [
|
|
194
|
+
"read-only",
|
|
195
|
+
"30 days"
|
|
196
|
+
]
|
|
116
197
|
},
|
|
117
198
|
{
|
|
118
199
|
"name": "Book with Review",
|
|
119
200
|
"description": "Read events and propose new bookings. Every create/update/delete requires your approval.",
|
|
120
201
|
"risk": "high",
|
|
121
202
|
"mode": "review",
|
|
122
|
-
"bounds": {
|
|
123
|
-
|
|
203
|
+
"bounds": {
|
|
204
|
+
"booking_daily_max": "3",
|
|
205
|
+
"booking_duration_max": "120",
|
|
206
|
+
"lookahead_days_max": "30"
|
|
207
|
+
},
|
|
208
|
+
"context": {
|
|
209
|
+
"allowed_calendars": "primary",
|
|
210
|
+
"allowed_attendees": "",
|
|
211
|
+
"allowed_domains": ""
|
|
212
|
+
},
|
|
124
213
|
"intent": "Read my calendar and propose bookings. Every create, update, or delete requires my review — I see the title, time, duration, and attendees before it goes through.",
|
|
125
214
|
"ttl": 2592000,
|
|
126
|
-
"tags": [
|
|
215
|
+
"tags": [
|
|
216
|
+
"3 bookings/day",
|
|
217
|
+
"2h max",
|
|
218
|
+
"30d lookahead",
|
|
219
|
+
"review mode"
|
|
220
|
+
]
|
|
127
221
|
}
|
|
128
222
|
],
|
|
129
223
|
"setupHint": "Create OAuth credentials at Google Cloud Console and enable the Google Calendar API.",
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
// src/index.ts
|
|
7
7
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
8
8
|
import { existsSync as existsSync5, readFileSync as readFileSync4, renameSync } from "fs";
|
|
9
|
-
import { join as join4 } from "path";
|
|
9
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
10
10
|
import { homedir as homedir3 } from "os";
|
|
11
11
|
import express from "express";
|
|
12
12
|
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
@@ -356,6 +356,7 @@ async function activateIntegration(manifest) {
|
|
|
356
356
|
profile: manifest.profile,
|
|
357
357
|
toolGating: manifest.toolGating,
|
|
358
358
|
npmPackage: manifest.npmPackage,
|
|
359
|
+
...typeof manifest.readPolicy?.defaultAgeDays === "number" ? { readAgeDays: manifest.readPolicy.defaultAgeDays } : {},
|
|
359
360
|
enabled: true
|
|
360
361
|
});
|
|
361
362
|
}
|
|
@@ -391,6 +392,18 @@ async function removeIntegration(id) {
|
|
|
391
392
|
}
|
|
392
393
|
return res.json();
|
|
393
394
|
}
|
|
395
|
+
async function setReadPolicy(id, readAgeDays) {
|
|
396
|
+
const res = await fetch(`${MCP_BASE}/internal/integration/${encodeURIComponent(id)}/read-policy`, {
|
|
397
|
+
method: "PATCH",
|
|
398
|
+
headers: { ...internalHeaders(), "Content-Type": "application/json" },
|
|
399
|
+
body: JSON.stringify({ readAgeDays })
|
|
400
|
+
});
|
|
401
|
+
if (!res.ok) {
|
|
402
|
+
const err = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
403
|
+
throw new Error(err.error);
|
|
404
|
+
}
|
|
405
|
+
return res.json();
|
|
406
|
+
}
|
|
394
407
|
async function getGateContent(path) {
|
|
395
408
|
const qs = path ? `?path=${encodeURIComponent(path)}` : "";
|
|
396
409
|
const res = await fetch(`${MCP_BASE}/internal/gate-content${qs}`, {
|
|
@@ -1364,6 +1377,30 @@ function requireAuthQueryOrHeader(vault2) {
|
|
|
1364
1377
|
};
|
|
1365
1378
|
}
|
|
1366
1379
|
|
|
1380
|
+
// src/lib/read-policy-defaults.ts
|
|
1381
|
+
async function backfillReadPolicyDefaults(data) {
|
|
1382
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
1383
|
+
for (const m of data.manifests ?? []) {
|
|
1384
|
+
const days = m.readPolicy?.defaultAgeDays;
|
|
1385
|
+
if (typeof m.id === "string" && typeof days === "number" && Number.isInteger(days) && days >= 0) {
|
|
1386
|
+
defaults.set(m.id, days);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (defaults.size === 0) return;
|
|
1390
|
+
try {
|
|
1391
|
+
const current = await getIntegrations();
|
|
1392
|
+
for (const integration of current.integrations ?? []) {
|
|
1393
|
+
const fallback = defaults.get(integration.id);
|
|
1394
|
+
if (fallback === void 0) continue;
|
|
1395
|
+
if (integration.readAgeDays != null) continue;
|
|
1396
|
+
await setReadPolicy(integration.id, fallback);
|
|
1397
|
+
console.error(`[Control Plane] Read policy default applied: ${integration.id} \u2192 ${fallback}d`);
|
|
1398
|
+
}
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
console.error(`[Control Plane] \u26A0 Could not apply read-policy defaults: ${err instanceof Error ? err.message : err}`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1367
1404
|
// src/routes/mcp.ts
|
|
1368
1405
|
import { Router as Router5 } from "express";
|
|
1369
1406
|
|
|
@@ -1471,15 +1508,15 @@ import { promisify } from "util";
|
|
|
1471
1508
|
var run = promisify(execFile);
|
|
1472
1509
|
var SUPPORTED = /* @__PURE__ */ new Set(["darwin", "win32", "linux"]);
|
|
1473
1510
|
var MODULE_DIR = pathDirname(fileURLToPath(import.meta.url));
|
|
1474
|
-
function findCli(
|
|
1511
|
+
function findCli(dirname4 = MODULE_DIR) {
|
|
1475
1512
|
const candidates = [
|
|
1476
1513
|
// Bundled (the shipped layout): dist/control-plane → dist → root → bin
|
|
1477
|
-
resolve(
|
|
1514
|
+
resolve(dirname4, "..", "..", "bin", "suveren-gateway.js"),
|
|
1478
1515
|
// Bundled, were the routes/ directory ever preserved.
|
|
1479
|
-
resolve(
|
|
1516
|
+
resolve(dirname4, "..", "..", "..", "bin", "suveren-gateway.js"),
|
|
1480
1517
|
// Dev, running from source: the repo's own bundle directory.
|
|
1481
|
-
resolve(
|
|
1482
|
-
resolve(
|
|
1518
|
+
resolve(dirname4, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
|
|
1519
|
+
resolve(dirname4, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
|
|
1483
1520
|
// Deliberately NO process.cwd() fallback: it makes the result depend on
|
|
1484
1521
|
// where the process happened to be launched from, so the same install
|
|
1485
1522
|
// resolves differently between a shell and a login service — and it would
|
|
@@ -2195,6 +2232,21 @@ app.use("/autostart", jsonParser, authGuard, createAutostartRouter());
|
|
|
2195
2232
|
app.use("/api/encrypt-intent", jsonParser, authGuard, createEncryptIntentRouter());
|
|
2196
2233
|
app.use("/api/decrypt-intent", jsonParser, authGuard, createDecryptIntentRouter(vault));
|
|
2197
2234
|
app.use("/api/approved-intents", jsonParser, authGuard, createApprovedIntentsRouter(vault));
|
|
2235
|
+
app.put("/integrations/:id/read-policy", jsonParser, authGuard, async (req, res) => {
|
|
2236
|
+
const id = req.params.id;
|
|
2237
|
+
const { readAgeDays } = req.body ?? {};
|
|
2238
|
+
if (readAgeDays !== null && !(typeof readAgeDays === "number" && Number.isInteger(readAgeDays) && readAgeDays >= 0)) {
|
|
2239
|
+
res.status(400).json({ error: "readAgeDays must be a non-negative integer, or null to clear it" });
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
try {
|
|
2243
|
+
const result = await setReadPolicy(id, readAgeDays);
|
|
2244
|
+
res.json(result);
|
|
2245
|
+
} catch (err) {
|
|
2246
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2247
|
+
res.status(502).json({ error: `Could not set read policy: ${message}` });
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
2198
2250
|
app.get("/integrations/:id/discover/:field", authGuard, async (req, res) => {
|
|
2199
2251
|
const integrationId = req.params.id;
|
|
2200
2252
|
const fieldName = req.params.field;
|
|
@@ -2425,7 +2477,14 @@ function detectInstallMethod() {
|
|
|
2425
2477
|
if (existsSync5("/.dockerenv")) return "docker";
|
|
2426
2478
|
const dir = import.meta.dirname ?? __dirname;
|
|
2427
2479
|
if (dir.includes("/node_modules/@suveren/gateway/")) return "npm";
|
|
2428
|
-
|
|
2480
|
+
let cursor = dir;
|
|
2481
|
+
for (let i = 0; i < 8; i++) {
|
|
2482
|
+
if (existsSync5(join4(cursor, ".git"))) return "dev";
|
|
2483
|
+
const parent = dirname3(cursor);
|
|
2484
|
+
if (parent === cursor) break;
|
|
2485
|
+
cursor = parent;
|
|
2486
|
+
}
|
|
2487
|
+
return "npm";
|
|
2429
2488
|
}
|
|
2430
2489
|
var INSTALL_METHOD = detectInstallMethod();
|
|
2431
2490
|
function detectRunningVersion() {
|
|
@@ -2500,6 +2559,7 @@ app.listen(port, "0.0.0.0", () => {
|
|
|
2500
2559
|
console.error(`[Control Plane] \u26A0 MCP server at ${MCP_BASE} returned 0 integration manifests \u2014 wrong SUVEREN_MCP_INTERNAL_URL? (dev MCP is :3431, npm is :3430)`);
|
|
2501
2560
|
} else {
|
|
2502
2561
|
console.error(`[Control Plane] Integrations: ${n} manifests loaded from ${MCP_BASE}`);
|
|
2562
|
+
void backfillReadPolicyDefaults(d);
|
|
2503
2563
|
}
|
|
2504
2564
|
}).catch((err) => {
|
|
2505
2565
|
console.error(`[Control Plane] \u26A0 Could not reach MCP server at ${MCP_BASE} for manifests \u2014 wrong SUVEREN_MCP_INTERNAL_URL? (dev=:3431, npm=:3430). ${err instanceof Error ? err.message : err}`);
|
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -1332,6 +1332,29 @@ function getByDottedPath(obj, path) {
|
|
|
1332
1332
|
obj
|
|
1333
1333
|
);
|
|
1334
1334
|
}
|
|
1335
|
+
function firstParsableDate(obj, paths) {
|
|
1336
|
+
for (const path of Array.isArray(paths) ? paths : [paths]) {
|
|
1337
|
+
const parsed = parseMessageTimestamp(getByDottedPath(obj, path));
|
|
1338
|
+
if (parsed !== null) return parsed;
|
|
1339
|
+
}
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
function renderAgeConstraint(template, maxAgeDays, nowMs) {
|
|
1343
|
+
const floorIso = new Date(nowMs - maxAgeDays * MS_PER_DAY).toISOString();
|
|
1344
|
+
return template.replace(/\{days\}/g, String(maxAgeDays)).replace(/\{date\}/g, floorIso.slice(0, 10));
|
|
1345
|
+
}
|
|
1346
|
+
function ageFloorValue(maxAgeDays, nowMs, format = "iso") {
|
|
1347
|
+
const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
|
|
1348
|
+
if (format === "epoch_ms") return floorMs;
|
|
1349
|
+
if (format === "epoch_s") return String(Math.floor(floorMs / 1e3));
|
|
1350
|
+
return new Date(floorMs).toISOString();
|
|
1351
|
+
}
|
|
1352
|
+
function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
|
|
1353
|
+
const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
|
|
1354
|
+
const asked = parseMessageTimestamp(requested);
|
|
1355
|
+
if (asked !== null && asked >= floorMs) return requested;
|
|
1356
|
+
return ageFloorValue(maxAgeDays, nowMs, format);
|
|
1357
|
+
}
|
|
1335
1358
|
|
|
1336
1359
|
// src/lib/tool-proxy.ts
|
|
1337
1360
|
import { getProfile as getProfile3 } from "@hap/core";
|
|
@@ -1494,18 +1517,20 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
|
|
|
1494
1517
|
const readProfile = readAdapter ? getProfile3(matchingAuths[0].profileId) : void 0;
|
|
1495
1518
|
const boundsSchema = readProfile?.boundsSchema;
|
|
1496
1519
|
const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
|
|
1520
|
+
const hasAgeDimension = Boolean(readAdapter?.ageField);
|
|
1497
1521
|
let readMaxAge = null;
|
|
1498
|
-
if (
|
|
1499
|
-
|
|
1522
|
+
if (hasAgeDimension) {
|
|
1523
|
+
const localAge = integrationManager2.getReadAgeDays(tool.integrationId);
|
|
1524
|
+
readMaxAge = localAge ?? (ageBoundField ? maxReadAgeDays(
|
|
1500
1525
|
matchingAuths.map((a) => a.bounds ?? a.frame),
|
|
1501
1526
|
ageBoundField
|
|
1502
|
-
);
|
|
1527
|
+
) : null);
|
|
1503
1528
|
if (readMaxAge === null) {
|
|
1504
1529
|
return denyRead(
|
|
1505
1530
|
state2,
|
|
1506
1531
|
tool,
|
|
1507
1532
|
"unset_age",
|
|
1508
|
-
`no read-age window is set on your authorization
|
|
1533
|
+
`no read-age window is set for this integration${ageBoundField ? ` (or on your authorization's ${ageBoundField})` : ""}. Reads are not permitted with an unbounded window \u2014 set how far back the agent may read under the integration's Read policy, then try again. Nothing was read.`
|
|
1509
1534
|
);
|
|
1510
1535
|
}
|
|
1511
1536
|
}
|
|
@@ -1540,10 +1565,21 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
|
|
|
1540
1565
|
if (readAdapter?.pinnedArgs) {
|
|
1541
1566
|
outgoing = { ...outgoing, ...readAdapter.pinnedArgs };
|
|
1542
1567
|
}
|
|
1568
|
+
if (readAdapter?.ageFloorArg && hasAgeDimension && readMaxAge !== null) {
|
|
1569
|
+
outgoing = {
|
|
1570
|
+
...outgoing,
|
|
1571
|
+
[readAdapter.ageFloorArg]: clampAgeFloor(
|
|
1572
|
+
outgoing[readAdapter.ageFloorArg],
|
|
1573
|
+
readMaxAge,
|
|
1574
|
+
Date.now(),
|
|
1575
|
+
readAdapter.ageFloorFormat
|
|
1576
|
+
)
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1543
1579
|
if (readAdapter?.queryArg) {
|
|
1544
1580
|
const clauses = [];
|
|
1545
|
-
if (
|
|
1546
|
-
clauses.push(readAdapter.ageConstraint
|
|
1581
|
+
if (hasAgeDimension && readAdapter.ageConstraint && readMaxAge !== null) {
|
|
1582
|
+
clauses.push(renderAgeConstraint(readAdapter.ageConstraint, readMaxAge, Date.now()));
|
|
1547
1583
|
}
|
|
1548
1584
|
if (clauses.length > 0) {
|
|
1549
1585
|
const base = typeof args[readAdapter.queryArg] === "string" ? args[readAdapter.queryArg] : "";
|
|
@@ -1581,10 +1617,10 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
|
|
|
1581
1617
|
);
|
|
1582
1618
|
}
|
|
1583
1619
|
}
|
|
1584
|
-
if (readAdapter &&
|
|
1620
|
+
if (readAdapter && hasAgeDimension && readAdapter.resultDatePath && readMaxAge !== null) {
|
|
1585
1621
|
const parsed = parseFirstJson(result);
|
|
1586
|
-
const
|
|
1587
|
-
if (isOlderThanMaxAge(
|
|
1622
|
+
const itemDate = parsed === void 0 ? null : firstParsableDate(parsed, readAdapter.resultDatePath);
|
|
1623
|
+
if (isOlderThanMaxAge(itemDate, readMaxAge, Date.now())) {
|
|
1588
1624
|
return denyRead(
|
|
1589
1625
|
state2,
|
|
1590
1626
|
tool,
|
|
@@ -2571,6 +2607,10 @@ function ensureIntegrationsDir() {
|
|
|
2571
2607
|
writeFileSync5(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
|
|
2572
2608
|
}
|
|
2573
2609
|
}
|
|
2610
|
+
function readAgeOf(config) {
|
|
2611
|
+
const days = config.readAgeDays;
|
|
2612
|
+
return typeof days === "number" && Number.isFinite(days) && days >= 0 ? days : null;
|
|
2613
|
+
}
|
|
2574
2614
|
var RESPAWN_DELAYS = [2e3, 4e3, 6e3];
|
|
2575
2615
|
var MAX_RESPAWN_ATTEMPTS = 3;
|
|
2576
2616
|
var CONNECT_TIMEOUT_MS = 3e4;
|
|
@@ -2838,14 +2878,32 @@ var IntegrationManager = class {
|
|
|
2838
2878
|
return tools;
|
|
2839
2879
|
}
|
|
2840
2880
|
/**
|
|
2841
|
-
*
|
|
2842
|
-
*
|
|
2843
|
-
*
|
|
2844
|
-
*
|
|
2881
|
+
* The LOCAL read-age window (days) for a running integration, or null when
|
|
2882
|
+
* none is set here (the read path then falls back to the signed grant bound
|
|
2883
|
+
* — see `IntegrationConfig.readAgeDays`).
|
|
2884
|
+
*
|
|
2885
|
+
* 0 is a real value ("read nothing") and MUST survive this call, so the
|
|
2886
|
+
* guard tests the type, never truthiness.
|
|
2845
2887
|
*/
|
|
2846
2888
|
getReadAgeDays(integrationId) {
|
|
2847
|
-
const
|
|
2848
|
-
return
|
|
2889
|
+
const config = this.running.get(integrationId)?.config;
|
|
2890
|
+
return config ? readAgeOf(config) : null;
|
|
2891
|
+
}
|
|
2892
|
+
/**
|
|
2893
|
+
* Apply a new local read-age window to the RUNNING snapshot, so the change
|
|
2894
|
+
* takes effect on the next read without restarting the subprocess. Read
|
|
2895
|
+
* policy is enforced gateway-side and never reaches the downstream MCP
|
|
2896
|
+
* server, so nothing has to be re-spawned.
|
|
2897
|
+
*
|
|
2898
|
+
* Persistence is the registry's job — callers update both. Returns false if
|
|
2899
|
+
* the integration is not running (registry-only update still applies).
|
|
2900
|
+
*/
|
|
2901
|
+
setReadAgeDays(integrationId, days) {
|
|
2902
|
+
const entry = this.running.get(integrationId);
|
|
2903
|
+
if (!entry) return false;
|
|
2904
|
+
if (days === null) delete entry.config.readAgeDays;
|
|
2905
|
+
else entry.config.readAgeDays = days;
|
|
2906
|
+
return true;
|
|
2849
2907
|
}
|
|
2850
2908
|
/**
|
|
2851
2909
|
* Get status info for all known integrations.
|
|
@@ -2857,7 +2915,8 @@ var IntegrationManager = class {
|
|
|
2857
2915
|
id: entry.config.id,
|
|
2858
2916
|
name: entry.config.name,
|
|
2859
2917
|
running: true,
|
|
2860
|
-
toolCount: entry.tools.length
|
|
2918
|
+
toolCount: entry.tools.length,
|
|
2919
|
+
readAgeDays: readAgeOf(entry.config)
|
|
2861
2920
|
});
|
|
2862
2921
|
}
|
|
2863
2922
|
if (allConfigs) {
|
|
@@ -2867,7 +2926,8 @@ var IntegrationManager = class {
|
|
|
2867
2926
|
id: config.id,
|
|
2868
2927
|
name: config.name,
|
|
2869
2928
|
running: false,
|
|
2870
|
-
toolCount: 0
|
|
2929
|
+
toolCount: 0,
|
|
2930
|
+
readAgeDays: readAgeOf(config)
|
|
2871
2931
|
});
|
|
2872
2932
|
}
|
|
2873
2933
|
}
|
|
@@ -3325,6 +3385,27 @@ app.delete("/internal/remove-integration/:id", internalOnly, async (req, res) =>
|
|
|
3325
3385
|
console.error(`[Suveren MCP] Integration ${id} removed`);
|
|
3326
3386
|
res.json({ ok: true, id });
|
|
3327
3387
|
});
|
|
3388
|
+
app.patch("/internal/integration/:id/read-policy", internalOnly, (req, res) => {
|
|
3389
|
+
const id = req.params.id;
|
|
3390
|
+
const raw = req.body.readAgeDays;
|
|
3391
|
+
let readAgeDays;
|
|
3392
|
+
if (raw === null) {
|
|
3393
|
+
readAgeDays = null;
|
|
3394
|
+
} else if (typeof raw === "number" && Number.isInteger(raw) && raw >= 0) {
|
|
3395
|
+
readAgeDays = raw;
|
|
3396
|
+
} else {
|
|
3397
|
+
res.status(400).json({ error: "readAgeDays must be a non-negative integer, or null to clear it" });
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
const updated = integrationRegistry.update(id, { readAgeDays: readAgeDays ?? void 0 });
|
|
3401
|
+
if (!updated) {
|
|
3402
|
+
res.status(404).json({ error: `Integration "${id}" not found` });
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
integrationManager.setReadAgeDays(id, readAgeDays);
|
|
3406
|
+
console.error(`[Suveren MCP] Read policy for ${id}: ${readAgeDays === null ? "cleared" : `${readAgeDays}d`}`);
|
|
3407
|
+
res.json({ ok: true, id, readAgeDays });
|
|
3408
|
+
});
|
|
3328
3409
|
app.get("/internal/integrations", internalOnly, (_req, res) => {
|
|
3329
3410
|
const configs = integrationRegistry.getAll();
|
|
3330
3411
|
const statuses = integrationManager.getStatus(configs);
|