@cloudgrid-io/mcp 0.21.22 → 0.21.24

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/README.md CHANGED
@@ -95,7 +95,7 @@ posted). `CLOUDGRID_QA_IDLE_MS` overrides the abandoned-idle window (default
95
95
  | `grid_collab` | API | Collab: GET PUSH ACCESS to the SAME live entity that someone else owns — you become a collaborator on THAT entity, no... |
96
96
  | `grid_create_grid` | API | Create a new grid (workspace) for the signed-in user — they become its admin. |
97
97
  | `grid_note` | API | Optionally leave a one-paragraph summary of what you built this session and why. |
98
- | `grid_plug` | API | Plug an app, website, game, or single HTML page into CloudGrid — the live runtime that runs it and provides its infra... |
98
+ | `grid_plug` | API | Plug an app, website, game, or single HTML page into CloudGrid and get a live public URL. |
99
99
  | `grid_get_app_source` | API | Retrieve the CURRENT deployed HTML of an inspiration/drop inline as text, so you can edit it and re-plug the SAME URL... |
100
100
  | `grid_login` | API | Start a CLI-free CloudGrid sign-in. |
101
101
  | `grid_login_status` | API | Finish a sign-in started by grid_login. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.21.22",
3
+ "version": "0.21.24",
4
4
  "description": "MCP access to CloudGrid — the live runtime environment where the user's apps run WITH the infrastructure they need out of the box: managed MongoDB, Redis (cache/queues/pub-sub), a pgvector store, persistent disk, and built-in AI with no API keys, in any language or stack, no Docker or Kubernetes to manage. Brainstorm, build, plug, and improve apps, games, dashboards, and tools; instant single-page publishes and full runtime-app deploys with a live URL — for developers and non-technical users. Three editions: local stdio (full toolset), hosted web (light, CLI-free), and a public docs-search server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -71,6 +71,7 @@
71
71
  "test:collab": "node test/collab.test.mjs",
72
72
  "test:url-shape": "node test/url-shape-guard.test.mjs",
73
73
  "test:pull-grid": "node test/pull-grid-param.test.mjs",
74
+ "test:pull-claim-scope": "node test/pull-claim-scope.test.mjs",
74
75
  "test:pull-output-schema": "node test/pull-output-schema.test.mjs",
75
76
  "test:web-identity": "node --test test/web-identity.test.mjs",
76
77
  "test:web-auth-integration": "node --test test/web-auth-integration.test.mjs",
@@ -78,6 +79,7 @@
78
79
  "test:oauth-client-persistence": "node --test test/oauth-client-persistence.test.mjs",
79
80
  "test:oauth-token-exchange": "node --test test/oauth-token-exchange.test.mjs",
80
81
  "test:oauth-resource-indicators": "node --test test/oauth-resource-indicators.test.mjs",
82
+ "test:oauth-redirect-consent": "node --test test/oauth-redirect-consent.test.mjs",
81
83
  "test:oauth-observability": "node --test test/oauth-flow-observability.test.mjs",
82
84
  "test:web-capabilities-log": "node --test test/web-capabilities-log.test.mjs",
83
85
  "test:mcp-transport-observability": "node --test test/mcp-transport-observability.test.mjs",
@@ -3,5 +3,5 @@ uvicorn[standard]==0.30.1
3
3
  pymongo==4.18.2
4
4
  numpy==1.26.4
5
5
  httpx==0.27.0
6
- pypdf==6.19.0
6
+ pypdf==6.20.1
7
7
  python-docx==1.1.2
@@ -5,5 +5,5 @@
5
5
  # pypdf / python-docx are imported lazily by source.extract_text for pdf/docx.
6
6
  pymongo==4.18.2
7
7
  httpx==0.27.0
8
- pypdf==6.19.0
8
+ pypdf==6.21.1
9
9
  python-docx==1.1.2
@@ -13,82 +13,40 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "@vitejs/plugin-react": "^6.0.4",
16
- "vite": "^8.1.5"
16
+ "vite": "^8.3.5"
17
17
  }
18
18
  },
