@reproapp/mcp 0.0.9 → 0.0.10
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 +27 -170
- package/package.json +1 -1
- package/src/index.js +286 -37
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Repro MCP Server
|
|
2
2
|
|
|
3
|
-
Repro
|
|
3
|
+
Add Repro tools to an AI agent that supports MCP. Once connected, the agent can
|
|
4
|
+
inspect Repro sessions, requests, traces, database changes, and privacy policies
|
|
5
|
+
without you copying data out of Repro manually.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## Add To Your AI Agent
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Default hosted Repro setup:
|
|
9
|
+
Use the default hosted Repro setup:
|
|
10
10
|
|
|
11
11
|
```json
|
|
12
12
|
{
|
|
@@ -19,181 +19,38 @@ Default hosted Repro setup:
|
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Custom Repro deployment:
|
|
23
|
-
|
|
24
|
-
```json
|
|
25
|
-
{
|
|
26
|
-
"mcpServers": {
|
|
27
|
-
"repro": {
|
|
28
|
-
"command": "npx",
|
|
29
|
-
"args": ["-y", "@reproapp/mcp@latest"],
|
|
30
|
-
"env": {
|
|
31
|
-
"REPRO_AI_BASE_URL": "https://your-repro-ai-url/",
|
|
32
|
-
"REPRO_API_BASE_URL": "https://your-repro-api-url/api/",
|
|
33
|
-
"REPRO_WEB_BASE_URL": "https://your-repro-web-url/"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
22
|
The installed binary is `repro-mcp`.
|
|
41
23
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
When a user asks about Repro sessions, requests, traces, privacy policies, or app configuration, the AI client must use the Repro MCP tools only. It must not write local scripts, call Repro HTTP APIs directly, read cached Repro auth files, or patch policy JSON outside the MCP tool surface.
|
|
45
|
-
|
|
46
|
-
Privacy policy updates are intentionally handle-based:
|
|
47
|
-
|
|
48
|
-
- Use `inspect_privacy_policy` or `list_privacy_policy_blockers` first.
|
|
49
|
-
- For one policy statement, pass the exact `statementSid` to `update_privacy_policy`.
|
|
50
|
-
- For all currently listed blockers, pass the returned `blockerSetId` to `update_privacy_policy`.
|
|
51
|
-
- Broad natural-language updates such as "make all blockers keep exact" are read-only/dry-run unless they include a `blockerSetId` or exact statement ids.
|
|
52
|
-
- `KEEP_EXACT` on sensitive or raw-text review statements requires `force=true` and a human reason.
|
|
53
|
-
|
|
54
|
-
## Configuration
|
|
55
|
-
|
|
56
|
-
Built-in defaults:
|
|
57
|
-
|
|
58
|
-
- `REPRO_AI_BASE_URL=https://repro-ai-u8lhw.ondigitalocean.app`
|
|
59
|
-
- `REPRO_API_BASE_URL=https://repro-api-d7288.ondigitalocean.app/api`
|
|
60
|
-
- `REPRO_WEB_BASE_URL=https://getrepro.io`
|
|
61
|
-
|
|
62
|
-
Optional:
|
|
63
|
-
|
|
64
|
-
- `REPRO_MCP_TRANSPORT` (`stdio` or `http`; default `http` when `PORT` is set, otherwise `stdio`)
|
|
65
|
-
- `REPRO_MCP_HTTP_HOST`
|
|
66
|
-
- `REPRO_MCP_HTTP_PORT`
|
|
67
|
-
- `REPRO_MCP_HTTP_PATH`
|
|
68
|
-
- `REPRO_MCP_CORS_ORIGIN`
|
|
69
|
-
- `REPRO_API_BASE_URL`
|
|
70
|
-
- `REPRO_API_PUBLIC_BASE_URL`
|
|
71
|
-
- `REPRO_WEB_BASE_URL`
|
|
72
|
-
- `REPRO_AI_TOKEN`
|
|
73
|
-
- `REPRO_API_TOKEN`
|
|
74
|
-
- `REPRO_DEFAULT_TENANT_ID`
|
|
75
|
-
- `REPRO_APP_ID`
|
|
76
|
-
- `REPRO_REQUEST_TIMEOUT_MS`
|
|
77
|
-
- `REPRO_INVESTIGATE_TIMEOUT_MS`
|
|
78
|
-
- `REPRO_MCP_ENV_FILE`
|
|
79
|
-
- `REPRO_MCP_AUTH_FILE`
|
|
80
|
-
- `REPRO_MCP_AUTH_TIMEOUT_MS`
|
|
81
|
-
- `REPRO_MCP_AUTO_AUTH`
|
|
82
|
-
- `REPRO_MCP_AUTH_MODE` (`wait` or `prompt`; default `prompt` for HTTP and `wait` for stdio)
|
|
83
|
-
- `REPRO_MCP_OPEN_BROWSER`
|
|
84
|
-
- `REPRO_MCP_DEBUG_LOG`
|
|
85
|
-
- `REPRO_MCP_LAUNCH_LOG`
|
|
86
|
-
- `REPRO_PATH_MAPPINGS`
|
|
87
|
-
|
|
88
|
-
Hosted HTTP example:
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
export REPRO_MCP_HTTP_HOST="0.0.0.0"
|
|
92
|
-
export REPRO_MCP_HTTP_PORT="8080"
|
|
93
|
-
export REPRO_MCP_HTTP_PATH="/mcp"
|
|
94
|
-
export REPRO_AI_BASE_URL="http://repro-ai:4011/"
|
|
95
|
-
export REPRO_API_BASE_URL="http://api:4010/api/"
|
|
96
|
-
export REPRO_API_PUBLIC_BASE_URL="https://api.example.com/api/"
|
|
97
|
-
export REPRO_WEB_BASE_URL="https://app.example.com/"
|
|
98
|
-
export REPRO_DEFAULT_TENANT_ID="TENANT_123"
|
|
99
|
-
export REPRO_INVESTIGATE_TIMEOUT_MS="90000"
|
|
100
|
-
export REPRO_MCP_AUTO_AUTH="true"
|
|
101
|
-
export REPRO_MCP_AUTH_MODE="prompt"
|
|
102
|
-
export REPRO_MCP_OPEN_BROWSER="false"
|
|
103
|
-
export REPRO_PATH_MAPPINGS='[
|
|
104
|
-
{
|
|
105
|
-
"remotePrefix": "/srv/repro-demo",
|
|
106
|
-
"localPrefix": "/Users/alice/work/repro-demo"
|
|
107
|
-
}
|
|
108
|
-
]'
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
`run-repro-mcp.sh` loads `repro-mcp/.env`, then `repro-mcp/.env.local`, then the file pointed to by `REPRO_MCP_ENV_FILE` when provided. Environment variables already set by the process take precedence in `src/index.js`; for DigitalOcean App Platform, prefer setting values in the app's environment configuration instead of committing an env file.
|
|
112
|
-
|
|
113
|
-
`REPRO_API_BASE_URL` is used for privacy policy inspection/update tools and browser authentication. `REPRO_WEB_BASE_URL` is used for Privacy Studio links.
|
|
114
|
-
|
|
115
|
-
For hosted MCP, point `REPRO_API_BASE_URL` at the private/internal API address used by the MCP container. Set `REPRO_API_PUBLIC_BASE_URL` to the public API origin users can open in a browser. The MCP server sends that public origin to the API when it creates browser-auth sessions, so the returned authorize URL is browser-accessible even when MCP talks to the API over VPC.
|
|
116
|
-
|
|
117
|
-
`REPRO_PATH_MAPPINGS` is optional. Use it only when telemetry contains deployed code paths that should map to a checkout available inside the MCP runtime. For a hosted container without a mounted checkout, keep it as `[]`.
|
|
118
|
-
|
|
119
|
-
For local development, copy `.env.local.example` to `.env.local`. It contains the old localhost defaults:
|
|
120
|
-
|
|
121
|
-
- `REPRO_AI_BASE_URL=http://127.0.0.1:4011`
|
|
122
|
-
- `REPRO_API_BASE_URL=http://127.0.0.1:4010/api`
|
|
123
|
-
- `REPRO_WEB_BASE_URL=http://127.0.0.1:3000`
|
|
124
|
-
- local `REPRO_PATH_MAPPINGS` for the demo checkout
|
|
125
|
-
|
|
126
|
-
If `REPRO_AI_TOKEN` and `REPRO_API_TOKEN` are not set, `repro-mcp` starts a browser login flow against `REPRO_API_BASE_URL`, caches the resulting app-user bearer token, and reuses it on future requests. Stdio uses `REPRO_MCP_AUTH_MODE=wait`: it prints the authorize URL and waits for approval. HTTP uses `REPRO_MCP_AUTH_MODE=prompt`: the first protected MCP request returns a JSON-RPC error with `error.data.authorizeUrl`; open that URL, approve access, then retry the same MCP request.
|
|
127
|
-
|
|
128
|
-
For the cached app-user token to work with `repro-ai`, configure `repro-ai` with the same `APP_USER_JWT_SECRET` as `repro-api`. Static `REPRO_AI_TOKEN` / `REPRO_API_TOKEN` values are optional overrides, not the normal hosted browser-auth path.
|
|
24
|
+
## Use It
|
|
129
25
|
|
|
130
|
-
|
|
26
|
+
After adding the MCP server, ask your AI agent Repro-specific questions:
|
|
131
27
|
|
|
132
|
-
|
|
28
|
+
```text
|
|
29
|
+
Investigate why session SESSION_123 failed checkout.
|
|
133
30
|
|
|
134
|
-
|
|
31
|
+
Show the request, trace, and DB evidence for this bug report.
|
|
135
32
|
|
|
136
|
-
|
|
137
|
-
docker build -t repro-mcp ./repro-mcp
|
|
138
|
-
```
|
|
33
|
+
Inspect privacy policy blockers for APP_123, serviceName orders-api, in dev.
|
|
139
34
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
REPRO_MCP_HTTP_HOST=0.0.0.0
|
|
144
|
-
PORT=8080
|
|
145
|
-
REPRO_MCP_HTTP_PATH=/mcp
|
|
146
|
-
REPRO_AI_BASE_URL=http://repro-ai:4011/
|
|
147
|
-
REPRO_API_BASE_URL=http://api:4010/api/
|
|
148
|
-
REPRO_API_PUBLIC_BASE_URL=https://your-public-api-host/api/
|
|
149
|
-
REPRO_WEB_BASE_URL=https://your-repro-web-host/
|
|
150
|
-
REPRO_MCP_AUTO_AUTH=true
|
|
151
|
-
REPRO_MCP_AUTH_MODE=prompt
|
|
152
|
-
REPRO_MCP_OPEN_BROWSER=false
|
|
153
|
-
REPRO_PATH_MAPPINGS=[]
|
|
35
|
+
Update these privacy blockers using blockerSetId BLOCKERS_123, then explain what still needs review.
|
|
154
36
|
```
|
|
155
37
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
- `repro-mcp` runs as a Web Service from `repro-mcp/Dockerfile` on port `8080`.
|
|
159
|
-
- `repro-mcp` reaches `repro-api` and `repro-ai` through VPC/internal routing.
|
|
160
|
-
- `REPRO_API_PUBLIC_BASE_URL` points to the public API URL used only for the browser authorize page.
|
|
161
|
-
- `repro-api` and `repro-ai` share the same `APP_USER_JWT_SECRET`, so the browser-auth app-user JWT can authorize both services.
|
|
162
|
-
|
|
163
|
-
HTTP endpoints:
|
|
164
|
-
|
|
165
|
-
- `GET /health` and `GET /healthz`: health checks
|
|
166
|
-
- `POST /mcp`: JSON-RPC MCP requests
|
|
167
|
-
- `GET /mcp`: event-stream placeholder for clients that open a stream
|
|
38
|
+
If authentication is required, the agent will provide an authorization URL.
|
|
39
|
+
Open it, approve access, then retry the same request.
|
|
168
40
|
|
|
169
|
-
|
|
41
|
+
## Privacy Policy Safety
|
|
170
42
|
|
|
171
|
-
|
|
43
|
+
Privacy policy updates are handle-based:
|
|
172
44
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
45
|
+
- Pass `serviceName` when an app has separate runtime policies per backend service.
|
|
46
|
+
- Inspect first with `inspect_privacy_policy` or `list_privacy_policy_blockers`.
|
|
47
|
+
- Update one statement with its exact `statementSid`.
|
|
48
|
+
- Update a listed blocker set with the exact `blockerSetId`.
|
|
49
|
+
- Broad requests such as "make all blockers keep exact" are read-only unless they include exact ids.
|
|
50
|
+
- Sensitive `KEEP_EXACT` decisions require an explicit human reason.
|
|
178
51
|
|
|
179
|
-
|
|
180
|
-
npm run start:stdio
|
|
181
|
-
```
|
|
52
|
+
## Agent Rules
|
|
182
53
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
- `resolve_scope`
|
|
187
|
-
- `list_actions`
|
|
188
|
-
- `list_requests`
|
|
189
|
-
- `get_request`
|
|
190
|
-
- `get_captured_value`
|
|
191
|
-
- `get_requests_relative_to_anchor`
|
|
192
|
-
- `get_db_changes`
|
|
193
|
-
- `get_trace`
|
|
194
|
-
- `get_action_path`
|
|
195
|
-
- `get_function_candidates`
|
|
196
|
-
- `inspect_privacy_policy`
|
|
197
|
-
- `list_privacy_policy_blockers`
|
|
198
|
-
- `update_privacy_policy`
|
|
199
|
-
- `approve_privacy_policy`
|
|
54
|
+
For Repro-scoped questions about sessions, requests, traces, privacy policies,
|
|
55
|
+
apps, or configuration, use the Repro MCP tools. Do not call Repro HTTP APIs
|
|
56
|
+
directly or edit policy JSON outside Repro.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -38,6 +38,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
38
38
|
properties: {
|
|
39
39
|
tenantId: { type: 'string' },
|
|
40
40
|
appId: { type: 'string' },
|
|
41
|
+
serviceName: { type: 'string' },
|
|
41
42
|
sessionId: { type: 'string' },
|
|
42
43
|
environment: { type: 'string' },
|
|
43
44
|
state: { type: 'string', enum: ['active', 'pending', 'approved'] },
|
|
@@ -56,6 +57,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
56
57
|
properties: {
|
|
57
58
|
tenantId: { type: 'string' },
|
|
58
59
|
appId: { type: 'string' },
|
|
60
|
+
serviceName: { type: 'string' },
|
|
59
61
|
appIds: {
|
|
60
62
|
type: 'array',
|
|
61
63
|
items: { type: 'string' },
|
|
@@ -79,6 +81,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
79
81
|
properties: {
|
|
80
82
|
tenantId: { type: 'string' },
|
|
81
83
|
appId: { type: 'string' },
|
|
84
|
+
serviceName: { type: 'string' },
|
|
82
85
|
environment: { type: 'string' },
|
|
83
86
|
state: { type: 'string', enum: ['active', 'pending', 'approved'] },
|
|
84
87
|
query: { type: 'string' },
|
|
@@ -101,6 +104,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
101
104
|
properties: {
|
|
102
105
|
tenantId: { type: 'string' },
|
|
103
106
|
appId: { type: 'string' },
|
|
107
|
+
serviceName: { type: 'string' },
|
|
104
108
|
environment: { type: 'string' },
|
|
105
109
|
state: { type: 'string', enum: ['active', 'pending', 'approved'] },
|
|
106
110
|
query: { type: 'string' },
|
|
@@ -119,6 +123,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
119
123
|
properties: {
|
|
120
124
|
tenantId: { type: 'string' },
|
|
121
125
|
appId: { type: 'string' },
|
|
126
|
+
serviceName: { type: 'string' },
|
|
122
127
|
environment: { type: 'string' },
|
|
123
128
|
state: { type: 'string', enum: ['active', 'pending', 'approved'] },
|
|
124
129
|
query: { type: 'string' },
|
|
@@ -170,6 +175,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
170
175
|
properties: {
|
|
171
176
|
tenantId: { type: 'string' },
|
|
172
177
|
appId: { type: 'string' },
|
|
178
|
+
serviceName: { type: 'string' },
|
|
173
179
|
environment: { type: 'string' },
|
|
174
180
|
openUi: { type: 'boolean' },
|
|
175
181
|
},
|
|
@@ -202,6 +208,10 @@ const TOOL_DESCRIPTION_OVERRIDES = {
|
|
|
202
208
|
'Exact evidence tool: list captured HTTP and backend transport requests in scope. Use this before investigate whenever the user asks what request happened, which endpoint ran, request timing/status, or which request RID anchors later DB/trace/message evidence.',
|
|
203
209
|
get_request:
|
|
204
210
|
'Exact evidence tool: fetch one captured request by RID, including request/response bodies and captured value refs when present. Use this before answering factual questions about a specific request payload, response, endpoint, or transport message wrapper.',
|
|
211
|
+
list_session_evidence:
|
|
212
|
+
'Generic exact evidence tool: list every supported kind of captured session evidence without client-specific assumptions. Use this first for factual inventory questions about emails, messages, requests, DB changes, traces, actions, payloads, or whether data exists.',
|
|
213
|
+
get_session_evidence:
|
|
214
|
+
'Generic exact evidence tool: fetch the full structured data for one item returned by list_session_evidence. Use this before claiming captured fields are absent or omitted.',
|
|
205
215
|
get_requests_relative_to_anchor:
|
|
206
216
|
'Exact evidence tool: list captured requests before or after an action/request/DB anchor. Use for ordering questions and localizing the request sequence around a UI action or backend event.',
|
|
207
217
|
get_db_changes:
|
|
@@ -1262,7 +1272,7 @@ function inputWithExactCapturedPayloads(name, input) {
|
|
|
1262
1272
|
}
|
|
1263
1273
|
|
|
1264
1274
|
async function expandLinkedCapturedPayloads(name, response, client, originalInput = {}) {
|
|
1265
|
-
if (name
|
|
1275
|
+
if (name === 'get_captured_value') {
|
|
1266
1276
|
return response;
|
|
1267
1277
|
}
|
|
1268
1278
|
|
|
@@ -1271,20 +1281,41 @@ async function expandLinkedCapturedPayloads(name, response, client, originalInpu
|
|
|
1271
1281
|
|
|
1272
1282
|
while (true) {
|
|
1273
1283
|
const links = collectCapturedPayloadLinks(root)
|
|
1274
|
-
.filter((entry) => !visited.has(
|
|
1275
|
-
|
|
1284
|
+
.filter((entry) => !visited.has(capturedPayloadExpansionKey(entry)));
|
|
1285
|
+
const valueRefs = collectCapturedValueRefs(root)
|
|
1286
|
+
.filter((entry) => !visited.has(capturedPayloadExpansionKey(entry)));
|
|
1287
|
+
const expansions = [...links, ...valueRefs];
|
|
1288
|
+
if (!expansions.length) {
|
|
1276
1289
|
return root;
|
|
1277
1290
|
}
|
|
1278
1291
|
|
|
1279
|
-
for (const
|
|
1280
|
-
const key =
|
|
1292
|
+
for (const expansion of expansions) {
|
|
1293
|
+
const key = capturedPayloadExpansionKey(expansion);
|
|
1281
1294
|
visited.add(key);
|
|
1282
1295
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1296
|
+
if (expansion.kind === 'value-capture') {
|
|
1297
|
+
const valueId = expansion.capture?.valueId;
|
|
1298
|
+
if (typeof valueId !== 'string' || !valueId.trim()) {
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
const expanded = await client.executeTool('get_captured_value', {
|
|
1302
|
+
...originalInput,
|
|
1303
|
+
valueId: valueId.trim(),
|
|
1304
|
+
});
|
|
1305
|
+
const value = expanded?.data?.capturedValue?.value ?? expanded?.capturedValue?.value;
|
|
1306
|
+
if (typeof value === 'undefined') {
|
|
1307
|
+
throw new Error(`Repro MCP could not expand captured value ${valueId}`);
|
|
1308
|
+
}
|
|
1309
|
+
expansion.holder[expansion.targetKey] = value;
|
|
1310
|
+
const availabilityKey = `${expansion.targetKey}Availability`;
|
|
1311
|
+
if (typeof expansion.holder[availabilityKey] === 'string') {
|
|
1312
|
+
expansion.holder[availabilityKey] = 'captured';
|
|
1313
|
+
}
|
|
1314
|
+
continue;
|
|
1286
1315
|
}
|
|
1287
1316
|
|
|
1317
|
+
const { link } = expansion;
|
|
1318
|
+
const toolName = typeof link.tool === 'string' ? link.tool : '';
|
|
1288
1319
|
const linkArgs = link.arguments && typeof link.arguments === 'object' && !Array.isArray(link.arguments)
|
|
1289
1320
|
? link.arguments
|
|
1290
1321
|
: {};
|
|
@@ -1335,8 +1366,72 @@ function collectCapturedPayloadLinks(value, out = []) {
|
|
|
1335
1366
|
return out;
|
|
1336
1367
|
}
|
|
1337
1368
|
|
|
1338
|
-
function
|
|
1369
|
+
function collectCapturedValueRefs(value, out = []) {
|
|
1370
|
+
if (!value || typeof value !== 'object') {
|
|
1371
|
+
return out;
|
|
1372
|
+
}
|
|
1373
|
+
if (Array.isArray(value)) {
|
|
1374
|
+
for (const item of value) {
|
|
1375
|
+
collectCapturedValueRefs(item, out);
|
|
1376
|
+
}
|
|
1377
|
+
return out;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
1381
|
+
if (
|
|
1382
|
+
key.endsWith('ValueCapture') &&
|
|
1383
|
+
nested &&
|
|
1384
|
+
typeof nested === 'object' &&
|
|
1385
|
+
!Array.isArray(nested) &&
|
|
1386
|
+
nested.status === 'stored' &&
|
|
1387
|
+
typeof nested.valueId === 'string' &&
|
|
1388
|
+
nested.valueId.trim()
|
|
1389
|
+
) {
|
|
1390
|
+
const targetKey = valueCaptureTargetKey(key);
|
|
1391
|
+
if (targetKey && shouldExpandValueCaptureTarget(value[targetKey])) {
|
|
1392
|
+
out.push({
|
|
1393
|
+
kind: 'value-capture',
|
|
1394
|
+
holder: value,
|
|
1395
|
+
captureKey: key,
|
|
1396
|
+
targetKey,
|
|
1397
|
+
capture: nested,
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
collectCapturedValueRefs(nested, out);
|
|
1402
|
+
}
|
|
1403
|
+
return out;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function shouldExpandValueCaptureTarget(value) {
|
|
1407
|
+
if (typeof value === 'undefined' || value === null) return true;
|
|
1408
|
+
return Boolean(
|
|
1409
|
+
value &&
|
|
1410
|
+
typeof value === 'object' &&
|
|
1411
|
+
!Array.isArray(value) &&
|
|
1412
|
+
value.__kind === 'omitted'
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function valueCaptureTargetKey(key) {
|
|
1417
|
+
if (key === 'responseBodyValueCapture') return 'responseBody';
|
|
1418
|
+
if (key === 'respBodyValueCapture') return 'respBody';
|
|
1419
|
+
if (!key.endsWith('ValueCapture')) return null;
|
|
1420
|
+
const base = key.slice(0, -'ValueCapture'.length);
|
|
1421
|
+
return base || null;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function capturedPayloadExpansionKey(expansion) {
|
|
1425
|
+
if (expansion?.kind === 'value-capture') {
|
|
1426
|
+
return JSON.stringify({
|
|
1427
|
+
kind: 'value-capture',
|
|
1428
|
+
valueId: expansion.capture?.valueId ?? null,
|
|
1429
|
+
targetKey: expansion.targetKey ?? null,
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
const link = expansion?.link;
|
|
1339
1433
|
return JSON.stringify({
|
|
1434
|
+
kind: 'mcp-link',
|
|
1340
1435
|
tool: link?.tool ?? null,
|
|
1341
1436
|
path: link?.path ?? null,
|
|
1342
1437
|
arguments: link?.arguments ?? null,
|
|
@@ -1506,10 +1601,11 @@ async function resolveScope(input, config, client) {
|
|
|
1506
1601
|
const appId = optionalNonEmptyString(input.appId) || config.defaultAppId;
|
|
1507
1602
|
const sessionId = optionalNonEmptyString(input.sessionId);
|
|
1508
1603
|
if (appId && !sessionId) {
|
|
1604
|
+
const serviceName = optionalNonEmptyString(input.serviceName);
|
|
1509
1605
|
const requestedEnvironment = optionalNonEmptyString(input.environment);
|
|
1510
1606
|
const state = normalizePolicyState(input.state);
|
|
1511
1607
|
const query = optionalNonEmptyString(input.query) || '';
|
|
1512
|
-
const privacyConfig = await client.requestApi(
|
|
1608
|
+
const privacyConfig = await client.requestApi(buildPrivacyApiPath(appId, serviceName), {
|
|
1513
1609
|
method: 'GET',
|
|
1514
1610
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
1515
1611
|
});
|
|
@@ -1521,6 +1617,7 @@ async function resolveScope(input, config, client) {
|
|
|
1521
1617
|
selection?.environmentId ?? requestedEnvironment,
|
|
1522
1618
|
query,
|
|
1523
1619
|
input.openUi === true,
|
|
1620
|
+
serviceName,
|
|
1524
1621
|
);
|
|
1525
1622
|
if (ui.openRequested) {
|
|
1526
1623
|
try {
|
|
@@ -1535,6 +1632,7 @@ async function resolveScope(input, config, client) {
|
|
|
1535
1632
|
status: selection ? 'ok' : 'not_found',
|
|
1536
1633
|
kind: 'app_privacy_scope',
|
|
1537
1634
|
appId,
|
|
1635
|
+
serviceName: serviceName ?? null,
|
|
1538
1636
|
tenantId: optionalNonEmptyString(input.tenantId) || config.defaultTenantId || null,
|
|
1539
1637
|
environment: selection?.environmentId ?? requestedEnvironment ?? null,
|
|
1540
1638
|
state: selection?.state ?? state,
|
|
@@ -1595,6 +1693,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1595
1693
|
const appIds = normalizeCoverageAppIds(input, config);
|
|
1596
1694
|
const state = normalizePolicyState(input.state);
|
|
1597
1695
|
const requestedEnvironment = optionalNonEmptyString(input.environment);
|
|
1696
|
+
const serviceName = optionalNonEmptyString(input.serviceName);
|
|
1598
1697
|
const topic = normalizeCoverageTopic(input);
|
|
1599
1698
|
const query = queryForCoverageTopic(topic);
|
|
1600
1699
|
const includeTechnical = input.includeTechnical === true;
|
|
@@ -1602,7 +1701,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1602
1701
|
|
|
1603
1702
|
for (const appId of appIds) {
|
|
1604
1703
|
try {
|
|
1605
|
-
const privacyConfig = await client.requestApi(
|
|
1704
|
+
const privacyConfig = await client.requestApi(buildPrivacyApiPath(appId, serviceName), {
|
|
1606
1705
|
method: 'GET',
|
|
1607
1706
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
1608
1707
|
});
|
|
@@ -1614,7 +1713,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1614
1713
|
environment: requestedEnvironment ?? null,
|
|
1615
1714
|
state,
|
|
1616
1715
|
summary: 'No pending or approved privacy policy was found for this app/environment.',
|
|
1617
|
-
ui: buildPrivacyStudioUi(config, appId, requestedEnvironment, query, false),
|
|
1716
|
+
ui: buildPrivacyStudioUi(config, appId, requestedEnvironment, query, false, serviceName),
|
|
1618
1717
|
matches: [],
|
|
1619
1718
|
needsReviewReasons: ['No policy found.'],
|
|
1620
1719
|
});
|
|
@@ -1634,13 +1733,20 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1634
1733
|
? assessPolicyBlockerCoverage(matchedStatements, contradictions)
|
|
1635
1734
|
: assessCoverage(matchedStatements, contradictions);
|
|
1636
1735
|
const evidence = buildCoverageEvidence(matchedStatements, contradictions, { includeTechnical });
|
|
1637
|
-
const ui = buildPrivacyStudioUi(
|
|
1736
|
+
const ui = buildPrivacyStudioUi(
|
|
1737
|
+
config,
|
|
1738
|
+
appId,
|
|
1739
|
+
selection.environmentId,
|
|
1740
|
+
query,
|
|
1741
|
+
input.openUi === true,
|
|
1742
|
+
serviceName,
|
|
1743
|
+
);
|
|
1638
1744
|
const policyBlockers = buildPolicyBlockerSummaries(
|
|
1639
1745
|
blockerQuery ? matchedStatements : matchedStatements.filter(isReviewBlockerStatement),
|
|
1640
1746
|
config,
|
|
1641
1747
|
appId,
|
|
1642
1748
|
selection.environmentId,
|
|
1643
|
-
{ includeTechnical },
|
|
1749
|
+
{ includeTechnical, serviceName },
|
|
1644
1750
|
);
|
|
1645
1751
|
if (ui.openRequested) {
|
|
1646
1752
|
try {
|
|
@@ -1654,6 +1760,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1654
1760
|
|
|
1655
1761
|
results.push({
|
|
1656
1762
|
appId,
|
|
1763
|
+
serviceName: serviceName ?? null,
|
|
1657
1764
|
environment: selection.environmentId,
|
|
1658
1765
|
state: selection.state,
|
|
1659
1766
|
inspectedPolicy: {
|
|
@@ -1683,7 +1790,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1683
1790
|
environment: requestedEnvironment ?? null,
|
|
1684
1791
|
state,
|
|
1685
1792
|
summary: error instanceof Error ? error.message : String(error),
|
|
1686
|
-
ui: buildPrivacyStudioUi(config, appId, requestedEnvironment, query, false),
|
|
1793
|
+
ui: buildPrivacyStudioUi(config, appId, requestedEnvironment, query, false, serviceName),
|
|
1687
1794
|
matches: [],
|
|
1688
1795
|
needsReviewReasons: ['Could not inspect policy because the API request failed.'],
|
|
1689
1796
|
});
|
|
@@ -1702,6 +1809,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1702
1809
|
inspected: {
|
|
1703
1810
|
requestedEnvironment: requestedEnvironment ?? null,
|
|
1704
1811
|
requestedState: state,
|
|
1812
|
+
serviceName: serviceName ?? null,
|
|
1705
1813
|
appCount: appIds.length,
|
|
1706
1814
|
},
|
|
1707
1815
|
summary: summarizeCoverageResults({ safe, needsReview, notCovered, errors }),
|
|
@@ -2025,9 +2133,10 @@ async function inspectPrivacyPolicy(input, config, client) {
|
|
|
2025
2133
|
throw rpcError(-32603, 'This MCP client cannot fetch app privacy policies');
|
|
2026
2134
|
}
|
|
2027
2135
|
|
|
2136
|
+
const serviceName = optionalNonEmptyString(input.serviceName);
|
|
2028
2137
|
const requestedEnvironment = optionalNonEmptyString(input.environment);
|
|
2029
2138
|
const state = normalizePolicyState(input.state);
|
|
2030
|
-
const privacyConfig = await client.requestApi(
|
|
2139
|
+
const privacyConfig = await client.requestApi(buildPrivacyApiPath(appId, serviceName), {
|
|
2031
2140
|
method: 'GET',
|
|
2032
2141
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
2033
2142
|
});
|
|
@@ -2036,10 +2145,18 @@ async function inspectPrivacyPolicy(input, config, client) {
|
|
|
2036
2145
|
return {
|
|
2037
2146
|
status: 'not_found',
|
|
2038
2147
|
appId,
|
|
2148
|
+
serviceName: serviceName ?? null,
|
|
2039
2149
|
environment: requestedEnvironment ?? null,
|
|
2040
2150
|
state,
|
|
2041
2151
|
message: 'No pending or approved privacy policy exists for the requested app/environment.',
|
|
2042
|
-
ui: buildPrivacyStudioUi(
|
|
2152
|
+
ui: buildPrivacyStudioUi(
|
|
2153
|
+
config,
|
|
2154
|
+
appId,
|
|
2155
|
+
requestedEnvironment,
|
|
2156
|
+
policyInspectionQuery(input),
|
|
2157
|
+
input.openUi === true,
|
|
2158
|
+
serviceName,
|
|
2159
|
+
),
|
|
2043
2160
|
};
|
|
2044
2161
|
}
|
|
2045
2162
|
|
|
@@ -2051,7 +2168,14 @@ async function inspectPrivacyPolicy(input, config, client) {
|
|
|
2051
2168
|
const matchedLeaves = new Set(matches.flatMap((match) => statementTargetLeaves(match.statement)));
|
|
2052
2169
|
const contradictions = findSameLeafPolicyContradictions(statements, matchedLeaves);
|
|
2053
2170
|
const uiQuery = query || contradictions[0]?.leaf || '';
|
|
2054
|
-
const ui = buildPrivacyStudioUi(
|
|
2171
|
+
const ui = buildPrivacyStudioUi(
|
|
2172
|
+
config,
|
|
2173
|
+
appId,
|
|
2174
|
+
selection.environmentId,
|
|
2175
|
+
uiQuery,
|
|
2176
|
+
input.openUi === true,
|
|
2177
|
+
serviceName,
|
|
2178
|
+
);
|
|
2055
2179
|
const blockerStatements = blockerQuery
|
|
2056
2180
|
? matches.map((match) => match.statement).filter(isReviewBlockerStatement)
|
|
2057
2181
|
: matches.map((match) => match.statement).filter(isReviewBlockerStatement);
|
|
@@ -2060,12 +2184,13 @@ async function inspectPrivacyPolicy(input, config, client) {
|
|
|
2060
2184
|
config,
|
|
2061
2185
|
appId,
|
|
2062
2186
|
selection.environmentId,
|
|
2063
|
-
{ includeTechnical },
|
|
2187
|
+
{ includeTechnical, serviceName },
|
|
2064
2188
|
);
|
|
2065
2189
|
const blockerSet = policyBlockers.length
|
|
2066
2190
|
? rememberPrivacyBlockerSet({
|
|
2067
2191
|
tenantId: optionalNonEmptyString(input.tenantId) || config.defaultTenantId || null,
|
|
2068
2192
|
appId,
|
|
2193
|
+
serviceName,
|
|
2069
2194
|
environment: selection.environmentId,
|
|
2070
2195
|
state: selection.state,
|
|
2071
2196
|
query,
|
|
@@ -2085,6 +2210,7 @@ async function inspectPrivacyPolicy(input, config, client) {
|
|
|
2085
2210
|
return {
|
|
2086
2211
|
status: 'ok',
|
|
2087
2212
|
appId,
|
|
2213
|
+
serviceName: serviceName ?? null,
|
|
2088
2214
|
environment: selection.environmentId,
|
|
2089
2215
|
state: selection.state,
|
|
2090
2216
|
generatedAt: selection.record?.policy?.generatedAt ?? selection.record?.generatedAt ?? null,
|
|
@@ -2116,12 +2242,13 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2116
2242
|
throw rpcError(-32603, 'This MCP client cannot fetch or update app privacy policies');
|
|
2117
2243
|
}
|
|
2118
2244
|
|
|
2245
|
+
const serviceName = optionalNonEmptyString(input.serviceName);
|
|
2119
2246
|
const requestedEnvironment = optionalNonEmptyString(input.environment);
|
|
2120
2247
|
const state = normalizePolicyState(input.state);
|
|
2121
2248
|
const decision = normalizePolicyUpdateDecision(input.decision);
|
|
2122
2249
|
const apply = input.apply === true;
|
|
2123
2250
|
const approve = input.approve === true;
|
|
2124
|
-
const privacyConfig = await client.requestApi(
|
|
2251
|
+
const privacyConfig = await client.requestApi(buildPrivacyApiPath(appId, serviceName), {
|
|
2125
2252
|
method: 'GET',
|
|
2126
2253
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
2127
2254
|
});
|
|
@@ -2130,10 +2257,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2130
2257
|
return {
|
|
2131
2258
|
status: 'not_found',
|
|
2132
2259
|
appId,
|
|
2260
|
+
serviceName: serviceName ?? null,
|
|
2133
2261
|
environment: requestedEnvironment ?? null,
|
|
2134
2262
|
state,
|
|
2135
2263
|
summary: 'No pending or approved privacy policy exists for the requested app/environment.',
|
|
2136
|
-
ui: buildPrivacyStudioUi(
|
|
2264
|
+
ui: buildPrivacyStudioUi(
|
|
2265
|
+
config,
|
|
2266
|
+
appId,
|
|
2267
|
+
requestedEnvironment,
|
|
2268
|
+
policyInspectionQuery(input),
|
|
2269
|
+
input.openUi === true,
|
|
2270
|
+
serviceName,
|
|
2271
|
+
),
|
|
2137
2272
|
};
|
|
2138
2273
|
}
|
|
2139
2274
|
|
|
@@ -2142,6 +2277,7 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2142
2277
|
const blockerSetSelection = resolvePrivacyBlockerSetSelection(input, {
|
|
2143
2278
|
tenantId: optionalNonEmptyString(input.tenantId) || config.defaultTenantId || null,
|
|
2144
2279
|
appId,
|
|
2280
|
+
serviceName,
|
|
2145
2281
|
environment: selection.environmentId,
|
|
2146
2282
|
state: selection.state,
|
|
2147
2283
|
});
|
|
@@ -2150,10 +2286,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2150
2286
|
status: 'blocked',
|
|
2151
2287
|
code: blockerSetSelection.error.code,
|
|
2152
2288
|
appId,
|
|
2289
|
+
serviceName: serviceName ?? null,
|
|
2153
2290
|
environment: selection.environmentId,
|
|
2154
2291
|
state: selection.state,
|
|
2155
2292
|
summary: blockerSetSelection.error.message,
|
|
2156
|
-
ui: buildPrivacyStudioUi(
|
|
2293
|
+
ui: buildPrivacyStudioUi(
|
|
2294
|
+
config,
|
|
2295
|
+
appId,
|
|
2296
|
+
selection.environmentId,
|
|
2297
|
+
query,
|
|
2298
|
+
input.openUi === true,
|
|
2299
|
+
serviceName,
|
|
2300
|
+
),
|
|
2157
2301
|
nextStep: 'Run inspect_privacy_policy or list_privacy_policy_blockers again, then pass the returned blockerSetId.',
|
|
2158
2302
|
};
|
|
2159
2303
|
}
|
|
@@ -2168,10 +2312,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2168
2312
|
return {
|
|
2169
2313
|
status: 'not_found',
|
|
2170
2314
|
appId,
|
|
2315
|
+
serviceName: serviceName ?? null,
|
|
2171
2316
|
environment: selection.environmentId,
|
|
2172
2317
|
state: selection.state,
|
|
2173
2318
|
summary: 'No matching policy statements were found for the requested update.',
|
|
2174
|
-
ui: buildPrivacyStudioUi(
|
|
2319
|
+
ui: buildPrivacyStudioUi(
|
|
2320
|
+
config,
|
|
2321
|
+
appId,
|
|
2322
|
+
selection.environmentId,
|
|
2323
|
+
query,
|
|
2324
|
+
input.openUi === true,
|
|
2325
|
+
serviceName,
|
|
2326
|
+
),
|
|
2175
2327
|
matches: rankPolicyStatementMatches(statements, input, query).slice(0, 8).map((match) => formatPolicyMatch(match.statement, match.score, { includeTechnical: true })),
|
|
2176
2328
|
};
|
|
2177
2329
|
}
|
|
@@ -2180,10 +2332,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2180
2332
|
status: 'blocked',
|
|
2181
2333
|
code: 'STALE_BLOCKER_SET',
|
|
2182
2334
|
appId,
|
|
2335
|
+
serviceName: serviceName ?? null,
|
|
2183
2336
|
environment: selection.environmentId,
|
|
2184
2337
|
state: selection.state,
|
|
2185
2338
|
summary: `The blockerSetId references ${blockerSetSelection.statementSids.length} statement(s), but only ${selectedStatements.length} still exist in the current policy.`,
|
|
2186
|
-
ui: buildPrivacyStudioUi(
|
|
2339
|
+
ui: buildPrivacyStudioUi(
|
|
2340
|
+
config,
|
|
2341
|
+
appId,
|
|
2342
|
+
selection.environmentId,
|
|
2343
|
+
query,
|
|
2344
|
+
input.openUi === true,
|
|
2345
|
+
serviceName,
|
|
2346
|
+
),
|
|
2187
2347
|
matches: selectedStatements.map((statement) => formatPolicyMatch(statement, 0, { includeTechnical: true })),
|
|
2188
2348
|
nextStep: 'Run inspect_privacy_policy or list_privacy_policy_blockers again, then retry with the new blockerSetId.',
|
|
2189
2349
|
};
|
|
@@ -2200,10 +2360,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2200
2360
|
status: 'blocked',
|
|
2201
2361
|
code: safety.code,
|
|
2202
2362
|
appId,
|
|
2363
|
+
serviceName: serviceName ?? null,
|
|
2203
2364
|
environment: selection.environmentId,
|
|
2204
2365
|
state: selection.state,
|
|
2205
2366
|
summary: safety.summary,
|
|
2206
|
-
ui: buildPrivacyStudioUi(
|
|
2367
|
+
ui: buildPrivacyStudioUi(
|
|
2368
|
+
config,
|
|
2369
|
+
appId,
|
|
2370
|
+
selection.environmentId,
|
|
2371
|
+
query,
|
|
2372
|
+
input.openUi === true,
|
|
2373
|
+
serviceName,
|
|
2374
|
+
),
|
|
2207
2375
|
matches: selectedStatements.map((statement) => formatPolicyMatch(statement, 0, { includeTechnical: true })),
|
|
2208
2376
|
proposed: safeBuildPolicyUpdateDiff(selection.policy, selectedStatements, decision, input.rawTextHints),
|
|
2209
2377
|
safety,
|
|
@@ -2218,6 +2386,7 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2218
2386
|
selection.environmentId,
|
|
2219
2387
|
query || selectedStatements[0]?.when?.selector || '',
|
|
2220
2388
|
input.openUi === true,
|
|
2389
|
+
serviceName,
|
|
2221
2390
|
);
|
|
2222
2391
|
if (ui.openRequested) {
|
|
2223
2392
|
try {
|
|
@@ -2233,6 +2402,7 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2233
2402
|
return {
|
|
2234
2403
|
status: 'dry_run',
|
|
2235
2404
|
appId,
|
|
2405
|
+
serviceName: serviceName ?? null,
|
|
2236
2406
|
environment: selection.environmentId,
|
|
2237
2407
|
state: selection.state,
|
|
2238
2408
|
summary: `Prepared privacy policy update for ${selectedStatements.length} statement(s). No API write was made because apply=true was not set.`,
|
|
@@ -2247,9 +2417,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2247
2417
|
};
|
|
2248
2418
|
}
|
|
2249
2419
|
|
|
2250
|
-
const policyTarget = privacyPolicyTargetForApp(
|
|
2420
|
+
const policyTarget = privacyPolicyTargetForApp(
|
|
2421
|
+
appId,
|
|
2422
|
+
serviceName,
|
|
2423
|
+
selection.record,
|
|
2424
|
+
selection.policy,
|
|
2425
|
+
);
|
|
2251
2426
|
const pendingConfig = await client.requestApi(
|
|
2252
|
-
|
|
2427
|
+
buildPrivacyApiPath(
|
|
2428
|
+
appId,
|
|
2429
|
+
serviceName,
|
|
2430
|
+
`/environments/${encodeURIComponent(selection.environmentId)}/policy`,
|
|
2431
|
+
),
|
|
2253
2432
|
{
|
|
2254
2433
|
method: 'PUT',
|
|
2255
2434
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
@@ -2277,7 +2456,11 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2277
2456
|
let approvedConfig = null;
|
|
2278
2457
|
if (approve) {
|
|
2279
2458
|
approvedConfig = await client.requestApi(
|
|
2280
|
-
|
|
2459
|
+
buildPrivacyApiPath(
|
|
2460
|
+
appId,
|
|
2461
|
+
serviceName,
|
|
2462
|
+
`/environments/${encodeURIComponent(selection.environmentId)}/pending/approve`,
|
|
2463
|
+
),
|
|
2281
2464
|
{
|
|
2282
2465
|
method: 'POST',
|
|
2283
2466
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
@@ -2288,6 +2471,7 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2288
2471
|
return {
|
|
2289
2472
|
status: approve ? 'approved' : 'pending',
|
|
2290
2473
|
appId,
|
|
2474
|
+
serviceName: serviceName ?? null,
|
|
2291
2475
|
environment: selection.environmentId,
|
|
2292
2476
|
previousState: selection.state,
|
|
2293
2477
|
summary: approve
|
|
@@ -2316,8 +2500,9 @@ async function approvePrivacyPolicy(input, config, client) {
|
|
|
2316
2500
|
throw rpcError(-32603, 'This MCP client cannot approve app privacy policies');
|
|
2317
2501
|
}
|
|
2318
2502
|
|
|
2503
|
+
const serviceName = optionalNonEmptyString(input.serviceName);
|
|
2319
2504
|
const requestedEnvironment = optionalNonEmptyString(input.environment);
|
|
2320
|
-
const privacyConfig = await client.requestApi(
|
|
2505
|
+
const privacyConfig = await client.requestApi(buildPrivacyApiPath(appId, serviceName), {
|
|
2321
2506
|
method: 'GET',
|
|
2322
2507
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
2323
2508
|
});
|
|
@@ -2326,13 +2511,25 @@ async function approvePrivacyPolicy(input, config, client) {
|
|
|
2326
2511
|
return {
|
|
2327
2512
|
status: 'not_found',
|
|
2328
2513
|
appId,
|
|
2514
|
+
serviceName: serviceName ?? null,
|
|
2329
2515
|
environment: requestedEnvironment ?? null,
|
|
2330
2516
|
summary: 'No pending privacy policy exists for the requested app/environment.',
|
|
2331
|
-
ui: buildPrivacyStudioUi(
|
|
2517
|
+
ui: buildPrivacyStudioUi(
|
|
2518
|
+
config,
|
|
2519
|
+
appId,
|
|
2520
|
+
requestedEnvironment,
|
|
2521
|
+
'',
|
|
2522
|
+
input.openUi === true,
|
|
2523
|
+
serviceName,
|
|
2524
|
+
),
|
|
2332
2525
|
};
|
|
2333
2526
|
}
|
|
2334
2527
|
const approvedConfig = await client.requestApi(
|
|
2335
|
-
|
|
2528
|
+
buildPrivacyApiPath(
|
|
2529
|
+
appId,
|
|
2530
|
+
serviceName,
|
|
2531
|
+
`/environments/${encodeURIComponent(selection.environmentId)}/pending/approve`,
|
|
2532
|
+
),
|
|
2336
2533
|
{
|
|
2337
2534
|
method: 'POST',
|
|
2338
2535
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
@@ -2341,9 +2538,17 @@ async function approvePrivacyPolicy(input, config, client) {
|
|
|
2341
2538
|
return {
|
|
2342
2539
|
status: 'approved',
|
|
2343
2540
|
appId,
|
|
2541
|
+
serviceName: serviceName ?? null,
|
|
2344
2542
|
environment: selection.environmentId,
|
|
2345
2543
|
summary: 'Approved pending privacy policy through the Repro API validation path.',
|
|
2346
|
-
ui: buildPrivacyStudioUi(
|
|
2544
|
+
ui: buildPrivacyStudioUi(
|
|
2545
|
+
config,
|
|
2546
|
+
appId,
|
|
2547
|
+
selection.environmentId,
|
|
2548
|
+
'',
|
|
2549
|
+
input.openUi === true,
|
|
2550
|
+
serviceName,
|
|
2551
|
+
),
|
|
2347
2552
|
api: {
|
|
2348
2553
|
approvedEnvironmentCount: Array.isArray(approvedConfig?.environments) ? approvedConfig.environments.length : undefined,
|
|
2349
2554
|
},
|
|
@@ -2451,7 +2656,15 @@ function cleanupExpiredPrivacyBlockerSets(now = Date.now()) {
|
|
|
2451
2656
|
}
|
|
2452
2657
|
}
|
|
2453
2658
|
|
|
2454
|
-
function rememberPrivacyBlockerSet({
|
|
2659
|
+
function rememberPrivacyBlockerSet({
|
|
2660
|
+
tenantId,
|
|
2661
|
+
appId,
|
|
2662
|
+
serviceName,
|
|
2663
|
+
environment,
|
|
2664
|
+
state,
|
|
2665
|
+
query,
|
|
2666
|
+
statements,
|
|
2667
|
+
}) {
|
|
2455
2668
|
cleanupExpiredPrivacyBlockerSets();
|
|
2456
2669
|
const statementSids = mergeUniqueStrings(
|
|
2457
2670
|
(statements ?? []).map((statement) => String(statement?.sid ?? '')).filter(Boolean),
|
|
@@ -2461,6 +2674,7 @@ function rememberPrivacyBlockerSet({ tenantId, appId, environment, state, query,
|
|
|
2461
2674
|
v: 1,
|
|
2462
2675
|
tenantId: tenantId || null,
|
|
2463
2676
|
appId,
|
|
2677
|
+
serviceName: serviceName || null,
|
|
2464
2678
|
environment,
|
|
2465
2679
|
state,
|
|
2466
2680
|
queryHash: crypto.createHash('sha256').update(query || '').digest('hex').slice(0, 12),
|
|
@@ -2475,6 +2689,7 @@ function rememberPrivacyBlockerSet({ tenantId, appId, environment, state, query,
|
|
|
2475
2689
|
id,
|
|
2476
2690
|
tenantId: payload.tenantId,
|
|
2477
2691
|
appId: payload.appId,
|
|
2692
|
+
serviceName: payload.serviceName,
|
|
2478
2693
|
environment: payload.environment,
|
|
2479
2694
|
state: payload.state,
|
|
2480
2695
|
query: query || null,
|
|
@@ -2519,6 +2734,9 @@ function resolvePrivacyBlockerSetSelection(input, expected) {
|
|
|
2519
2734
|
}
|
|
2520
2735
|
const mismatches = [];
|
|
2521
2736
|
if (record.appId !== expected.appId) mismatches.push(`appId ${record.appId}`);
|
|
2737
|
+
if ((record.serviceName || null) !== (expected.serviceName || null)) {
|
|
2738
|
+
mismatches.push(`serviceName ${record.serviceName || '(app-wide)'}`);
|
|
2739
|
+
}
|
|
2522
2740
|
if (record.environment !== expected.environment) mismatches.push(`environment ${record.environment}`);
|
|
2523
2741
|
if (record.state !== expected.state) mismatches.push(`state ${record.state}`);
|
|
2524
2742
|
if (record.tenantId && expected.tenantId && record.tenantId !== expected.tenantId) {
|
|
@@ -2944,15 +3162,21 @@ function mergeUniqueStrings(values) {
|
|
|
2944
3162
|
return output;
|
|
2945
3163
|
}
|
|
2946
3164
|
|
|
2947
|
-
function privacyPolicyTargetForApp(appId, record, policy) {
|
|
3165
|
+
function privacyPolicyTargetForApp(appId, serviceName, record, policy) {
|
|
2948
3166
|
const target =
|
|
2949
3167
|
record?.proposal?.target ??
|
|
2950
3168
|
policy?.sourceProposal?.target ??
|
|
2951
3169
|
{};
|
|
2952
|
-
|
|
3170
|
+
const result = {
|
|
2953
3171
|
...target,
|
|
2954
3172
|
appId,
|
|
2955
3173
|
};
|
|
3174
|
+
if (serviceName) {
|
|
3175
|
+
result.serviceName = serviceName;
|
|
3176
|
+
} else {
|
|
3177
|
+
delete result.serviceName;
|
|
3178
|
+
}
|
|
3179
|
+
return result;
|
|
2956
3180
|
}
|
|
2957
3181
|
|
|
2958
3182
|
function cloneJson(value) {
|
|
@@ -3195,7 +3419,14 @@ function buildPolicyBlockerSummaries(statements, config, appId, environment, opt
|
|
|
3195
3419
|
const descriptor = describePolicyStatement(statement);
|
|
3196
3420
|
const review = statement?.meta?.review ?? {};
|
|
3197
3421
|
const query = descriptor.codeAnchor || descriptor.label || statement?.when?.selector || '';
|
|
3198
|
-
const ui = buildPrivacyStudioUi(
|
|
3422
|
+
const ui = buildPrivacyStudioUi(
|
|
3423
|
+
config,
|
|
3424
|
+
appId,
|
|
3425
|
+
environment,
|
|
3426
|
+
query,
|
|
3427
|
+
false,
|
|
3428
|
+
options.serviceName,
|
|
3429
|
+
);
|
|
3199
3430
|
const blocker = {
|
|
3200
3431
|
label: descriptor.label,
|
|
3201
3432
|
codeAnchor: descriptor.codeAnchor,
|
|
@@ -3438,7 +3669,22 @@ function summarizeRawTextHints(policy, query) {
|
|
|
3438
3669
|
}));
|
|
3439
3670
|
}
|
|
3440
3671
|
|
|
3441
|
-
function
|
|
3672
|
+
function buildPrivacyApiPath(appId, serviceName, suffix = '') {
|
|
3673
|
+
const path = `v1/apps/${encodeURIComponent(appId)}/privacy${suffix}`;
|
|
3674
|
+
if (!serviceName) {
|
|
3675
|
+
return path;
|
|
3676
|
+
}
|
|
3677
|
+
return `${path}?serviceName=${encodeURIComponent(serviceName)}`;
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
function buildPrivacyStudioUi(
|
|
3681
|
+
config,
|
|
3682
|
+
appId,
|
|
3683
|
+
environment,
|
|
3684
|
+
query,
|
|
3685
|
+
openRequested,
|
|
3686
|
+
serviceName,
|
|
3687
|
+
) {
|
|
3442
3688
|
const baseUrl = config.webBaseUrl || deriveWebBaseUrl(config.apiBaseUrl);
|
|
3443
3689
|
if (!baseUrl) {
|
|
3444
3690
|
return {
|
|
@@ -3451,6 +3697,9 @@ function buildPrivacyStudioUi(config, appId, environment, query, openRequested)
|
|
|
3451
3697
|
|
|
3452
3698
|
const url = new URL('/admin/privacy', baseUrl);
|
|
3453
3699
|
url.searchParams.set('service', appId);
|
|
3700
|
+
if (serviceName) {
|
|
3701
|
+
url.searchParams.set('serviceName', serviceName);
|
|
3702
|
+
}
|
|
3454
3703
|
if (environment) {
|
|
3455
3704
|
url.searchParams.set('env', environment);
|
|
3456
3705
|
}
|