@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.
Files changed (98) hide show
  1. package/bin/suveren-gateway.js +59 -0
  2. package/content/integrations/calendar.json +116 -22
  3. package/content/integrations/gmail.json +3 -0
  4. package/dist/control-plane/index.mjs +67 -7
  5. package/dist/mcp-server/http.mjs +98 -17
  6. package/dist/ui/assets/index-DedRWpBs.js +105 -0
  7. package/dist/ui/index.html +1 -1
  8. package/lib/autostart-templates.d.mts +37 -0
  9. package/lib/autostart-templates.mjs +208 -0
  10. package/node_modules/jose/README.md +13 -13
  11. package/node_modules/jose/dist/types/index.d.ts +6 -5
  12. package/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts +17 -11
  13. package/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts +7 -19
  14. package/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts +17 -11
  15. package/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts +7 -19
  16. package/node_modules/jose/dist/types/jwe/general/decrypt.d.ts +18 -13
  17. package/node_modules/jose/dist/types/jwe/general/encrypt.d.ts +20 -21
  18. package/node_modules/jose/dist/types/jwk/embedded.d.ts +8 -12
  19. package/node_modules/jose/dist/types/jwk/thumbprint.d.ts +0 -15
  20. package/node_modules/jose/dist/types/jwks/local.d.ts +12 -21
  21. package/node_modules/jose/dist/types/jwks/remote.d.ts +34 -165
  22. package/node_modules/jose/dist/types/jws/compact/sign.d.ts +2 -13
  23. package/node_modules/jose/dist/types/jws/compact/verify.d.ts +17 -13
  24. package/node_modules/jose/dist/types/jws/flattened/sign.d.ts +2 -13
  25. package/node_modules/jose/dist/types/jws/flattened/verify.d.ts +17 -13
  26. package/node_modules/jose/dist/types/jws/general/sign.d.ts +16 -17
  27. package/node_modules/jose/dist/types/jws/general/verify.d.ts +18 -15
  28. package/node_modules/jose/dist/types/jwt/decrypt.d.ts +17 -11
  29. package/node_modules/jose/dist/types/jwt/encrypt.d.ts +10 -34
  30. package/node_modules/jose/dist/types/jwt/sign.d.ts +2 -13
  31. package/node_modules/jose/dist/types/jwt/unsecured.d.ts +4 -13
  32. package/node_modules/jose/dist/types/jwt/verify.d.ts +14 -13
  33. package/node_modules/jose/dist/types/key/export.d.ts +0 -14
  34. package/node_modules/jose/dist/types/key/generate_key_pair.d.ts +7 -14
  35. package/node_modules/jose/dist/types/key/generate_secret.d.ts +16 -14
  36. package/node_modules/jose/dist/types/key/import.d.ts +18 -28
  37. package/node_modules/jose/dist/types/types.d.ts +242 -223
  38. package/node_modules/jose/dist/types/util/base64url.d.ts +11 -4
  39. package/node_modules/jose/dist/types/util/decode_jwt.d.ts +1 -9
  40. package/node_modules/jose/dist/types/util/decode_protected_header.d.ts +0 -8
  41. package/node_modules/jose/dist/types/util/errors.d.ts +128 -121
  42. package/node_modules/jose/dist/webapi/jwe/compact/decrypt.js +3 -21
  43. package/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +5 -153
  44. package/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +16 -111
  45. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +15 -3
  46. package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +33 -48
  47. package/node_modules/jose/dist/webapi/jwk/embedded.js +5 -3
  48. package/node_modules/jose/dist/webapi/jwks/local.js +27 -47
  49. package/node_modules/jose/dist/webapi/jwks/remote.js +3 -3
  50. package/node_modules/jose/dist/webapi/jws/compact/sign.js +5 -2
  51. package/node_modules/jose/dist/webapi/jws/compact/verify.js +3 -15
  52. package/node_modules/jose/dist/webapi/jws/flattened/sign.js +7 -57
  53. package/node_modules/jose/dist/webapi/jws/flattened/verify.js +3 -89
  54. package/node_modules/jose/dist/webapi/jws/general/sign.js +18 -5
  55. package/node_modules/jose/dist/webapi/jws/general/verify.js +21 -3
  56. package/node_modules/jose/dist/webapi/jwt/decrypt.js +4 -4
  57. package/node_modules/jose/dist/webapi/jwt/sign.js +2 -3
  58. package/node_modules/jose/dist/webapi/jwt/unsecured.js +4 -3
  59. package/node_modules/jose/dist/webapi/jwt/verify.js +5 -5
  60. package/node_modules/jose/dist/webapi/key/export.js +34 -1
  61. package/node_modules/jose/dist/webapi/key/generate_key_pair.js +32 -82
  62. package/node_modules/jose/dist/webapi/key/import.js +8 -8
  63. package/node_modules/jose/dist/webapi/lib/asn1.js +29 -65
  64. package/node_modules/jose/dist/webapi/lib/buffer_utils.js +1 -0
  65. package/node_modules/jose/dist/webapi/lib/content_encryption.js +20 -77
  66. package/node_modules/jose/dist/webapi/lib/crypto_key.js +13 -127
  67. package/node_modules/jose/dist/webapi/lib/helpers.js +23 -0
  68. package/node_modules/jose/dist/webapi/lib/jwe_algorithms.js +143 -0
  69. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +181 -0
  70. package/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +109 -0
  71. package/node_modules/jose/dist/webapi/lib/jwk_to_key.js +11 -96
  72. package/node_modules/jose/dist/webapi/lib/jws_algorithms.js +74 -0
  73. package/node_modules/jose/dist/webapi/lib/jws_sign.js +68 -0
  74. package/node_modules/jose/dist/webapi/lib/jws_verify.js +108 -0
  75. package/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +9 -8
  76. package/node_modules/jose/dist/webapi/lib/key.js +170 -0
  77. package/node_modules/jose/dist/webapi/lib/key_algorithm.js +16 -0
  78. package/node_modules/jose/dist/webapi/lib/key_descriptor.js +7 -0
  79. package/node_modules/jose/dist/webapi/lib/key_management.js +186 -29
  80. package/node_modules/jose/dist/webapi/lib/{validate_crit.js → options.js} +18 -0
  81. package/node_modules/jose/dist/webapi/lib/signing.js +20 -52
  82. package/node_modules/jose/dist/webapi/util/base64url.js +11 -3
  83. package/node_modules/jose/dist/webapi/util/decode_jwt.js +2 -2
  84. package/node_modules/jose/dist/webapi/util/decode_protected_header.js +5 -15
  85. package/node_modules/jose/dist/webapi/util/errors.js +1 -1
  86. package/node_modules/jose/package.json +2 -4
  87. package/package.json +2 -1
  88. package/profiles/email/0.4.profile.json +2 -1
  89. package/dist/ui/assets/index-BDQY7Z3H.js +0 -105
  90. package/node_modules/jose/dist/webapi/lib/aesgcmkw.js +0 -15
  91. package/node_modules/jose/dist/webapi/lib/aeskw.js +0 -25
  92. package/node_modules/jose/dist/webapi/lib/check_key_type.js +0 -122
  93. package/node_modules/jose/dist/webapi/lib/ecdhes.js +0 -52
  94. package/node_modules/jose/dist/webapi/lib/key_to_jwk.js +0 -34
  95. package/node_modules/jose/dist/webapi/lib/normalize_key.js +0 -166
  96. package/node_modules/jose/dist/webapi/lib/pbes2kw.js +0 -42
  97. package/node_modules/jose/dist/webapi/lib/rsaes.js +0 -24
  98. package/node_modules/jose/dist/webapi/lib/validate_algorithms.js +0 -10