19
- "node_modules/@emnapi/core": {
20
- "version": "2.0.0-alpha.3",
21
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz",
22
- "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==",
23
- "dev": true,
24
- "license": "MIT",
25
- "optional": true,
26
- "peer": true,
27
- "dependencies": {
28
- "@emnapi/wasi-threads": "2.0.1",
29
- "tslib": "^2.4.0"
30
- }
31
- },
32
- "node_modules/@emnapi/runtime": {
33
- "version": "2.0.0-alpha.3",
34
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz",
35
- "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==",
36
- "dev": true,
37
- "license": "MIT",
38
- "optional": true,
39
- "peer": true,
40
- "dependencies": {
41
- "tslib": "^2.4.0"
42
- }
43
- },
44
- "node_modules/@emnapi/wasi-threads": {
45
- "version": "2.0.1",
46
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz",
47
- "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==",
19
+ "node_modules/@oxc-project/types": {
20
+ "version": "0.155.0",
21
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz",
22
+ "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==",
48
23
  "dev": true,
49
24
  "license": "MIT",
50
- "optional": true,
51
- "peer": true,
52
- "dependencies": {
53
- "tslib": "^2.4.0"
25
+ "funding": {
26
+ "url": "https://github.com/sponsors/oxc-project"
54
27
  }
55
28
  },
56
- "node_modules/@napi-rs/wasm-runtime": {
57
- "version": "1.2.0",
58
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz",
59
- "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==",
29
+ "node_modules/@rolldown/binding-android-arm-eabi": {
30
+ "version": "1.2.15",
31
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
32
+ "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==",
33
+ "cpu": [
34
+ "arm"
35
+ ],
60
36
  "dev": true,
61
37
  "license": "MIT",
62
38
  "optional": true,
63
- "dependencies": {
64
- "@tybys/wasm-util": "^0.10.3"
65
- },
39
+ "os": [
40
+ "android"
41
+ ],
66
42
  "engines": {
67
- "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
68
- },
69
- "funding": {
70
- "type": "github",
71
- "url": "https://github.com/sponsors/Brooooooklyn"
72
- },
73
- "peerDependencies": {
74
- "@emnapi/core": "^2.0.0-alpha.3",
75
- "@emnapi/runtime": "^2.0.0-alpha.3"
76
- }
77
- },
78
- "node_modules/@oxc-project/types": {
79
- "version": "0.139.0",
80
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
81
- "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
82
- "dev": true,
83
- "license": "MIT",
84
- "funding": {
85
- "url": "https://github.com/sponsors/Boshen"
43
+ "node": "^20.19.0 || >=22.12.0"
86
44
  }
87
45
  },
88
46
  "node_modules/@rolldown/binding-android-arm64": {
89
- "version": "1.1.5",
90
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
91
- "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
47
+ "version": "1.2.15",
48
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz",
49
+ "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==",
92
50
  "cpu": [
93
51
  "arm64"
94
52
  ],
@@ -103,9 +61,9 @@
103
61
  }
104
62
  },
105
63
  "node_modules/@rolldown/binding-darwin-arm64": {
106
- "version": "1.1.5",
107
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
108
- "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
64
+ "version": "1.2.15",
65
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz",
66
+ "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==",
109
67
  "cpu": [
110
68
  "arm64"
111
69
  ],
@@ -120,9 +78,9 @@
120
78
  }
121
79
  },
122
80
  "node_modules/@rolldown/binding-darwin-x64": {
123
- "version": "1.1.5",
124
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
125
- "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
81
+ "version": "1.2.15",
82
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz",
83
+ "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==",
126
84
  "cpu": [
127
85
  "x64"
128
86
  ],
@@ -137,9 +95,9 @@
137
95
  }
138
96
  },
139
97
  "node_modules/@rolldown/binding-freebsd-x64": {
140
- "version": "1.1.5",
141
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
142
- "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
98
+ "version": "1.2.15",
99
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz",
100
+ "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==",
143
101
  "cpu": [
144
102
  "x64"
145
103
  ],
@@ -154,9 +112,9 @@
154
112
  }
155
113
  },
156
114
  "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
157
- "version": "1.1.5",
158
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
159
- "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
115
+ "version": "1.2.15",
116
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz",
117
+ "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==",
160
118
  "cpu": [
161
119
  "arm"
162
120
  ],
@@ -171,13 +129,16 @@
171
129
  }
172
130
  },
173
131
  "node_modules/@rolldown/binding-linux-arm64-gnu": {
174
- "version": "1.1.5",
175
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
176
- "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
132
+ "version": "1.2.15",
133
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz",
134
+ "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==",
177
135
  "cpu": [
178
136
  "arm64"
179
137
  ],
180
138
  "dev": true,
139
+ "libc": [
140
+ "glibc"
141
+ ],
181
142
  "license": "MIT",
182
143
  "optional": true,
183
144
  "os": [
@@ -188,13 +149,16 @@
188
149
  }
189
150
  },
190
151
  "node_modules/@rolldown/binding-linux-arm64-musl": {
191
- "version": "1.1.5",
192
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
193
- "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
152
+ "version": "1.2.15",
153
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz",
154
+ "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==",
194
155
  "cpu": [
195
156
  "arm64"
196
157
  ],
197
158
  "dev": true,
159
+ "libc": [
160
+ "musl"
161
+ ],
198
162
  "license": "MIT",
199
163
  "optional": true,
200
164
  "os": [
@@ -205,13 +169,16 @@
205
169
  }
206
170
  },
207
171
  "node_modules/@rolldown/binding-linux-ppc64-gnu": {
208
- "version": "1.1.5",
209
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
210
- "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
172
+ "version": "1.2.15",
173
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz",
174
+ "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==",
211
175
  "cpu": [
212
176
  "ppc64"
213
177
  ],
214
178
  "dev": true,
179
+ "libc": [
180
+ "glibc"
181
+ ],
215
182
  "license": "MIT",
216
183
  "optional": true,
217
184
  "os": [
@@ -222,13 +189,16 @@
222
189
  }
223
190
  },
224
191
  "node_modules/@rolldown/binding-linux-s390x-gnu": {
225
- "version": "1.1.5",
226
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
227
- "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
192
+ "version": "1.2.15",
193
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz",
194
+ "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==",
228
195
  "cpu": [
229
196
  "s390x"
230
197
  ],
231
198
  "dev": true,
199
+ "libc": [
200
+ "glibc"
201
+ ],
232
202
  "license": "MIT",
233
203
  "optional": true,
234
204
  "os": [
@@ -239,13 +209,16 @@
239
209
  }
240
210
  },
241
211
  "node_modules/@rolldown/binding-linux-x64-gnu": {
242
- "version": "1.1.5",
243
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
244
- "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
212
+ "version": "1.2.15",
213
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz",
214
+ "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==",
245
215
  "cpu": [
246
216
  "x64"
247
217
  ],
248
218
  "dev": true,
219
+ "libc": [
220
+ "glibc"
221
+ ],
249
222
  "license": "MIT",
250
223
  "optional": true,
251
224
  "os": [
@@ -256,13 +229,16 @@
256
229
  }
257
230
  },
258
231
  "node_modules/@rolldown/binding-linux-x64-musl": {
259
- "version": "1.1.5",
260
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
261
- "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
232
+ "version": "1.2.15",
233
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz",
234
+ "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==",
262
235
  "cpu": [
263
236
  "x64"
264
237
  ],
265
238
  "dev": true,
239
+ "libc": [
240
+ "musl"
241
+ ],
266
242
  "license": "MIT",
267
243
  "optional": true,
268
244
  "os": [
@@ -273,9 +249,9 @@
273
249
  }
274
250
  },
275
251
  "node_modules/@rolldown/binding-openharmony-arm64": {
276
- "version": "1.1.5",
277
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
278
- "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
252
+ "version": "1.2.15",
253
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz",
254
+ "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==",
279
255
  "cpu": [
280
256
  "arm64"
281
257
  ],
@@ -289,63 +265,10 @@
289
265
  "node": "^20.19.0 || >=22.12.0"
290
266
  }
291
267
  },
292
- "node_modules/@rolldown/binding-wasm32-wasi": {
293
- "version": "1.1.5",
294
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
295
- "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
296
- "cpu": [
297
- "wasm32"
298
- ],
299
- "dev": true,
300
- "license": "MIT",
301
- "optional": true,
302
- "dependencies": {
303
- "@emnapi/core": "1.11.1",
304
- "@emnapi/runtime": "1.11.1",
305
- "@napi-rs/wasm-runtime": "^1.1.6"
306
- },
307
- "engines": {
308
- "node": "^20.19.0 || >=22.12.0"
309
- }
310
- },
311
- "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
312
- "version": "1.11.1",
313
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
314
- "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
315
- "dev": true,
316
- "license": "MIT",
317
- "optional": true,
318
- "dependencies": {
319
- "@emnapi/wasi-threads": "1.2.2",
320
- "tslib": "^2.4.0"
321
- }
322
- },
323
- "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
324
- "version": "1.11.1",
325
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
326
- "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
327
- "dev": true,
328
- "license": "MIT",
329
- "optional": true,
330
- "dependencies": {
331
- "tslib": "^2.4.0"
332
- }
333
- },
334
- "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
335
- "version": "1.2.2",
336
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
337
- "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
338
- "dev": true,
339
- "license": "MIT",
340
- "optional": true,
341
- "dependencies": {
342
- "tslib": "^2.4.0"
343
- }
344
- },
345
268
  "node_modules/@rolldown/binding-win32-arm64-msvc": {
346
- "version": "1.1.5",
347
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
348
- "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
269
+ "version": "1.2.15",
270
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz",
271
+ "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==",
349
272
  "cpu": [
350
273
  "arm64"
351
274
  ],
@@ -360,9 +283,9 @@
360
283
  }
361
284
  },
362
285
  "node_modules/@rolldown/binding-win32-x64-msvc": {
363
- "version": "1.1.5",
364
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
365
- "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
286
+ "version": "1.2.15",
287
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz",
288
+ "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==",
366
289
  "cpu": [
367
290
  "x64"
368
291
  ],
@@ -383,17 +306,6 @@
383
306
  "dev": true,
384
307
  "license": "MIT"
385
308
  },
386
- "node_modules/@tybys/wasm-util": {
387
- "version": "0.10.3",
388
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
389
- "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
390
- "dev": true,
391
- "license": "MIT",
392
- "optional": true,
393
- "dependencies": {
394
- "tslib": "^2.4.0"
395
- }
396
- },
397
309
  "node_modules/@vitejs/plugin-react": {
398
310
  "version": "6.0.4",
399
311
  "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz",
@@ -743,9 +655,9 @@
743
655
  }
744
656
  },