@@ -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
- { "key": "clientId", "label": "Client ID", "type": "text", "placeholder": "From Google Cloud Console" },
16
- { "key": "clientSecret", "label": "Client Secret", "type": "password", "placeholder": "OAuth2 client secret" }
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": { "clientId": "clientId", "clientSecret": "clientSecret" },
46
+ "credentialKeys": {
47
+ "clientId": "clientId",
48
+ "clientSecret": "clientSecret"
49
+ },
34
50
  "tokenStorage": "refreshToken",
35
- "extraParams": { "access_type": "offline", "prompt": "consent" }
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": { "duration": 0, "lookahead": 0 }
75
+ "staticExecution": {
76
+ "duration": 0,
77
+ "lookahead": 0
78
+ }
57
79
  },
58
80
  "overrides": {
59
81
  "list_calendars": {
60
82
  "category": "read",
61
- "read": { "resourceBound": "allowed_calendars", "resultResourcePath": "id" }
83
+ "read": {
84
+ "resourceBound": "allowed_calendars",
85
+ "resultResourcePath": "id"
86
+ }
62
87
  },
63
88
  "list_events": {
64
89
  "category": "read",
65
- "read": { "resourceBound": "allowed_calendars", "resourceArg": "calendarId", "resourceDefault": "primary" }
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": { "resourceBound": "allowed_calendars", "resourceArg": "calendarId", "resourceDefault": "primary" }
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": { "resourceBound": "allowed_calendars", "resourceArrayArg": "calendarIds", "resourceDefault": "primary" }
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
- { "field": "allowed_attendees", "transform": "join" },
81
- { "field": "allowed_domains", "transform": "join_domains" }
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": { "action_type": "booking" }
139
+ "staticExecution": {
140
+ "action_type": "booking"
141
+ }
86
142
  },
87
143
  "update_event": {
88
144
  "executionMapping": {
89
145
  "attendees": [
90
- { "field": "allowed_attendees", "transform": "join" },
91
- { "field": "allowed_domains", "transform": "join_domains" }
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": { "action_type": "booking", "duration": 0, "lookahead": 0 }
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": { "action_type": "delete", "duration": 0, "lookahead": 0 }
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": { "booking_daily_max": "0", "booking_duration_max": "0", "lookahead_days_max": "0" },
112
- "context": { "allowed_calendars": "primary", "allowed_attendees": "", "allowed_domains": "" },
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": ["read-only", "30 days"]
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": { "booking_daily_max": "3", "booking_duration_max": "120", "lookahead_days_max": "30" },
123
- "context": { "allowed_calendars": "primary", "allowed_attendees": "", "allowed_domains": "" },
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": ["3 bookings/day", "2h max", "30d lookahead", "review mode"]
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,6 +6,9 @@
6
6
  "icon": "mail",
7
7
  "profile": "email",
8
8
  "npmPackage": "@shinzolabs/gmail-mcp",
9
+ "readPolicy": {
10
+ "defaultAgeDays": 30
11
+ },
9
12
  "mcp": {
10
13
  "command": "gmail-mcp",
11
14
  "args": [],
@@ -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(dirname3 = MODULE_DIR) {
1511
+ function findCli(dirname4 = MODULE_DIR) {
1475
1512
  const candidates = [
1476
1513
  // Bundled (the shipped layout): dist/control-plane → dist → root → bin
1477
- resolve(dirname3, "..", "..", "bin", "suveren-gateway.js"),
1514
+ resolve(dirname4, "..", "..", "bin", "suveren-gateway.js"),
1478
1515
  // Bundled, were the routes/ directory ever preserved.
1479
- resolve(dirname3, "..", "..", "..", "bin", "suveren-gateway.js"),
1516
+ resolve(dirname4, "..", "..", "..", "bin", "suveren-gateway.js"),
1480
1517
  // Dev, running from source: the repo's own bundle directory.
1481
- resolve(dirname3, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
1482
- resolve(dirname3, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
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
- return "dev";
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}`);
@@ -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 (ageBoundField) {
1499
- readMaxAge = maxReadAgeDays(
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 (${ageBoundField}). Reads are not permitted with an unbounded window \u2014 set how far back the agent may read, then try again. Nothing was read.`
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 (ageBoundField && readAdapter.ageConstraint && readMaxAge !== null) {
1546
- clauses.push(readAdapter.ageConstraint.replace("{days}", String(readMaxAge)));
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 && ageBoundField && readAdapter.resultDatePath && readMaxAge !== null) {
1620
+ if (readAdapter && hasAgeDimension && readAdapter.resultDatePath && readMaxAge !== null) {
1585
1621
  const parsed = parseFirstJson(result);
1586
- const rawDate = parsed === void 0 ? void 0 : getByDottedPath(parsed, readAdapter.resultDatePath);
1587
- if (isOlderThanMaxAge(parseMessageTimestamp(rawDate), readMaxAge, Date.now())) {
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
- * RESERVED / NOT YET WIRED (see IntegrationConfig.readAgeDays). The local
2842
- * read-age window (days) for a running integration, for a deferred feature
2843
- * that moves the read age off the signed grant. The read path does NOT call
2844
- * this yet — it still enforces the signed `read_max_age_days` bound.
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 days = this.running.get(integrationId)?.config.readAgeDays;
2848
- return typeof days === "number" && Number.isFinite(days) ? days : null;
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);