745
657
  "node_modules/nanoid": {
746
- "version": "3.3.16",
747
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
748
- "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
658
+ "version": "3.3.20",
659
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
660
+ "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
749
661
  "dev": true,
750
662
  "funding": [
751
663
  {
@@ -769,9 +681,9 @@
769
681
  "license": "ISC"
770
682
  },
771
683
  "node_modules/picomatch": {
772
- "version": "4.0.5",
773
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
774
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
684
+ "version": "4.0.7",
685
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
686
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
775
687
  "dev": true,
776
688
  "license": "MIT",
777
689
  "engines": {
@@ -782,9 +694,9 @@
782
694
  }
783
695
  },
784
696
  "node_modules/postcss": {
785
- "version": "8.5.24",
786
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz",
787
- "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==",
697
+ "version": "8.5.31",
698
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
699
+ "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
788
700
  "dev": true,
789
701
  "funding": [
790
702
  {
@@ -802,9 +714,9 @@
802
714
  ],
803
715
  "license": "MIT",
804
716
  "dependencies": {
805
- "nanoid": "^3.3.16",
717
+ "nanoid": "^3.3.20",
806
718
  "picocolors": "^1.1.1",
807
- "source-map-js": "^1.2.1"
719
+ "source-map-js": "^1.2.2"
808
720
  },
809
721
  "engines": {
810
722
  "node": "^10 || ^12 || >=14"
@@ -836,13 +748,13 @@
836
748
  }
837
749
  },
838
750
  "node_modules/rolldown": {
839
- "version": "1.1.5",
840
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
841
- "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
751
+ "version": "1.2.15",
752
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz",
753
+ "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==",
842
754
  "dev": true,
843
755
  "license": "MIT",
844
756
  "dependencies": {
845
- "@oxc-project/types": "=0.139.0",
757
+ "@oxc-project/types": "=0.155.0",
846
758
  "@rolldown/pluginutils": "^1.0.0"
847
759
  },
848
760
  "bin": {
@@ -852,21 +764,21 @@
852
764
  "node": "^20.19.0 || >=22.12.0"
853
765
  },
854
766
  "optionalDependencies": {
855
- "@rolldown/binding-android-arm64": "1.1.5",
856
- "@rolldown/binding-darwin-arm64": "1.1.5",
857
- "@rolldown/binding-darwin-x64": "1.1.5",
858
- "@rolldown/binding-freebsd-x64": "1.1.5",
859
- "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
860
- "@rolldown/binding-linux-arm64-gnu": "1.1.5",
861
- "@rolldown/binding-linux-arm64-musl": "1.1.5",
862
- "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
863
- "@rolldown/binding-linux-s390x-gnu": "1.1.5",
864
- "@rolldown/binding-linux-x64-gnu": "1.1.5",
865
- "@rolldown/binding-linux-x64-musl": "1.1.5",
866
- "@rolldown/binding-openharmony-arm64": "1.1.5",
867
- "@rolldown/binding-wasm32-wasi": "1.1.5",
868
- "@rolldown/binding-win32-arm64-msvc": "1.1.5",
869
- "@rolldown/binding-win32-x64-msvc": "1.1.5"
767
+ "@rolldown/binding-android-arm-eabi": "1.2.15",
768
+ "@rolldown/binding-android-arm64": "1.2.15",
769
+ "@rolldown/binding-darwin-arm64": "1.2.15",
770
+ "@rolldown/binding-darwin-x64": "1.2.15",
771
+ "@rolldown/binding-freebsd-x64": "1.2.15",
772
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.15",
773
+ "@rolldown/binding-linux-arm64-gnu": "1.2.15",
774
+ "@rolldown/binding-linux-arm64-musl": "1.2.15",
775
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.15",
776
+ "@rolldown/binding-linux-s390x-gnu": "1.2.15",
777
+ "@rolldown/binding-linux-x64-gnu": "1.2.15",
778
+ "@rolldown/binding-linux-x64-musl": "1.2.15",
779
+ "@rolldown/binding-openharmony-arm64": "1.2.15",
780
+ "@rolldown/binding-win32-arm64-msvc": "1.2.15",
781
+ "@rolldown/binding-win32-x64-msvc": "1.2.15"
870
782
  }
871
783
  },
872
784
  "node_modules/scheduler": {
@@ -905,25 +817,17 @@
905
817
  "url": "https://github.com/sponsors/SuperchupuDev"
906
818
  }
907
819
  },
908
- "node_modules/tslib": {
909
- "version": "2.8.1",
910
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
911
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
912
- "dev": true,
913
- "license": "0BSD",
914
- "optional": true
915
- },
916
820
  "node_modules/vite": {
917
- "version": "8.1.5",
918
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
919
- "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
821
+ "version": "8.3.5",
822
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
823
+ "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
920
824
  "dev": true,
921
825
  "license": "MIT",
922
826
  "dependencies": {
923
- "lightningcss": "^1.32.0",
924
- "picomatch": "^4.0.5",
925
- "postcss": "^8.5.17",
926
- "rolldown": "~1.1.5",
827
+ "lightningcss": "^1.33.0",
828
+ "picomatch": "^4.0.7",
829
+ "postcss": "^8.5.30",
830
+ "rolldown": "~1.2.14",
927
831
  "tinyglobby": "^0.2.17"
928
832
  },
929
833
  "bin": {
@@ -940,7 +844,7 @@
940
844
  },
941
845
  "peerDependencies": {
942
846
  "@types/node": "^20.19.0 || >=22.12.0",
943
- "@vitejs/devtools": "^0.3.0",
847
+ "@vitejs/devtools": "^0.7.1 || ^0.8.0",
944
848
  "esbuild": "^0.27.0 || ^0.28.0",
945
849
  "jiti": ">=1.21.0",
946
850
  "less": "^4.0.0",
@@ -14,6 +14,6 @@
14
14
  },
15
15
  "devDependencies": {
16
16
  "@vitejs/plugin-react": "^6.0.4",
17
- "vite": "^8.1.5"
17
+ "vite": "^8.3.5"
18
18
  }
19
19
  }
package/src/oauth.js CHANGED
@@ -117,10 +117,10 @@ function normalizeResource(uri) {
117
117
  // 3. Later, DROP the old key. client_ids minted under it stop verifying and
118
118
  // those users re-add the connector ONCE — the deliberate-rotation cost,
119
119
  // versus today's every-deploy breakage.
120
- // An attacker who obtains the secret can forge a client_id for an arbitrary
121
- // redirect set — but /oauth/register already mints one for any redirect set
122
- // unauthenticated, so the secret gates INTEGRITY, not registration; PKCE +
123
- // CloudGrid sign-in still gate getting a token.
120
+ // Whoever holds the secret can forge a client_id for an arbitrary redirect set,
121
+ // so /oauth/authorize re-applies the redirect URI rules to every client_id it
122
+ // verifies rather than trusting the signed set alone; PKCE + CloudGrid sign-in
123
+ // still gate getting a token.
124
124
  const CLIENT_ID_PREFIX = "cg1"; // versioned so the scheme can evolve
125
125
 
126
126
  // A client_id CARRIES its redirect set and travels in a query string, so the set
@@ -147,6 +147,58 @@ function canonicalRedirectUris(uris) {
147
147
  return JSON.stringify([...new Set(uris.map((u) => String(u)))].sort());
148
148
  }
149
149
 
150
+ // The signed payload. Without a client_name it is the bare redirect array — the
151
+ // original format, so every client_id minted before client_name was carried
152
+ // still verifies unchanged. With one, it is { redirect_uris, client_name }.
153
+ function signedPayload(canonical, clientName) {
154
+ if (!clientName) return canonical;
155
+ return JSON.stringify({ redirect_uris: JSON.parse(canonical), client_name: clientName });
156
+ }
157
+
158
+ // A registered client_name is shown on the consent step, so it is bounded and
159
+ // stripped of control characters. It is self-asserted by the client: the consent
160
+ // step shows the redirect HOST alongside it, and the host is what the user can
161
+ // actually check.
162
+ const MAX_CLIENT_NAME_LENGTH = 80;
163
+ function cleanClientName(name) {
164
+ if (typeof name !== "string") return "";
165
+ return name.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_CLIENT_NAME_LENGTH);
166
+ }
167
+
168
+ // --- Redirect URI rules -------------------------------------------------------
169
+ // A redirect URI must be `https:`, or `http:` on a loopback address (local
170
+ // clients run a callback listener on 127.0.0.1 / [::1] / localhost). Anything
171
+ // else — other schemes, http to a non-loopback host, a fragment (RFC 6749
172
+ // §3.1.2), embedded credentials — is refused. Enforced at registration
173
+ // (invalid_redirect_uri, RFC 7591 §3.2.2) and again at authorize, so a client_id
174
+ // minted before these rules cannot be used with a URI that breaks them.
175
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "[::1]", "localhost"]);
176
+
177
+ // Returns null when the URI is acceptable, else a reason for error_description.
178
+ export function redirectUriProblem(uri) {
179
+ if (typeof uri !== "string" || uri === "") return "redirect_uri must be a non-empty string.";
180
+ let u;
181
+ try {
182
+ u = new URL(uri);
183
+ } catch {
184
+ return "redirect_uri must be an absolute URI.";
185
+ }
186
+ if (uri.includes("#")) return "redirect_uri must not contain a fragment.";
187
+ if (u.username || u.password) return "redirect_uri must not contain user credentials.";
188
+ if (u.protocol === "https:" && u.hostname) return null;
189
+ if (u.protocol === "http:" && LOOPBACK_HOSTS.has(u.hostname)) return null;
190
+ return "redirect_uri must use https, or http on a loopback address (127.0.0.1, [::1], localhost).";
191
+ }
192
+
193
+ function isHttpUrl(uri) {
194
+ try {
195
+ const { protocol } = new URL(String(uri));
196
+ return protocol === "https:" || protocol === "http:";
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
150
202
  function signClientId(canonical, secret) {
151
203
  const payload = b64url(Buffer.from(canonical, "utf8"));
152
204
  const signingInput = `${CLIENT_ID_PREFIX}.${payload}`;
@@ -154,7 +206,8 @@ function signClientId(canonical, secret) {
154
206
  return `${signingInput}.${sig}`;
155
207
  }
156
208
 
157
- // Verify a client_id against any accepted secret; returns { redirect_uris } or null.
209
+ // Verify a client_id against any accepted secret; returns
210
+ // { redirect_uris, client_name } or null.
158
211
  function verifyClientId(clientId, secrets) {
159
212
  const parts = String(clientId).split(".");
160
213
  if (parts.length !== 3 || parts[0] !== CLIENT_ID_PREFIX) return null;
@@ -172,8 +225,12 @@ function verifyClientId(clientId, secrets) {
172
225
  if (!ok) return null;
173
226
  try {
174
227
  const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
175
- const uris = JSON.parse(json);
176
- return Array.isArray(uris) ? { redirect_uris: uris } : null;
228
+ const parsed = JSON.parse(json);
229
+ if (Array.isArray(parsed)) return { redirect_uris: parsed, client_name: "" };
230
+ if (parsed && Array.isArray(parsed.redirect_uris)) {
231
+ return { redirect_uris: parsed.redirect_uris, client_name: cleanClientName(parsed.client_name) };
232
+ }
233
+ return null;
177
234
  } catch {
178
235
  return null;
179
236
  }
@@ -185,39 +242,96 @@ function corsOk(res) {
185
242
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, mcp-protocol-version");
186
243
  }
187
244
 
188
- // The §23-voice interstitial. Opens the CloudGrid sign-in in a new tab and polls
189
- // until it completes, then returns the browser to the client app.
190
- function interstitialHtml(sid, loginUrl) {
245
+ // HTML-escape for text and attribute positions.
246
+ function escapeHtml(value) {
247
+ return String(value)
248
+ .replace(/&/g, "&")
249
+ .replace(/</g, "&lt;")
250
+ .replace(/>/g, "&gt;")
251
+ .replace(/"/g, "&quot;")
252
+ .replace(/'/g, "&#39;");
253
+ }
254
+
255
+ // JSON for embedding inside an inline <script>: `<` and the JS line
256
+ // separators are escaped so the value cannot close the script element.
257
+ function scriptJson(value) {
258
+ return JSON.stringify(value)
259
+ .replace(/</g, "\\u003c")
260
+ .replace(/>/g, "\\u003e")
261
+ .replace(/&/g, "\\u0026")
262
+ .replace(/\u2028/g, "\\u2028")
263
+ .replace(/\u2029/g, "\\u2029");
264
+ }
265
+
266
+ // Every OAuth HTML response refuses to be framed, and does not leak its URL
267
+ // (which carries client_id and state) as a referrer.
268
+ function oauthPageHeaders(res) {
269
+ res.setHeader("X-Frame-Options", "DENY");
270
+ res.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
271
+ res.setHeader("Referrer-Policy", "no-referrer");
272
+ }
273
+
274
+ function sendOAuthError(res, message) {
275
+ oauthPageHeaders(res);
276
+ res.status(400).type("text/plain; charset=utf-8").send(message);
277
+ }
278
+
279
+ // The §23-voice interstitial. First a consent step naming the client and the
280
+ // host the browser returns to; nothing starts until the user continues. Then it
281
+ // opens the CloudGrid sign-in in a new tab and polls until it completes, and
282
+ // returns the browser to the client app.
283
+ function interstitialHtml({ sid, loginUrl, clientName, redirectHost }) {
284
+ const who = clientName
285
+ ? `<strong>${escapeHtml(clientName)}</strong> (<strong>${escapeHtml(redirectHost)}</strong>)`
286
+ : `An application at <strong>${escapeHtml(redirectHost)}</strong>`;
191
287
  return `<!doctype html>
192
288
  <html lang="en">
193
289
  <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
290
+ <meta name="referrer" content="no-referrer">
194
291
  <title>Connect CloudGrid</title>
195
292
  <link rel="icon" type="image/png" href="/favicon.png">
196
293
  <style>
197
294
  body { margin:0; min-height:100vh; display:grid; place-items:center;
198
295
  font-family: Inter, system-ui, sans-serif; background:#0d0d0f; color:#fafafa; }
199
296
  main { max-width:420px; padding:2.5rem; text-align:center; }
297
+ p.host { margin-top:1rem; font-size:.95rem; }
200
298
  a.btn { display:inline-block; margin-top:1.25rem; padding:.75rem 1.5rem; border-radius:8px;
201
299
  background:#fafafa; color:#0d0d0f; text-decoration:none; font-weight:600; }
202
300
  p.status { margin-top:1.5rem; font-size:.9rem; opacity:.7; }
301
+ [hidden] { display:none !important; }
203
302
  </style></head>
204
303
  <body><main>
205
304
  <h1>Connect CloudGrid</h1>
206
- <p>Sign in with your CloudGrid account. This page returns to the app when you finish.</p>
207
- <a class="btn" href="${loginUrl}" target="_blank" rel="noopener">Sign in to CloudGrid</a>
208
- <p class="status" id="st">Waiting for sign-in.</p>
305
+ <p id="consent">${who} wants to connect to your CloudGrid account.</p>
306
+ <p class="host">After sign-in you will be returned to <strong>${escapeHtml(redirectHost)}</strong>. If you did not start this, close this page.</p>
307
+ <a class="btn" id="go" href="${escapeHtml(loginUrl)}" target="_blank" rel="noopener noreferrer">Continue to sign in</a>
308
+ <p class="status" id="st" hidden>Waiting for sign-in.</p>
209
309
  </main>
210
310
  <script>
311
+ const POLL_URL = ${scriptJson(`/oauth/authorize/poll?sid=${sid}`)};
312
+ const st = document.getElementById("st");
313
+ let started = false;
211
314
  async function tick() {
212
315
  try {
213
- const r = await fetch("/oauth/authorize/poll?sid=${sid}");
316
+ const r = await fetch(POLL_URL);
214
317
  const d = await r.json();
215
- if (d.status === "ready") { location.href = d.redirect; return; }
216
- if (d.status === "expired") { document.getElementById("st").textContent = "The sign-in window expired. Close this page and connect again."; return; }
318
+ if (d.status === "ready") {
319
+ let ok = false;
320
+ try { const u = new URL(d.redirect); ok = u.protocol === "https:" || u.protocol === "http:"; } catch {}
321
+ if (ok) { location.href = d.redirect; return; }
322
+ st.textContent = "This connection cannot be completed. Close this page and connect again.";
323
+ return;
324
+ }
325
+ if (d.status === "expired") { st.textContent = "The sign-in window expired. Close this page and connect again."; return; }
217
326
  } catch {}
218
327
  setTimeout(tick, 2000);
219
328
  }
220
- tick();
329
+ document.getElementById("go").addEventListener("click", () => {
330
+ st.hidden = false;
331
+ if (started) return;
332
+ started = true;
333
+ tick();
334
+ });
221
335
  </script></body></html>`;
222
336
  }
223
337
 
@@ -325,6 +439,13 @@ export function mountOAuth(app, publicBase, opts = {}) {
325
439
  });
326
440
  return;
327
441
  }
442
+ for (const uri of redirectUris) {
443
+ const problem = redirectUriProblem(uri);
444
+ if (problem) {
445
+ res.status(400).json({ error: "invalid_redirect_uri", error_description: problem });
446
+ return;
447
+ }
448
+ }
328
449
  const canonical = canonicalRedirectUris(redirectUris);
329
450
  if (Buffer.byteLength(canonical, "utf8") > MAX_REDIRECT_URIS_BYTES) {
330
451
  res.status(400).json({
@@ -333,9 +454,11 @@ export function mountOAuth(app, publicBase, opts = {}) {
333
454
  });
334
455
  return;
335
456
  }
336
- const clientId = signClientId(canonical, clientSecrets.secrets[0]);
457
+ const clientName = cleanClientName(req.body?.client_name);
458
+ const clientId = signClientId(signedPayload(canonical, clientName), clientSecrets.secrets[0]);
337
459
  res.status(201).json({
338
460
  client_id: clientId,
461
+ ...(clientName ? { client_name: clientName } : {}),
339
462
  token_endpoint_auth_method: "none",
340
463
  grant_types: ["authorization_code"],
341
464
  response_types: ["code"],
@@ -362,11 +485,19 @@ export function mountOAuth(app, publicBase, opts = {}) {
362
485
  );
363
486
  const client = verifyClientId(client_id, clientSecrets.secrets);
364
487
  if (!client || !client.redirect_uris.includes(String(redirect_uri))) {
365
- res.status(400).send("Unknown client or redirect_uri. Re-add the connector and try again.");
488
+ sendOAuthError(res, "Unknown client or redirect_uri. Re-add the connector and try again.");
489
+ return;
490
+ }
491
+ // Re-checked here, not only at registration: a client_id signed before the
492
+ // redirect rules existed still carries its original set. Rendered, never
493
+ // redirected to.
494
+ if (redirectUriProblem(String(redirect_uri))) {
495
+ console.error(`[oauth] authorize refused: redirect_uri not allowed redirect_host=${hostOf(redirect_uri)}`);
496
+ sendOAuthError(res, "This redirect_uri is not allowed. Re-add the connector and try again.");
366
497
  return;
367
498
  }
368
499
  if (response_type !== "code" || code_challenge_method !== "S256" || !code_challenge) {
369
- res.status(400).send("This server requires response_type=code with PKCE (S256).");
500
+ sendOAuthError(res, "This server requires response_type=code with PKCE (S256).");
370
501
  return;
371
502
  }
372
503
  // RFC 8707 (#329): if a `resource` is present it must be THIS server's
@@ -413,8 +544,16 @@ export function mountOAuth(app, publicBase, opts = {}) {
413
544
  resource: boundResource,
414
545
  created: Date.now(),
415
546
  });
547
+ oauthPageHeaders(res);
416
548
  res.setHeader("Content-Type", "text/html; charset=utf-8");
417
- res.send(interstitialHtml(sid, buildLoginUrl(cgCode)));
549
+ res.send(
550
+ interstitialHtml({
551
+ sid,
552
+ loginUrl: buildLoginUrl(cgCode),
553
+ clientName: client.client_name,
554
+ redirectHost: new URL(String(redirect_uri)).host,
555
+ }),
556
+ );
418
557
  });
419
558
 
420
559
  // The interstitial's poll — bridges to CloudGrid /auth/status.
@@ -433,6 +572,13 @@ export function mountOAuth(app, publicBase, opts = {}) {
433
572
  return;
434
573
  }
435
574
  if (upstream.status === "authenticated" && upstream.jwt) {
575
+ if (!isHttpUrl(sess.redirect_uri)) {
576
+ // Unreachable while authorize enforces the redirect rules; kept so a
577
+ // code is never handed to a non-http(s) target.
578
+ authSessions.delete(String(req.query.sid));
579
+ res.json({ status: "expired" });
580
+ return;
581
+ }
436
582
  const code = b64url(Buffer.from(randomUUID()));
437
583
  authCodes.set(code, {
438
584
  jwt: upstream.jwt,
@@ -760,6 +760,17 @@ export async function runReport(
760
760
  });
761
761
  }
762
762
 
763
+ // The session's anonymous claim token (the anon owner token) is a capability
764
+ // bound to the ONE entity it was minted for. Return it only when `entityId` is
765
+ // that entity: the remembered `entity_id`, else the token's own `ent` claim
766
+ // (decoded without verifying, only to compare ids; the server verifies on use).
767
+ // Any other entity gets no token, so it never leaks to an unrelated route (#418).
768
+ export function anonClaimTokenFor(claim, entityId) {
769
+ if (!claim?.token || !entityId) return null;
770
+ const boundId = claim.entity_id || decodeJwt(claim.token).ent || null;
771
+ return boundId === entityId ? claim.token : null;
772
+ }
773
+
763
774
  // grid_pull — continue/edit an existing entity IN PLACE (POST /entities/:id/pickup).
764
775
  // Like `git clone` of the SAME entity: your next grid_plug (target_entity_id)
765
776
  // updates it. Needs push access — you must OWN it or be a COLLABORATOR. A
@@ -789,7 +800,6 @@ export async function runPull(ctx, { claim_token, claim_url, entity_id, grid } =
789
800
  claimToken = null;
790
801
  }
791
802
  }
792
- if (!claimToken && ctx.state.lastAnonClaim) claimToken = ctx.state.lastAnonClaim.token;
793
803
 
794
804
  // Pickup needs the target entity id in the URL path. Prefer an explicit one,
795
805
  // else the entity remembered from this session's anonymous drop.
@@ -798,6 +808,9 @@ export async function runPull(ctx, { claim_token, claim_url, entity_id, grid } =
798
808
  ctx.state.lastAnonClaim?.entity_id ||
799
809
  // The last drop counts only when it is anon-owned (it holds an owner token).
800
810
  (ctx.state.lastDrop?.owner_token ? ctx.state.lastDrop.entity_id : null);
811
+ // The session's anon claim token rides along only for the entity it was
812
+ // minted for (#418). Pulling any other entity sends no claim_token.
813
+ if (!claimToken) claimToken = anonClaimTokenFor(ctx.state.lastAnonClaim, targetId);
801
814
  if (!claimToken && targetId && ctx.state.lastDrop?.entity_id === targetId) {
802
815
  claimToken = ctx.state.lastDrop.owner_token ?? null;
803
816
  }
@@ -1899,11 +1912,8 @@ export async function runPlug(ctx, input, deps = {}) {
1899
1912
  // session made anonymously.
1900
1913
  if (ctx.state.lastDrop?.entity_id === targetEntityId && ctx.state.lastDrop.owner_token) {
1901
1914
  ownerToken = ctx.state.lastDrop.owner_token;
1902
- } else if (
1903
- ctx.state.lastAnonClaim?.entity_id === targetEntityId &&
1904
- ctx.state.lastAnonClaim.token
1905
- ) {
1906
- ownerToken = ctx.state.lastAnonClaim.token;
1915
+ } else {
1916
+ ownerToken = anonClaimTokenFor(ctx.state.lastAnonClaim, targetEntityId);
1907
1917
  }
1908
1918
  }
1909
1919
  // An anon-minted (Guest-Grid) drop is edited via the owner-token wire even
@@ -406,7 +406,9 @@ export function registerTools(server, ctx) {
406
406
  "The source inline, one entry per file — for hosted/no-filesystem transports (a multi-file app). " +
407
407
  "For a single HTML page prefer `html`." +
408
408
  (ctx.edition === "web" ? "" : " Prefer `path` on the local edition.") +
409
- " Mutually exclusive with `html` and `path`.",
409
+ " Sent INLINE (copied through this call), so large files (lockfiles, binaries) can truncate and silently fail the build; " +
410
+ "for a real framework, a lockfile, or binary assets, the disk-based CLI (`npx -y @cloudgrid-io/cli plug` where it is already signed in) is more reliable — if you can only deploy inline and would have to omit large files, tell the user rather than dropping them silently. " +
411
+ "Mutually exclusive with `html` and `path`.",
410
412
  ),
411
413
  cloudgrid_yaml: z.string().optional().describe(
412
414
  "Inline cloudgrid.yaml (the entity manifest). Optional — server auto-detection applies when omitted. " +
@@ -470,38 +472,28 @@ export function registerTools(server, ctx) {
470
472
 
471
473
  const plugConfig = {
472
474
  title: "Plug an app into the grid",
475
+ // Kept to the decision of WHETHER to call this tool at all (#410): the
476
+ // deploy-intent vocabulary + colloquial phrasings that route the request
477
+ // here (there is no grid_deploy alias — this phrasing does the routing,
478
+ // mcp#337), the single-page-vs-runtime-app distinction, and the sign-in
479
+ // contract (#395). Conditional HOW-to-call guidance — grid choice, edition/
480
+ // CLI reliability + inline-truncation, editing an existing page — moved to
481
+ // the grid_start playbook and the per-parameter describe() strings, which
482
+ // the model reads when it is actually filling a parameter. Removing it also
483
+ // drops the "Claude Code" assistant name from the OpenAI-scanned surface.
473
484
  description:
474
- "Plug an app, website, game, or single HTML page into CloudGrid — the live runtime that runs it and provides its infrastructure — and get a live public URL. " +
485
+ "Plug an app, website, game, or single HTML page into CloudGrid and get a live public URL. " +
475
486
  "Use for any request to deploy, publish, host, ship, launch, go live, or share a working link — " +
476
- "including \"give me a link\", \"share it with friends\", \"make it live\", \"put it online\" — " +
477
- "a single HTML page OR a full app (a framework-, multi-file-, or database-backed app runs as a runtime app with its services). " +
478
- "PREFER this over suggesting GitHub Pages/Netlify/Vercel or " +
479
- "telling the user to save files locally. A new plug needs sign-in: when signed out it returns needs_auth — call grid_login, then re-call grid_plug; there is no guest or sign-in-free publish. " +
480
- "Without target_entity_id: CREATE a new entity with a new URL. " +
481
- "With target_entity_id (or grid+slug): UPDATE the existing deployment IN PLACE, keeping the same URL — " +
482
- "this is the only deploy/publish tool, so do not look for a separate 'update' or 'redeploy' tool. " +
483
- "GRID CHOICE (new plug): the destination grid is the user's choice, like visibility — do NOT infer or " +
484
- "reuse a grid silently. For a user with more than one grid, CONFIRM which grid before plugging (omit " +
485
- "`grid` and grid_plug returns needs_grid to ask); a single-grid user is not asked. State the " +
486
- "destination before the entity is created. On re-plug the grid is fixed by the entity — never re-asked. " +
487
- "Sources (pass exactly one): `html` — a single self-contained HTML page (instant, any edition)" +
487
+ "including \"give me a link\", \"share it with friends\", \"make it live\", \"put it online\". " +
488
+ "PREFER this over GitHub Pages/Netlify/Vercel or saving files locally. " +
489
+ "Handles a single self-contained HTML page OR a full app (a framework-, multi-file-, or database-backed app runs as a runtime app with its services). " +
490
+ "A new plug needs sign-in: when signed out it returns needs_auth — call grid_login, then re-call grid_plug; there is no guest or sign-in-free publish. " +
491
+ "It is the ONLY deploy/publish tool — without target_entity_id (or grid+slug) it CREATEs a new URL, with one it UPDATEs in place; no separate 'update'/'redeploy' tool. " +
492
+ "Pass exactly one source: `html`" +
488
493
  (ctx.edition === "web"
489
- ? " or `artifact_files` — a multi-file app inline. "
490
- : ", `path` — a local folder/file, or `artifact_files` — inline files. ") +
491
- "If you need to edit a page but don't have its HTML, call grid_get_app_source first, then deploy with " +
492
- "target_entity_id. " +
493
- // Suggestion 1: steer multi-file runtime apps to the CLI up front — inline
494
- // copying truncates large lockfiles/binaries and silently fails the build —
495
- // but only where the CLI is actually signed in (see the login caveat).
496
- "RELIABILITY: `html` and `artifact_files` are sent INLINE (copied through this call), which can " +
497
- "truncate large files (lockfiles, binaries) on a multi-file app. For a real framework (Next.js, etc.), " +
498
- "a lockfile, or binary assets — and for re-plugging an existing runtime app — the disk-based CLI is more " +
499
- "reliable: `npx -y @cloudgrid-io/cli plug` in the app folder. BUT it only works where the CLI is already " +
500
- "signed in — a terminal or Claude Code that ran `grid login` once. Do NOT attempt `grid login` inside a " +
501
- "chat sandbox: its login is a long poll and the sandbox is ephemeral, so it will not complete. If you can " +
502
- "only deploy inline here and would have to omit large files, do NOT drop them silently — tell the user " +
503
- "and offer to run the full deploy from Claude Code or a terminal (same entity_id). Reserve inline for a " +
504
- "single page or a few small text files. (CloudGrid calls this operation 'plug'.)",
494
+ ? " or `artifact_files` (inline multi-file app). "
495
+ : ", `path` (local folder/file), or `artifact_files`. ") +
496
+ "To edit a page without its HTML, call grid_get_app_source first, then deploy with target_entity_id.",
505
497
  inputSchema: plugInputSchema,
506
498
  outputSchema: {
507
499
  entity_id: z.string().optional().describe("Globally unique — pass back as target_entity_id to re-plug."),