@transcend-io/mcp-server-consent 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/scopes-DYOovdFV.mjs +1311 -0
- package/dist/scopes-DYOovdFV.mjs.map +1 -0
- package/package.json +17 -5
- package/dist/scopes-UQvBqSH8.mjs +0 -1287
- package/dist/scopes-UQvBqSH8.mjs.map +0 -1
package/dist/scopes-UQvBqSH8.mjs
DELETED
|
@@ -1,1287 +0,0 @@
|
|
|
1
|
-
import { EmptySchema, OffsetPaginationSchema, createListResult, createToolResult, defineTool, z } from "@transcend-io/mcp-server-base";
|
|
2
|
-
import { AirgapBundleAnalyticsBinInterval, AirgapBundleAnalyticsDimension, AirgapBundleAnalyticsMetric, ConsentManagerAnalyticsDataSource, ConsentManagerMetricBin, ConsentTrackerStatus, ConsentTrackerType, CookieOrderField, DataFlowOrderField, DataFlowScope, OrderDirection, ScopeName, TriageAction } from "@transcend-io/privacy-types";
|
|
3
|
-
import { AIRGAP_BUNDLE_AGGREGATE_ANALYTICS, AIRGAP_BUNDLE_TIMESERIES_ANALYTICS, CONSENT_MANAGER_ANALYTICS_DATA, COOKIES, COOKIE_STATS, DATA_FLOWS, EXPERIENCES, FETCH_CONSENT_MANAGER, FETCH_CONSENT_MANAGER_ID, PURPOSES, UPDATE_DATA_FLOWS, UPDATE_OR_CREATE_COOKIES } from "@transcend-io/sdk";
|
|
4
|
-
//#region src/resolveAirgapBundleId.ts
|
|
5
|
-
const bundleIdCache = /* @__PURE__ */ new WeakMap();
|
|
6
|
-
/**
|
|
7
|
-
* Lazily resolve the airgap bundle ID from the API key.
|
|
8
|
-
* Caches the result per GraphQL client instance so subsequent
|
|
9
|
-
* calls return instantly without an extra network request.
|
|
10
|
-
*/
|
|
11
|
-
async function resolveAirgapBundleId(graphql) {
|
|
12
|
-
const cached = bundleIdCache.get(graphql);
|
|
13
|
-
if (cached) return cached;
|
|
14
|
-
const id = (await graphql.makeRequest(FETCH_CONSENT_MANAGER_ID, {})).consentManager.consentManager.id;
|
|
15
|
-
bundleIdCache.set(graphql, id);
|
|
16
|
-
return id;
|
|
17
|
-
}
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/tools/consent_bulk_triage.ts
|
|
20
|
-
const BulkTriageItemSchema = z.object({
|
|
21
|
-
type: z.nativeEnum(ConsentTrackerType).describe("Item type"),
|
|
22
|
-
id: z.string().describe("Item ID (for data flows) or cookie name (for cookies)"),
|
|
23
|
-
action: z.nativeEnum(TriageAction).describe("Action to take: APPROVE or JUNK"),
|
|
24
|
-
trackingPurposes: z.array(z.string()).optional().describe("Tracking purposes to assign (required when approving)"),
|
|
25
|
-
service: z.string().optional().describe("Service name to assign")
|
|
26
|
-
});
|
|
27
|
-
const BulkTriageSchema = z.object({ items: z.array(BulkTriageItemSchema).min(1).describe("Items to triage") });
|
|
28
|
-
function createConsentBulkTriageTool(clients) {
|
|
29
|
-
return defineTool({
|
|
30
|
-
name: "consent_bulk_triage",
|
|
31
|
-
description: "Bulk triage action: approve or junk multiple cookies and data flows in a single call. For cookies, APPROVE sets status=LIVE; JUNK sets isJunk=true. For data flows, same behavior. Optionally assign tracking purposes and service when approving.",
|
|
32
|
-
category: "Consent Management",
|
|
33
|
-
readOnly: false,
|
|
34
|
-
annotations: {
|
|
35
|
-
readOnlyHint: false,
|
|
36
|
-
destructiveHint: true,
|
|
37
|
-
idempotentHint: false
|
|
38
|
-
},
|
|
39
|
-
zodSchema: BulkTriageSchema,
|
|
40
|
-
handler: async ({ items }) => {
|
|
41
|
-
const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
|
|
42
|
-
const cookieItems = items.filter((i) => i.type === "cookie");
|
|
43
|
-
const dfItems = items.filter((i) => i.type === "data_flow");
|
|
44
|
-
const results = {
|
|
45
|
-
cookies: [],
|
|
46
|
-
dataFlows: []
|
|
47
|
-
};
|
|
48
|
-
if (cookieItems.length > 0) {
|
|
49
|
-
const cookieInputs = cookieItems.map((item) => ({
|
|
50
|
-
name: item.id,
|
|
51
|
-
...item.action === "APPROVE" ? {
|
|
52
|
-
status: ConsentTrackerStatus.Live,
|
|
53
|
-
isJunk: false
|
|
54
|
-
} : {
|
|
55
|
-
status: ConsentTrackerStatus.Live,
|
|
56
|
-
isJunk: true
|
|
57
|
-
},
|
|
58
|
-
...item.trackingPurposes ? { trackingPurposes: item.trackingPurposes } : {},
|
|
59
|
-
...item.service ? { service: item.service } : {}
|
|
60
|
-
}));
|
|
61
|
-
await clients.graphql.makeRequest(UPDATE_OR_CREATE_COOKIES, {
|
|
62
|
-
airgapBundleId,
|
|
63
|
-
cookies: cookieInputs
|
|
64
|
-
});
|
|
65
|
-
results.cookies = cookieInputs.map((c) => ({
|
|
66
|
-
name: c.name,
|
|
67
|
-
action: c.isJunk ? "JUNKED" : "APPROVED",
|
|
68
|
-
status: c.status || "LIVE"
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
|
-
if (dfItems.length > 0) {
|
|
72
|
-
const dfInputs = dfItems.map((item) => ({
|
|
73
|
-
id: item.id,
|
|
74
|
-
...item.action === "APPROVE" ? {
|
|
75
|
-
status: ConsentTrackerStatus.Live,
|
|
76
|
-
isJunk: false
|
|
77
|
-
} : {
|
|
78
|
-
status: ConsentTrackerStatus.Live,
|
|
79
|
-
isJunk: true
|
|
80
|
-
},
|
|
81
|
-
...item.trackingPurposes ? { purposeIds: item.trackingPurposes } : {},
|
|
82
|
-
...item.service ? { service: item.service } : {}
|
|
83
|
-
}));
|
|
84
|
-
results.dataFlows = (await clients.graphql.makeRequest(UPDATE_DATA_FLOWS, {
|
|
85
|
-
airgapBundleId,
|
|
86
|
-
dataFlows: dfInputs
|
|
87
|
-
})).updateDataFlows.dataFlows.map((df) => ({
|
|
88
|
-
id: df.id,
|
|
89
|
-
action: df.isJunk ? "JUNKED" : "APPROVED",
|
|
90
|
-
status: df.status
|
|
91
|
-
}));
|
|
92
|
-
}
|
|
93
|
-
return createToolResult(true, {
|
|
94
|
-
totalProcessed: cookieItems.length + dfItems.length,
|
|
95
|
-
...results
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
//#endregion
|
|
101
|
-
//#region src/analyticsDateRange.ts
|
|
102
|
-
/**
|
|
103
|
-
* Resolve a date range from explicit ISO timestamps or a lookback window.
|
|
104
|
-
*/
|
|
105
|
-
function resolveAnalyticsDateRange(args) {
|
|
106
|
-
const endDate = args.end ? new Date(args.end) : /* @__PURE__ */ new Date();
|
|
107
|
-
const lookbackDays = args.days ?? 7;
|
|
108
|
-
const startDate = args.start ? new Date(args.start) : /* @__PURE__ */ new Date(endDate.getTime() - lookbackDays * 24 * 60 * 60 * 1e3);
|
|
109
|
-
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) throw new Error("Invalid start or end date");
|
|
110
|
-
if (startDate > endDate) throw new Error("Start date must be before end date");
|
|
111
|
-
return {
|
|
112
|
-
startEpoch: Math.floor(startDate.getTime() / 1e3),
|
|
113
|
-
endEpoch: Math.floor(endDate.getTime() / 1e3),
|
|
114
|
-
startIso: startDate.toISOString(),
|
|
115
|
-
endIso: endDate.toISOString()
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
//#endregion
|
|
119
|
-
//#region src/normalizeAnalyticsMetric.ts
|
|
120
|
-
const VALID_METRICS = new Set(Object.values(AirgapBundleAnalyticsMetric));
|
|
121
|
-
/** Common agent/API guesses mapped to GraphQL AnalyticsEvent values */
|
|
122
|
-
const ANALYTICS_METRIC_ALIASES = {
|
|
123
|
-
PAGE_VIEW: AirgapBundleAnalyticsMetric.PageViews,
|
|
124
|
-
CONSENT_SESSION: AirgapBundleAnalyticsMetric.SiteSessions,
|
|
125
|
-
CONSENT_SESSIONS: AirgapBundleAnalyticsMetric.SiteSessions
|
|
126
|
-
};
|
|
127
|
-
/**
|
|
128
|
-
* Normalize metric input, accepting common aliases (e.g. PAGE_VIEW → PAGE_VIEWS).
|
|
129
|
-
*/
|
|
130
|
-
function normalizeAnalyticsMetric(metric) {
|
|
131
|
-
const upper = metric.toUpperCase();
|
|
132
|
-
if (VALID_METRICS.has(upper)) return upper;
|
|
133
|
-
return ANALYTICS_METRIC_ALIASES[upper] ?? upper;
|
|
134
|
-
}
|
|
135
|
-
const airgapBundleAnalyticsMetricSchema = z.preprocess((value) => typeof value === "string" ? normalizeAnalyticsMetric(value) : value, z.nativeEnum(AirgapBundleAnalyticsMetric));
|
|
136
|
-
//#endregion
|
|
137
|
-
//#region src/tools/consent_get_aggregate_analytics.ts
|
|
138
|
-
const GetAggregateAnalyticsSchema = z.object({
|
|
139
|
-
metric: airgapBundleAnalyticsMetricSchema.describe("Analytics metric to query. CONSENT_CHANGED for opt-in/out counts; SITE_SESSIONS or PAGE_VIEWS for traffic totals."),
|
|
140
|
-
start: z.string().optional().describe("Start datetime (ISO 8601). Defaults to `days` lookback from end."),
|
|
141
|
-
end: z.string().optional().describe("End datetime (ISO 8601). Defaults to now."),
|
|
142
|
-
days: z.coerce.number().min(1).max(365).optional().describe("Lookback window in days when start is omitted (default: 7)."),
|
|
143
|
-
include_dimensions: z.array(z.nativeEnum(AirgapBundleAnalyticsDimension)).optional().describe("Dimension breakdowns (e.g. NEW_VALUE, REGIME, PURPOSE). Recommended for CONSENT_CHANGED.")
|
|
144
|
-
});
|
|
145
|
-
function createConsentGetAggregateAnalyticsTool(clients) {
|
|
146
|
-
return defineTool({
|
|
147
|
-
name: "consent_get_aggregate_analytics",
|
|
148
|
-
description: "Query aggregate consent analytics via airgapBundleAggregateAnalytics. Use CONSENT_CHANGED with NEW_VALUE/REGIME/PURPOSE for opt-in/out counts; SITE_SESSIONS or PAGE_VIEWS for total traffic. Requires ViewConsentManager API key scope.",
|
|
149
|
-
category: "Consent Management",
|
|
150
|
-
readOnly: true,
|
|
151
|
-
annotations: {
|
|
152
|
-
readOnlyHint: true,
|
|
153
|
-
destructiveHint: false,
|
|
154
|
-
idempotentHint: true
|
|
155
|
-
},
|
|
156
|
-
zodSchema: GetAggregateAnalyticsSchema,
|
|
157
|
-
handler: async ({ metric, start, end, days, include_dimensions }) => {
|
|
158
|
-
const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
|
|
159
|
-
const range = resolveAnalyticsDateRange({
|
|
160
|
-
start,
|
|
161
|
-
end,
|
|
162
|
-
days
|
|
163
|
-
});
|
|
164
|
-
const items = (await clients.graphql.makeRequest(AIRGAP_BUNDLE_AGGREGATE_ANALYTICS, {
|
|
165
|
-
id: airgapBundleId,
|
|
166
|
-
input: {
|
|
167
|
-
metric,
|
|
168
|
-
start: range.startEpoch,
|
|
169
|
-
end: range.endEpoch,
|
|
170
|
-
...include_dimensions?.length ? { includeDimensions: include_dimensions } : {}
|
|
171
|
-
}
|
|
172
|
-
})).airgapBundleAggregateAnalytics.items;
|
|
173
|
-
return createToolResult(true, {
|
|
174
|
-
airgapBundleId,
|
|
175
|
-
metric,
|
|
176
|
-
period: {
|
|
177
|
-
start: range.startIso,
|
|
178
|
-
end: range.endIso,
|
|
179
|
-
startEpoch: range.startEpoch,
|
|
180
|
-
endEpoch: range.endEpoch
|
|
181
|
-
},
|
|
182
|
-
items,
|
|
183
|
-
totalRows: items.length
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
//#endregion
|
|
189
|
-
//#region src/tools/consent_get_analytics_data.ts
|
|
190
|
-
const GetAnalyticsDataSchema = z.object({
|
|
191
|
-
data_source: z.nativeEnum(ConsentManagerAnalyticsDataSource).describe("analyticsData source: PRIVACY_SIGNAL_TIMESERIES (DNT/GPC), CONSENT_CHANGES_TIMESERIES (opt-in/out), or CONSENT_SESSIONS_BY_REGIME."),
|
|
192
|
-
start: z.string().optional().describe("Start datetime (ISO 8601). Defaults to `days` lookback from end."),
|
|
193
|
-
end: z.string().optional().describe("End datetime (ISO 8601). Defaults to now."),
|
|
194
|
-
days: z.coerce.number().min(1).max(365).optional().describe("Lookback window in days when start is omitted (default: 7)."),
|
|
195
|
-
bin: z.nativeEnum(ConsentManagerMetricBin).optional().default(ConsentManagerMetricBin.Daily).describe("Time bin size for analyticsData (1h or 1d, default: 1d).")
|
|
196
|
-
});
|
|
197
|
-
function createConsentGetAnalyticsDataTool(clients) {
|
|
198
|
-
return defineTool({
|
|
199
|
-
name: "consent_get_analytics_data",
|
|
200
|
-
description: "Query consent metrics via the analyticsData GraphQL query. Returns timeseries for privacy signals (DNT/GPC), consent changes (opt-in/out), or sessions by regime. Requires ViewConsentManager API key scope.",
|
|
201
|
-
category: "Consent Management",
|
|
202
|
-
readOnly: true,
|
|
203
|
-
annotations: {
|
|
204
|
-
readOnlyHint: true,
|
|
205
|
-
destructiveHint: false,
|
|
206
|
-
idempotentHint: true
|
|
207
|
-
},
|
|
208
|
-
zodSchema: GetAnalyticsDataSchema,
|
|
209
|
-
handler: async ({ data_source, start, end, days, bin }) => {
|
|
210
|
-
const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
|
|
211
|
-
const range = resolveAnalyticsDateRange({
|
|
212
|
-
start,
|
|
213
|
-
end,
|
|
214
|
-
days
|
|
215
|
-
});
|
|
216
|
-
const series = (await clients.graphql.makeRequest(CONSENT_MANAGER_ANALYTICS_DATA, { input: {
|
|
217
|
-
dataSource: data_source,
|
|
218
|
-
startDate: range.startIso,
|
|
219
|
-
endDate: range.endIso,
|
|
220
|
-
forceRefetch: true,
|
|
221
|
-
airgapBundleId,
|
|
222
|
-
binInterval: bin,
|
|
223
|
-
smoothTimeseries: false
|
|
224
|
-
} })).analyticsData.series;
|
|
225
|
-
return createToolResult(true, {
|
|
226
|
-
airgapBundleId,
|
|
227
|
-
dataSource: data_source,
|
|
228
|
-
binInterval: bin,
|
|
229
|
-
period: {
|
|
230
|
-
start: range.startIso,
|
|
231
|
-
end: range.endIso
|
|
232
|
-
},
|
|
233
|
-
series
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
//#endregion
|
|
239
|
-
//#region src/getDataFlowCount.ts
|
|
240
|
-
/**
|
|
241
|
-
* Fetch `dataFlows.totalCount` without paging nodes.
|
|
242
|
-
*
|
|
243
|
-
* Uses `first: 1` so the payload stays small. The list API hides CSP rows
|
|
244
|
-
* (same as the Consent Manager table), so these counts match what users see.
|
|
245
|
-
*/
|
|
246
|
-
async function getDataFlowCount(graphql, airgapBundleId, filterBy) {
|
|
247
|
-
return (await graphql.makeRequest(DATA_FLOWS, {
|
|
248
|
-
input: { airgapBundleId },
|
|
249
|
-
first: 1,
|
|
250
|
-
offset: 0,
|
|
251
|
-
filterBy
|
|
252
|
-
})).dataFlows.totalCount;
|
|
253
|
-
}
|
|
254
|
-
//#endregion
|
|
255
|
-
//#region src/tools/consent_get_inventory_stats.ts
|
|
256
|
-
const GetInventoryStatsSchema = z.object({});
|
|
257
|
-
function createConsentGetInventoryStatsTool(clients) {
|
|
258
|
-
return defineTool({
|
|
259
|
-
name: "consent_get_inventory_stats",
|
|
260
|
-
description: "Get cookie and data-flow inventory triage counts: live (approved), needs review, and junk. Counts match the Consent Manager tables and the default consent_list_cookies / consent_list_data_flows filters (CSP data flows are omitted, same as the UI). This is inventory status, not consent analytics — use consent_get_aggregate_analytics or consent_get_timeseries_analytics for opt-in/out and signal metrics.",
|
|
261
|
-
category: "Consent Management",
|
|
262
|
-
readOnly: true,
|
|
263
|
-
annotations: {
|
|
264
|
-
readOnlyHint: true,
|
|
265
|
-
destructiveHint: false,
|
|
266
|
-
idempotentHint: true
|
|
267
|
-
},
|
|
268
|
-
zodSchema: GetInventoryStatsSchema,
|
|
269
|
-
handler: async () => {
|
|
270
|
-
const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
|
|
271
|
-
const [cookieData, needReviewCount, liveCount, junkCount] = await Promise.all([
|
|
272
|
-
clients.graphql.makeRequest(COOKIE_STATS, { input: { airgapBundleId } }),
|
|
273
|
-
getDataFlowCount(clients.graphql, airgapBundleId, { status: ConsentTrackerStatus.NeedsReview }),
|
|
274
|
-
getDataFlowCount(clients.graphql, airgapBundleId, {
|
|
275
|
-
status: ConsentTrackerStatus.Live,
|
|
276
|
-
isJunk: false
|
|
277
|
-
}),
|
|
278
|
-
getDataFlowCount(clients.graphql, airgapBundleId, {
|
|
279
|
-
status: ConsentTrackerStatus.Live,
|
|
280
|
-
isJunk: true
|
|
281
|
-
})
|
|
282
|
-
]);
|
|
283
|
-
return createToolResult(true, {
|
|
284
|
-
cookies: cookieData.cookieStats,
|
|
285
|
-
dataFlows: {
|
|
286
|
-
liveCount,
|
|
287
|
-
needReviewCount,
|
|
288
|
-
junkCount
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
//#endregion
|
|
295
|
-
//#region src/tools/consent_get_preferences.ts
|
|
296
|
-
const GetPreferencesSchema = z.object({
|
|
297
|
-
identifier: z.string().describe("User identifier (e.g., email, user ID)"),
|
|
298
|
-
partition: z.string().optional().describe("Partition/organization context (optional)")
|
|
299
|
-
});
|
|
300
|
-
function createConsentGetPreferencesTool(clients) {
|
|
301
|
-
const { rest } = clients;
|
|
302
|
-
return defineTool({
|
|
303
|
-
name: "consent_get_preferences",
|
|
304
|
-
description: "Get consent preferences for a specific user/identifier",
|
|
305
|
-
category: "Consent Management",
|
|
306
|
-
readOnly: true,
|
|
307
|
-
annotations: {
|
|
308
|
-
readOnlyHint: true,
|
|
309
|
-
destructiveHint: false,
|
|
310
|
-
idempotentHint: true
|
|
311
|
-
},
|
|
312
|
-
requireSombra: true,
|
|
313
|
-
zodSchema: GetPreferencesSchema,
|
|
314
|
-
handler: async ({ identifier, partition }) => {
|
|
315
|
-
const result = await rest.getConsentPreferences(identifier, partition);
|
|
316
|
-
if (!result) return createToolResult(true, {
|
|
317
|
-
found: false,
|
|
318
|
-
message: "No consent preferences found for this identifier"
|
|
319
|
-
});
|
|
320
|
-
return createToolResult(true, {
|
|
321
|
-
found: true,
|
|
322
|
-
preferences: result
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
//#endregion
|
|
328
|
-
//#region src/tools/consent_get_timeseries_analytics.ts
|
|
329
|
-
const GetTimeseriesAnalyticsSchema = z.object({
|
|
330
|
-
metric: airgapBundleAnalyticsMetricSchema.describe("Analytics metric to query. PAGE_VIEWS for daily page-view volume; SITE_SESSIONS for sessions; SIGNAL_DETECTED for GPC/DNT signal counts over time."),
|
|
331
|
-
start: z.string().optional().describe("Start datetime (ISO 8601). Defaults to `days` lookback from end."),
|
|
332
|
-
end: z.string().optional().describe("End datetime (ISO 8601). Defaults to now."),
|
|
333
|
-
days: z.coerce.number().min(1).max(365).optional().describe("Lookback window in days when start is omitted (default: 7)."),
|
|
334
|
-
bin_interval: z.nativeEnum(AirgapBundleAnalyticsBinInterval).optional().default(AirgapBundleAnalyticsBinInterval.Hourly).describe("Time bin size: 1m, 1h, or 1d (default: 1h).")
|
|
335
|
-
});
|
|
336
|
-
function createConsentGetTimeseriesAnalyticsTool(clients) {
|
|
337
|
-
return defineTool({
|
|
338
|
-
name: "consent_get_timeseries_analytics",
|
|
339
|
-
description: "Query timeseries consent analytics via airgapBundleTimeseriesAnalytics. Use PAGE_VIEWS or SITE_SESSIONS for traffic volume; SIGNAL_DETECTED for privacy signal counts. Requires ViewConsentManager API key scope.",
|
|
340
|
-
category: "Consent Management",
|
|
341
|
-
readOnly: true,
|
|
342
|
-
annotations: {
|
|
343
|
-
readOnlyHint: true,
|
|
344
|
-
destructiveHint: false,
|
|
345
|
-
idempotentHint: true
|
|
346
|
-
},
|
|
347
|
-
zodSchema: GetTimeseriesAnalyticsSchema,
|
|
348
|
-
handler: async ({ metric, start, end, days, bin_interval }) => {
|
|
349
|
-
const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
|
|
350
|
-
const range = resolveAnalyticsDateRange({
|
|
351
|
-
start,
|
|
352
|
-
end,
|
|
353
|
-
days
|
|
354
|
-
});
|
|
355
|
-
const items = (await clients.graphql.makeRequest(AIRGAP_BUNDLE_TIMESERIES_ANALYTICS, {
|
|
356
|
-
id: airgapBundleId,
|
|
357
|
-
input: {
|
|
358
|
-
metric,
|
|
359
|
-
start: range.startEpoch,
|
|
360
|
-
end: range.endEpoch,
|
|
361
|
-
binInterval: bin_interval
|
|
362
|
-
}
|
|
363
|
-
})).airgapBundleTimeseriesAnalytics.items;
|
|
364
|
-
return createToolResult(true, {
|
|
365
|
-
airgapBundleId,
|
|
366
|
-
metric,
|
|
367
|
-
binInterval: bin_interval,
|
|
368
|
-
period: {
|
|
369
|
-
start: range.startIso,
|
|
370
|
-
end: range.endIso,
|
|
371
|
-
startEpoch: range.startEpoch,
|
|
372
|
-
endEpoch: range.endEpoch
|
|
373
|
-
},
|
|
374
|
-
items,
|
|
375
|
-
totalRows: items.length
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
//#endregion
|
|
381
|
-
//#region src/tools/consent_list_airgap_bundles.ts
|
|
382
|
-
const ListAirgapBundlesSchema = EmptySchema;
|
|
383
|
-
function createConsentListAirgapBundlesTool(clients) {
|
|
384
|
-
return defineTool({
|
|
385
|
-
name: "consent_list_airgap_bundles",
|
|
386
|
-
description: "Get the consent manager (airgap bundle) configured for your organization. Returns the bundle ID, URLs, configuration, and domains.",
|
|
387
|
-
category: "Consent Management",
|
|
388
|
-
readOnly: true,
|
|
389
|
-
annotations: {
|
|
390
|
-
readOnlyHint: true,
|
|
391
|
-
destructiveHint: false,
|
|
392
|
-
idempotentHint: true
|
|
393
|
-
},
|
|
394
|
-
zodSchema: ListAirgapBundlesSchema,
|
|
395
|
-
handler: async (_args) => {
|
|
396
|
-
return createToolResult(true, (await clients.graphql.makeRequest(FETCH_CONSENT_MANAGER, {})).consentManager.consentManager);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
//#endregion
|
|
401
|
-
//#region src/tools/consent_list_cookies.ts
|
|
402
|
-
const ListCookiesSchema = OffsetPaginationSchema.extend({
|
|
403
|
-
status: z.nativeEnum(ConsentTrackerStatus).describe("Filter by status: NEEDS_REVIEW (triage) or LIVE (approved)"),
|
|
404
|
-
isJunk: z.boolean().optional().describe("Filter by junk status"),
|
|
405
|
-
showZeroActivity: z.boolean().optional().describe("Include items with zero activity. Omit (default) so the NEEDS_REVIEW total matches consent_get_inventory_stats cookies.needReviewCount; set true for the full triage backlog including never-active cookies."),
|
|
406
|
-
text: z.string().optional().describe("Search text filter"),
|
|
407
|
-
service: z.string().optional().describe("Filter by service name"),
|
|
408
|
-
minOccurrences: z.number().min(0).optional().describe("Only return cookies with at least this many occurrences (traffic)"),
|
|
409
|
-
orderField: z.nativeEnum(CookieOrderField).optional().describe("Field to sort by (e.g. occurrences to rank by traffic)"),
|
|
410
|
-
orderDirection: z.nativeEnum(OrderDirection).optional().describe("Sort direction: ASC or DESC")
|
|
411
|
-
});
|
|
412
|
-
function createConsentListCookiesTool(clients) {
|
|
413
|
-
return defineTool({
|
|
414
|
-
name: "consent_list_cookies",
|
|
415
|
-
description: "List cookies in your consent manager. Requires a status filter: NEEDS_REVIEW for triage backlog, LIVE for approved cookies. Returns name, service, tracking purposes, activity (occurrences), junk status, and more. Sort by occurrences (orderField=occurrences, orderDirection=DESC) to surface top-traffic cookies, and use minOccurrences to filter low-traffic noise.",
|
|
416
|
-
category: "Consent Management",
|
|
417
|
-
readOnly: true,
|
|
418
|
-
annotations: {
|
|
419
|
-
readOnlyHint: true,
|
|
420
|
-
destructiveHint: false,
|
|
421
|
-
idempotentHint: true
|
|
422
|
-
},
|
|
423
|
-
zodSchema: ListCookiesSchema,
|
|
424
|
-
handler: async ({ first, offset, status, isJunk, showZeroActivity, text, service, minOccurrences, orderField, orderDirection }) => {
|
|
425
|
-
const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
|
|
426
|
-
const { nodes, totalCount } = (await clients.graphql.makeRequest(COOKIES, {
|
|
427
|
-
input: { airgapBundleId },
|
|
428
|
-
first,
|
|
429
|
-
offset,
|
|
430
|
-
filterBy: {
|
|
431
|
-
status,
|
|
432
|
-
...isJunk !== void 0 ? { isJunk } : {},
|
|
433
|
-
...showZeroActivity !== void 0 ? { showZeroActivity } : {},
|
|
434
|
-
...text ? { text } : {},
|
|
435
|
-
...service ? { service } : {},
|
|
436
|
-
...minOccurrences !== void 0 ? { minOccurrences } : {}
|
|
437
|
-
},
|
|
438
|
-
...orderField && orderDirection ? { orderBy: [{
|
|
439
|
-
field: orderField,
|
|
440
|
-
direction: orderDirection
|
|
441
|
-
}] } : {}
|
|
442
|
-
})).cookies;
|
|
443
|
-
return createListResult(nodes, {
|
|
444
|
-
totalCount,
|
|
445
|
-
hasNextPage: offset + nodes.length < totalCount
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
//#endregion
|
|
451
|
-
//#region src/tools/consent_list_data_flows.ts
|
|
452
|
-
const ListDataFlowsSchema = OffsetPaginationSchema.extend({
|
|
453
|
-
status: z.nativeEnum(ConsentTrackerStatus).describe("Filter by status: NEEDS_REVIEW (triage) or LIVE (approved)"),
|
|
454
|
-
isJunk: z.boolean().optional().describe("Filter by junk status"),
|
|
455
|
-
showZeroActivity: z.boolean().optional().describe("Include items with zero activity. Omit (default) so the NEEDS_REVIEW total matches consent_get_inventory_stats dataFlows.needReviewCount (the Consent Manager table). Set true for the full triage backlog including never-active flows."),
|
|
456
|
-
text: z.string().optional().describe("Search text filter"),
|
|
457
|
-
service: z.string().optional().describe("Filter by service name"),
|
|
458
|
-
unmappedOnly: z.boolean().optional().describe("Return only unmapped/orphaned flows with no associated service (catalog integration). Useful with status=LIVE to find approved flows that are not mapped to a service."),
|
|
459
|
-
type: z.nativeEnum(DataFlowScope).optional().describe("Filter by data flow scope type (e.g. HOST, PATH, REGEX, CSP)"),
|
|
460
|
-
minOccurrences: z.number().min(0).optional().describe("Only return flows with at least this many occurrences (traffic)"),
|
|
461
|
-
orderField: z.nativeEnum(DataFlowOrderField).optional().describe("Field to sort by"),
|
|
462
|
-
orderDirection: z.nativeEnum(OrderDirection).optional().describe("Sort direction: ASC or DESC")
|
|
463
|
-
});
|
|
464
|
-
function createConsentListDataFlowsTool(clients) {
|
|
465
|
-
return defineTool({
|
|
466
|
-
name: "consent_list_data_flows",
|
|
467
|
-
description: "List data flows (network requests) in your consent manager. Requires a status filter: NEEDS_REVIEW for triage backlog, LIVE for approved flows. Returns value (URL/host), service, tracking purposes, activity (occurrences), and more. Use unmappedOnly to find approved flows with no service, type to filter by scope (e.g. CSP), and minOccurrences to focus on high-traffic flows.",
|
|
468
|
-
category: "Consent Management",
|
|
469
|
-
readOnly: true,
|
|
470
|
-
annotations: {
|
|
471
|
-
readOnlyHint: true,
|
|
472
|
-
destructiveHint: false,
|
|
473
|
-
idempotentHint: true
|
|
474
|
-
},
|
|
475
|
-
zodSchema: ListDataFlowsSchema,
|
|
476
|
-
handler: async ({ first, offset, status, isJunk, showZeroActivity, text, service, unmappedOnly, type, minOccurrences, orderField, orderDirection }) => {
|
|
477
|
-
const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
|
|
478
|
-
const { nodes, totalCount } = (await clients.graphql.makeRequest(DATA_FLOWS, {
|
|
479
|
-
input: { airgapBundleId },
|
|
480
|
-
first,
|
|
481
|
-
offset,
|
|
482
|
-
filterBy: {
|
|
483
|
-
status,
|
|
484
|
-
...isJunk !== void 0 ? { isJunk } : {},
|
|
485
|
-
...showZeroActivity !== void 0 ? { showZeroActivity } : {},
|
|
486
|
-
...text ? { text } : {},
|
|
487
|
-
...unmappedOnly ? { service: "" } : service ? { service } : {},
|
|
488
|
-
...type ? { type } : {},
|
|
489
|
-
...minOccurrences !== void 0 ? { minOccurrences } : {}
|
|
490
|
-
},
|
|
491
|
-
...orderField && orderDirection ? { orderBy: [{
|
|
492
|
-
field: orderField,
|
|
493
|
-
direction: orderDirection
|
|
494
|
-
}] } : {}
|
|
495
|
-
})).dataFlows;
|
|
496
|
-
return createListResult(nodes, {
|
|
497
|
-
totalCount,
|
|
498
|
-
hasNextPage: offset + nodes.length < totalCount
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
//#endregion
|
|
504
|
-
//#region src/tools/consent_list_purposes.ts
|
|
505
|
-
const ListPurposesSchema = z.object({ limit: z.coerce.number().min(1).max(100).optional().default(50).describe("Maximum number of purposes to return (1-100, default 50).") });
|
|
506
|
-
function createConsentListPurposesTool(clients) {
|
|
507
|
-
return defineTool({
|
|
508
|
-
name: "consent_list_purposes",
|
|
509
|
-
description: "List all tracking purposes configured for consent management (max ~100 results).",
|
|
510
|
-
category: "Consent Management",
|
|
511
|
-
readOnly: true,
|
|
512
|
-
annotations: {
|
|
513
|
-
readOnlyHint: true,
|
|
514
|
-
destructiveHint: false,
|
|
515
|
-
idempotentHint: true
|
|
516
|
-
},
|
|
517
|
-
zodSchema: ListPurposesSchema,
|
|
518
|
-
handler: async ({ limit }) => {
|
|
519
|
-
const { nodes, totalCount } = (await clients.graphql.makeRequest(PURPOSES, { first: Math.min(limit, 100) })).purposes;
|
|
520
|
-
return createListResult(nodes, {
|
|
521
|
-
totalCount,
|
|
522
|
-
hasNextPage: nodes.length < totalCount
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
//#endregion
|
|
528
|
-
//#region src/tools/consent_list_regimes.ts
|
|
529
|
-
const ListRegimesSchema = z.object({
|
|
530
|
-
limit: z.coerce.number().min(1).max(100).optional().default(50).describe("Maximum number of regimes to return per page (1-100, default 50)."),
|
|
531
|
-
offset: z.coerce.number().min(0).optional().default(0).describe("Number of results to skip for pagination (default 0).")
|
|
532
|
-
});
|
|
533
|
-
function createConsentListRegimesTool(clients) {
|
|
534
|
-
return defineTool({
|
|
535
|
-
name: "consent_list_regimes",
|
|
536
|
-
description: "List all consent experiences (regional regimes) configured for your organization. Returns experience name, regions, purposes, opted-out purposes, and view state.",
|
|
537
|
-
category: "Consent Management",
|
|
538
|
-
readOnly: true,
|
|
539
|
-
annotations: {
|
|
540
|
-
readOnlyHint: true,
|
|
541
|
-
destructiveHint: false,
|
|
542
|
-
idempotentHint: true
|
|
543
|
-
},
|
|
544
|
-
zodSchema: ListRegimesSchema,
|
|
545
|
-
handler: async ({ limit, offset }) => {
|
|
546
|
-
const { totalCount, nodes } = (await clients.graphql.makeRequest(EXPERIENCES, {
|
|
547
|
-
first: limit,
|
|
548
|
-
offset
|
|
549
|
-
})).experiences;
|
|
550
|
-
return createListResult(nodes, {
|
|
551
|
-
totalCount,
|
|
552
|
-
hasNextPage: offset + nodes.length < totalCount
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
//#endregion
|
|
558
|
-
//#region src/tools/consent_set_preferences.ts
|
|
559
|
-
const PurposeConsentSchema = z.object({
|
|
560
|
-
purpose: z.string().describe("Purpose slug"),
|
|
561
|
-
enabled: z.boolean().describe("Whether consent is granted")
|
|
562
|
-
});
|
|
563
|
-
const SetPreferencesSchema = z.object({
|
|
564
|
-
identifier: z.string().optional().describe("User identifier"),
|
|
565
|
-
partition: z.string().describe("Partition/organization context"),
|
|
566
|
-
purposes: z.array(PurposeConsentSchema).describe("Array of purpose consent settings"),
|
|
567
|
-
confirmed: z.boolean().optional().describe("Whether consent was explicitly confirmed")
|
|
568
|
-
});
|
|
569
|
-
function createConsentSetPreferencesTool(clients) {
|
|
570
|
-
const { rest } = clients;
|
|
571
|
-
return defineTool({
|
|
572
|
-
name: "consent_set_preferences",
|
|
573
|
-
description: "Set consent preferences for a user (client-side sync)",
|
|
574
|
-
category: "Consent Management",
|
|
575
|
-
readOnly: false,
|
|
576
|
-
annotations: {
|
|
577
|
-
readOnlyHint: false,
|
|
578
|
-
destructiveHint: false,
|
|
579
|
-
idempotentHint: true
|
|
580
|
-
},
|
|
581
|
-
requireSombra: true,
|
|
582
|
-
zodSchema: SetPreferencesSchema,
|
|
583
|
-
handler: async ({ partition, identifier, purposes, confirmed }) => {
|
|
584
|
-
return createToolResult(true, {
|
|
585
|
-
...await rest.syncConsent({
|
|
586
|
-
partition,
|
|
587
|
-
identifier,
|
|
588
|
-
purposes: purposes.map((p) => ({
|
|
589
|
-
purpose: p.purpose,
|
|
590
|
-
enabled: p.enabled
|
|
591
|
-
})),
|
|
592
|
-
confirmed
|
|
593
|
-
}),
|
|
594
|
-
message: "Consent preferences synced successfully"
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
//#endregion
|
|
600
|
-
//#region src/tools/consent_update_cookies.ts
|
|
601
|
-
const UpdateCookieItemSchema = z.object({
|
|
602
|
-
name: z.string().describe("Cookie name (used as the identifier for upsert)"),
|
|
603
|
-
trackingPurposes: z.array(z.string()).optional().describe("Tracking purpose slugs (e.g., \"Advertising\", \"Analytics\")"),
|
|
604
|
-
description: z.string().optional().describe("Cookie description"),
|
|
605
|
-
service: z.string().optional().describe("Service/integration name"),
|
|
606
|
-
isJunk: z.boolean().optional().describe("Mark as junk"),
|
|
607
|
-
status: z.nativeEnum(ConsentTrackerStatus).optional().describe("Set status to LIVE (approve) or NEEDS_REVIEW")
|
|
608
|
-
});
|
|
609
|
-
const UpdateCookiesSchema = z.object({ cookies: z.array(UpdateCookieItemSchema).min(1).describe("Cookies to update") });
|
|
610
|
-
function createConsentUpdateCookiesTool(clients) {
|
|
611
|
-
return defineTool({
|
|
612
|
-
name: "consent_update_cookies",
|
|
613
|
-
description: "Update one or more cookies. Use to approve (status=LIVE), junk (isJunk=true), assign tracking purposes, or set a service. The cookie \"name\" field is the identifier for upsert — existing cookies with matching names will be updated.",
|
|
614
|
-
category: "Consent Management",
|
|
615
|
-
readOnly: false,
|
|
616
|
-
annotations: {
|
|
617
|
-
readOnlyHint: false,
|
|
618
|
-
destructiveHint: true,
|
|
619
|
-
idempotentHint: true
|
|
620
|
-
},
|
|
621
|
-
zodSchema: UpdateCookiesSchema,
|
|
622
|
-
handler: async ({ cookies }) => {
|
|
623
|
-
const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
|
|
624
|
-
const cookieInputs = cookies.map((c) => ({
|
|
625
|
-
name: c.name,
|
|
626
|
-
...c.trackingPurposes ? { trackingPurposes: c.trackingPurposes } : {},
|
|
627
|
-
...c.description !== void 0 ? { description: c.description } : {},
|
|
628
|
-
...c.service !== void 0 ? { service: c.service } : {},
|
|
629
|
-
...c.isJunk !== void 0 ? { isJunk: c.isJunk } : {},
|
|
630
|
-
...c.status !== void 0 ? { status: c.status } : {}
|
|
631
|
-
}));
|
|
632
|
-
await clients.graphql.makeRequest(UPDATE_OR_CREATE_COOKIES, {
|
|
633
|
-
airgapBundleId,
|
|
634
|
-
cookies: cookieInputs
|
|
635
|
-
});
|
|
636
|
-
return createToolResult(true, {
|
|
637
|
-
updated: cookieInputs.length,
|
|
638
|
-
cookies: cookieInputs.map((c) => ({
|
|
639
|
-
name: c.name,
|
|
640
|
-
status: c.status,
|
|
641
|
-
isJunk: c.isJunk,
|
|
642
|
-
trackingPurposes: c.trackingPurposes,
|
|
643
|
-
service: c.service
|
|
644
|
-
}))
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
//#endregion
|
|
650
|
-
//#region src/tools/consent_update_data_flows.ts
|
|
651
|
-
const UpdateDataFlowItemSchema = z.object({
|
|
652
|
-
id: z.string().describe("Data flow ID"),
|
|
653
|
-
trackingPurposes: z.array(z.string()).optional().describe("Tracking purpose slugs"),
|
|
654
|
-
description: z.string().optional().describe("Data flow description"),
|
|
655
|
-
service: z.string().optional().describe("Service/integration name"),
|
|
656
|
-
isJunk: z.boolean().optional().describe("Mark as junk"),
|
|
657
|
-
status: z.nativeEnum(ConsentTrackerStatus).optional().describe("Set status to LIVE (approve) or NEEDS_REVIEW")
|
|
658
|
-
});
|
|
659
|
-
const UpdateDataFlowsSchema = z.object({ dataFlows: z.array(UpdateDataFlowItemSchema).min(1).describe("Data flows to update") });
|
|
660
|
-
function createConsentUpdateDataFlowsTool(clients) {
|
|
661
|
-
return defineTool({
|
|
662
|
-
name: "consent_update_data_flows",
|
|
663
|
-
description: "Update one or more data flows. Use to approve (status=LIVE), junk (isJunk=true), assign tracking purposes, or set a service.",
|
|
664
|
-
category: "Consent Management",
|
|
665
|
-
readOnly: false,
|
|
666
|
-
annotations: {
|
|
667
|
-
readOnlyHint: false,
|
|
668
|
-
destructiveHint: true,
|
|
669
|
-
idempotentHint: true
|
|
670
|
-
},
|
|
671
|
-
zodSchema: UpdateDataFlowsSchema,
|
|
672
|
-
handler: async ({ dataFlows }) => {
|
|
673
|
-
const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
|
|
674
|
-
const dfInputs = dataFlows.map((df) => ({
|
|
675
|
-
id: df.id,
|
|
676
|
-
...df.trackingPurposes ? { purposeIds: df.trackingPurposes } : {},
|
|
677
|
-
...df.description !== void 0 ? { description: df.description } : {},
|
|
678
|
-
...df.service !== void 0 ? { service: df.service } : {},
|
|
679
|
-
...df.isJunk !== void 0 ? { isJunk: df.isJunk } : {},
|
|
680
|
-
...df.status !== void 0 ? { status: df.status } : {}
|
|
681
|
-
}));
|
|
682
|
-
const data = await clients.graphql.makeRequest(UPDATE_DATA_FLOWS, {
|
|
683
|
-
airgapBundleId,
|
|
684
|
-
dataFlows: dfInputs
|
|
685
|
-
});
|
|
686
|
-
return createToolResult(true, {
|
|
687
|
-
updated: data.updateDataFlows.dataFlows.length,
|
|
688
|
-
dataFlows: data.updateDataFlows.dataFlows.map((df) => ({
|
|
689
|
-
id: df.id,
|
|
690
|
-
value: df.value,
|
|
691
|
-
status: df.status,
|
|
692
|
-
isJunk: df.isJunk,
|
|
693
|
-
purposes: df.purposes.map((p) => p.name),
|
|
694
|
-
service: df.service?.title
|
|
695
|
-
}))
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
//#endregion
|
|
701
|
-
//#region src/tools/index.ts
|
|
702
|
-
function getConsentTools(clients) {
|
|
703
|
-
return [
|
|
704
|
-
createConsentGetPreferencesTool(clients),
|
|
705
|
-
createConsentSetPreferencesTool(clients),
|
|
706
|
-
createConsentListPurposesTool(clients),
|
|
707
|
-
createConsentListDataFlowsTool(clients),
|
|
708
|
-
createConsentListCookiesTool(clients),
|
|
709
|
-
createConsentListAirgapBundlesTool(clients),
|
|
710
|
-
createConsentListRegimesTool(clients),
|
|
711
|
-
createConsentGetInventoryStatsTool(clients),
|
|
712
|
-
createConsentGetAggregateAnalyticsTool(clients),
|
|
713
|
-
createConsentGetTimeseriesAnalyticsTool(clients),
|
|
714
|
-
createConsentGetAnalyticsDataTool(clients),
|
|
715
|
-
createConsentUpdateCookiesTool(clients),
|
|
716
|
-
createConsentUpdateDataFlowsTool(clients),
|
|
717
|
-
createConsentBulkTriageTool(clients)
|
|
718
|
-
];
|
|
719
|
-
}
|
|
720
|
-
//#endregion
|
|
721
|
-
//#region src/prompts/consent_inspect_site.ts
|
|
722
|
-
const consentInspectSitePrompt = {
|
|
723
|
-
name: "consent-inspect-site",
|
|
724
|
-
description: "Live site investigation methodology for consent triage using browser DevTools. Covers regime overrides, consent verification, performance entries, HTML search, ad infrastructure checks, and airgap classification queries.",
|
|
725
|
-
arguments: [
|
|
726
|
-
{
|
|
727
|
-
name: "site_url",
|
|
728
|
-
description: "The site to investigate (e.g. \"https://example.com\")",
|
|
729
|
-
required: true
|
|
730
|
-
},
|
|
731
|
-
{
|
|
732
|
-
name: "tracker_domains",
|
|
733
|
-
description: "Comma-separated tracker domains to look for (e.g. \"doubleclick.net,google-analytics.com\")",
|
|
734
|
-
required: true
|
|
735
|
-
},
|
|
736
|
-
{
|
|
737
|
-
name: "regime",
|
|
738
|
-
description: "The most permissive regime name for URL override (e.g. \"us\"). Choose the regime with fewest opted-out purposes so trackers fire.",
|
|
739
|
-
required: false
|
|
740
|
-
}
|
|
741
|
-
],
|
|
742
|
-
handler: (args) => {
|
|
743
|
-
const siteUrl = args.site_url || "(not specified)";
|
|
744
|
-
const trackerDomains = args.tracker_domains || "(not specified)";
|
|
745
|
-
const regime = args.regime || "us";
|
|
746
|
-
const domainList = trackerDomains.split(",").map((d) => d.trim()).filter(Boolean);
|
|
747
|
-
const domainArrayLiteral = JSON.stringify(domainList);
|
|
748
|
-
return [{
|
|
749
|
-
role: "user",
|
|
750
|
-
content: {
|
|
751
|
-
type: "text",
|
|
752
|
-
text: `Investigate how these trackers load on ${siteUrl}: ${trackerDomains}. Use regime "${regime}" for debug overrides.`
|
|
753
|
-
}
|
|
754
|
-
}, {
|
|
755
|
-
role: "assistant",
|
|
756
|
-
content: {
|
|
757
|
-
type: "text",
|
|
758
|
-
text: `## Live Site Investigation
|
|
759
|
-
|
|
760
|
-
### Important: Platform vs Client Sites
|
|
761
|
-
|
|
762
|
-
The bundle name (e.g. "acme-platform") may be a platform provider, not the actual
|
|
763
|
-
site with trackers. If the main domain is a corporate page without ad trackers, find a
|
|
764
|
-
real client site from links on the homepage and use that instead.
|
|
765
|
-
|
|
766
|
-
### Step 1: Navigate with Debug Overrides
|
|
767
|
-
|
|
768
|
-
Load the page with hash parameters to control consent behavior:
|
|
769
|
-
|
|
770
|
-
\`\`\`
|
|
771
|
-
${siteUrl}/#tcm-regime=${regime}&tcm-prompt=Hidden&log=*
|
|
772
|
-
\`\`\`
|
|
773
|
-
|
|
774
|
-
| Parameter | Purpose |
|
|
775
|
-
|-----------|---------|
|
|
776
|
-
| \`tcm-regime=${regime}\` | Force the most permissive privacy regime |
|
|
777
|
-
| \`tcm-prompt=Hidden\` | Suppress the consent banner |
|
|
778
|
-
| \`log=*\` | Enable verbose airgap debug logging |
|
|
779
|
-
|
|
780
|
-
When \`docs_list\` / \`docs_fetch\` are available, fetch the debugging article for full detail; otherwise open:
|
|
781
|
-
https://docs.transcend.io/docs/articles/consent-management/reference/debugging-and-testing
|
|
782
|
-
|
|
783
|
-
### Step 2: Verify Consent State
|
|
784
|
-
|
|
785
|
-
\`\`\`javascript
|
|
786
|
-
(() => {
|
|
787
|
-
if (!window.airgap) return 'airgap not loaded';
|
|
788
|
-
return JSON.stringify({
|
|
789
|
-
regimes: airgap.getRegimes(),
|
|
790
|
-
purposes: airgap.getConsent().purposes,
|
|
791
|
-
regimePurposes: airgap.getRegimePurposes(),
|
|
792
|
-
}, null, 2);
|
|
793
|
-
})()
|
|
794
|
-
\`\`\`
|
|
795
|
-
|
|
796
|
-
All purposes should be \`true\` or \`"Auto"\`. If not, opt in manually:
|
|
797
|
-
|
|
798
|
-
\`\`\`javascript
|
|
799
|
-
(() => {
|
|
800
|
-
airgap.optIn(Object.fromEntries(
|
|
801
|
-
airgap.getRegimePurposes().map(p => [p, true])
|
|
802
|
-
));
|
|
803
|
-
return JSON.stringify(airgap.getConsent().purposes);
|
|
804
|
-
})()
|
|
805
|
-
\`\`\`
|
|
806
|
-
|
|
807
|
-
### Step 3: Check Performance Entries for Tracker Domains
|
|
808
|
-
|
|
809
|
-
\`\`\`javascript
|
|
810
|
-
(() => {
|
|
811
|
-
const domains = ${domainArrayLiteral};
|
|
812
|
-
const entries = performance.getEntriesByType('resource');
|
|
813
|
-
const results = {};
|
|
814
|
-
for (const d of domains) {
|
|
815
|
-
results[d] = entries.filter(e => e.name.includes(d)).map(e => ({
|
|
816
|
-
url: e.name,
|
|
817
|
-
initiator: e.initiatorType,
|
|
818
|
-
duration: Math.round(e.duration),
|
|
819
|
-
size: e.transferSize,
|
|
820
|
-
}));
|
|
821
|
-
}
|
|
822
|
-
return JSON.stringify(results, null, 2);
|
|
823
|
-
})()
|
|
824
|
-
\`\`\`
|
|
825
|
-
|
|
826
|
-
### Step 4: Search Page HTML
|
|
827
|
-
|
|
828
|
-
\`\`\`javascript
|
|
829
|
-
(() => {
|
|
830
|
-
const terms = ${domainArrayLiteral};
|
|
831
|
-
const html = document.documentElement.outerHTML;
|
|
832
|
-
const results = {};
|
|
833
|
-
for (const term of terms) {
|
|
834
|
-
const matches = [];
|
|
835
|
-
let i = 0;
|
|
836
|
-
while ((i = html.indexOf(term, i)) !== -1) {
|
|
837
|
-
matches.push(html.substring(Math.max(0, i - 100), Math.min(html.length, i + 100)));
|
|
838
|
-
i += term.length;
|
|
839
|
-
if (matches.length > 3) break;
|
|
840
|
-
}
|
|
841
|
-
results[term] = { count: matches.length, samples: matches };
|
|
842
|
-
}
|
|
843
|
-
return JSON.stringify(results, null, 2);
|
|
844
|
-
})()
|
|
845
|
-
\`\`\`
|
|
846
|
-
|
|
847
|
-
### Step 5: Identify Ad Infrastructure
|
|
848
|
-
|
|
849
|
-
\`\`\`javascript
|
|
850
|
-
(() => {
|
|
851
|
-
const scripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src);
|
|
852
|
-
// Non-exhaustive list of common ad tech scripts; look for any third-party ad scripts beyond these
|
|
853
|
-
const adScripts = scripts.filter(s =>
|
|
854
|
-
s.includes('prebid') || s.includes('gpt.js') || s.includes('googletag') ||
|
|
855
|
-
s.includes('taboola') || s.includes('criteo') || s.includes('amazon-adsystem') ||
|
|
856
|
-
s.includes('adsbygoogle') || s.includes('doubleclick')
|
|
857
|
-
);
|
|
858
|
-
const adDivs = Array.from(document.querySelectorAll(
|
|
859
|
-
'[data-prebid], [data-ad], [data-ad-slot], [data-ad-unit], [id*="ad-slot"], [id*="ad-unit"], [class*="ad-container"]'
|
|
860
|
-
));
|
|
861
|
-
const adSlots = adDivs.map(d => ({
|
|
862
|
-
tag: d.tagName, id: d.id, class: d.className?.substring(0, 60),
|
|
863
|
-
dataSizes: d.getAttribute('data-sizes'),
|
|
864
|
-
dataPrebid: d.getAttribute('data-prebid'),
|
|
865
|
-
dataTargeting: d.getAttribute('data-targeting'),
|
|
866
|
-
}));
|
|
867
|
-
const iframes = Array.from(document.querySelectorAll('iframe'));
|
|
868
|
-
const adIframes = iframes.filter(f => f.title?.includes('ad') || f.id?.includes('ad'));
|
|
869
|
-
return JSON.stringify({
|
|
870
|
-
adScripts,
|
|
871
|
-
adSlotCount: adSlots.length,
|
|
872
|
-
adSlotSamples: adSlots.slice(0, 5),
|
|
873
|
-
adIframes: adIframes.map(f => ({
|
|
874
|
-
id: f.id, src: f.src?.substring(0, 150), title: f.title,
|
|
875
|
-
})),
|
|
876
|
-
}, null, 2);
|
|
877
|
-
})()
|
|
878
|
-
\`\`\`
|
|
879
|
-
|
|
880
|
-
### Step 6: Check Inline Initialization Scripts
|
|
881
|
-
|
|
882
|
-
\`\`\`javascript
|
|
883
|
-
(() => {
|
|
884
|
-
const scripts = Array.from(document.querySelectorAll('script:not([src])'));
|
|
885
|
-
const adInline = scripts.filter(s =>
|
|
886
|
-
s.textContent.includes('prebid') || s.textContent.includes('googletag') ||
|
|
887
|
-
s.textContent.includes('adsbygoogle') || s.textContent.includes('criteo') ||
|
|
888
|
-
s.textContent.includes('taboola')
|
|
889
|
-
);
|
|
890
|
-
return JSON.stringify(adInline.map(s => ({
|
|
891
|
-
parent: s.parentElement?.tagName,
|
|
892
|
-
preview: s.textContent.substring(0, 500),
|
|
893
|
-
})), null, 2);
|
|
894
|
-
})()
|
|
895
|
-
\`\`\`
|
|
896
|
-
|
|
897
|
-
### Step 7: Check Window Globals and Ad Config
|
|
898
|
-
|
|
899
|
-
\`\`\`javascript
|
|
900
|
-
(() => {
|
|
901
|
-
const knownAdGlobals = ['pbjs', 'googletag', '__tcfapi', '__gpp', '__cmp',
|
|
902
|
-
'adsbygoogle', '_taboola', 'criteo_q', 'apstag'];
|
|
903
|
-
const adGlobals = Object.keys(window).filter(k =>
|
|
904
|
-
knownAdGlobals.some(g => k.toLowerCase().includes(g.toLowerCase()))
|
|
905
|
-
);
|
|
906
|
-
const configs = {};
|
|
907
|
-
for (const g of adGlobals) {
|
|
908
|
-
try {
|
|
909
|
-
const val = window[g];
|
|
910
|
-
if (val && typeof val === 'object') {
|
|
911
|
-
configs[g] = JSON.stringify(val).substring(0, 500);
|
|
912
|
-
}
|
|
913
|
-
} catch {}
|
|
914
|
-
}
|
|
915
|
-
return JSON.stringify({ adGlobals, configs }, null, 2);
|
|
916
|
-
})()
|
|
917
|
-
\`\`\`
|
|
918
|
-
|
|
919
|
-
### Step 8: Check Airgap Classification Per Tracker
|
|
920
|
-
|
|
921
|
-
\`\`\`javascript
|
|
922
|
-
(async () => {
|
|
923
|
-
if (!window.airgap) return 'airgap not loaded';
|
|
924
|
-
const domains = ${domainArrayLiteral};
|
|
925
|
-
const results = {};
|
|
926
|
-
for (const d of domains) {
|
|
927
|
-
try {
|
|
928
|
-
const purposes = await airgap.getPurposes('https://' + d + '/');
|
|
929
|
-
const allowed = await airgap.isAllowed('https://' + d + '/');
|
|
930
|
-
results[d] = { purposes, allowed };
|
|
931
|
-
} catch (e) { results[d] = { error: e.message }; }
|
|
932
|
-
}
|
|
933
|
-
return JSON.stringify(results, null, 2);
|
|
934
|
-
})()
|
|
935
|
-
\`\`\`
|
|
936
|
-
|
|
937
|
-
### Step 9: Read Console Logs
|
|
938
|
-
|
|
939
|
-
Read the browser console output. The \`log=*\` override makes airgap emit detailed
|
|
940
|
-
allow/block decisions for every request, including purpose lookups. Search these logs
|
|
941
|
-
for each tracker domain to see how airgap classifies and handles it.
|
|
942
|
-
|
|
943
|
-
## Useful Console Commands Reference
|
|
944
|
-
|
|
945
|
-
| Command | Purpose |
|
|
946
|
-
|---------|---------|
|
|
947
|
-
| \`airgap.getConsent().purposes\` | Current consent state per purpose |
|
|
948
|
-
| \`airgap.getRegimes()\` | Active regime(s) for this session |
|
|
949
|
-
| \`airgap.getRegimePurposes()\` | Purposes regulated under current regime |
|
|
950
|
-
| \`await airgap.getPurposes('{url}')\` | What purposes a URL is classified under |
|
|
951
|
-
| \`await airgap.isAllowed('{url}')\` | Whether a URL is currently allowed |
|
|
952
|
-
| \`await airgap.isCookieAllowed({name:'{name}'})\` | Whether a cookie is allowed |
|
|
953
|
-
| \`await airgap.getCookiePurposes({name:'{name}'})\` | Cookie's assigned purposes |
|
|
954
|
-
| \`airgap.export().requests\` | Quarantined requests |
|
|
955
|
-
| \`airgap.export().cookies\` | Quarantined cookies |
|
|
956
|
-
| \`airgap.version\` | Current airgap version |
|
|
957
|
-
|
|
958
|
-
## Output Format
|
|
959
|
-
|
|
960
|
-
For each tracker return:
|
|
961
|
-
|
|
962
|
-
\`\`\`json
|
|
963
|
-
{
|
|
964
|
-
"domain": "<domain>",
|
|
965
|
-
"found_on_page": true,
|
|
966
|
-
"loading_method": "direct_script|tag_manager|iframe|dynamic|not_found",
|
|
967
|
-
"loaded_by": "<what script or mechanism loads it>",
|
|
968
|
-
"in_main_document": true,
|
|
969
|
-
"airgap_purposes": ["Advertising"],
|
|
970
|
-
"airgap_allowed": true,
|
|
971
|
-
"ad_infrastructure": "<detected ad chain, e.g. Prebid -> GPT>",
|
|
972
|
-
"related_config": "<relevant config values>",
|
|
973
|
-
"notes": "<additional observations>"
|
|
974
|
-
}
|
|
975
|
-
\`\`\`
|
|
976
|
-
|
|
977
|
-
Also return a site summary:
|
|
978
|
-
|
|
979
|
-
\`\`\`json
|
|
980
|
-
{
|
|
981
|
-
"site_investigated": "<actual URL used>",
|
|
982
|
-
"ad_stack": "<detected stack, e.g. Prebid -> Google Publisher Tags>",
|
|
983
|
-
"consent_manager": "Transcend CMP",
|
|
984
|
-
"total_ad_slots": "<count>",
|
|
985
|
-
"total_scripts": "<count>",
|
|
986
|
-
"total_iframes": "<count>"
|
|
987
|
-
}
|
|
988
|
-
\`\`\``
|
|
989
|
-
}
|
|
990
|
-
}];
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
|
-
//#endregion
|
|
994
|
-
//#region src/prompts/consent_research_tracker.ts
|
|
995
|
-
const consentResearchTrackerPrompt = {
|
|
996
|
-
name: "consent-research-tracker",
|
|
997
|
-
description: "Research methodology for classifying cookies and data flows. Covers company identification, privacy policy lookup, CMP database checks, and structured evidence gathering for consent purpose assignment.",
|
|
998
|
-
arguments: [
|
|
999
|
-
{
|
|
1000
|
-
name: "domain",
|
|
1001
|
-
description: "The tracker domain or cookie name to research (e.g. \"doubleclick.net\", \"_ga\")",
|
|
1002
|
-
required: true
|
|
1003
|
-
},
|
|
1004
|
-
{
|
|
1005
|
-
name: "type",
|
|
1006
|
-
description: "Whether this is a \"cookie\" or \"data_flow\" (default: \"data_flow\")",
|
|
1007
|
-
required: false
|
|
1008
|
-
},
|
|
1009
|
-
{
|
|
1010
|
-
name: "available_purposes",
|
|
1011
|
-
description: "Comma-separated list of the customer's configured purposes (e.g. \"Essential,Functional,Analytics,Advertising,SaleOfInfo\"). Only recommend purposes from this list.",
|
|
1012
|
-
required: false
|
|
1013
|
-
}
|
|
1014
|
-
],
|
|
1015
|
-
handler: (args) => {
|
|
1016
|
-
const domain = args.domain || "(not specified)";
|
|
1017
|
-
return [{
|
|
1018
|
-
role: "user",
|
|
1019
|
-
content: {
|
|
1020
|
-
type: "text",
|
|
1021
|
-
text: `Research the ${args.type || "data_flow"} "${domain}" to determine its consent classification. Available purposes: ${args.available_purposes || "(fetch from consent_list_purposes)"}`
|
|
1022
|
-
}
|
|
1023
|
-
}, {
|
|
1024
|
-
role: "assistant",
|
|
1025
|
-
content: {
|
|
1026
|
-
type: "text",
|
|
1027
|
-
text: `## Research Methodology
|
|
1028
|
-
|
|
1029
|
-
For each tracker or cookie, follow these steps in order:
|
|
1030
|
-
|
|
1031
|
-
### Step 1: Company Identification
|
|
1032
|
-
|
|
1033
|
-
Search the root domain (strip subdomains for broader matches) to find the operating company.
|
|
1034
|
-
Check for recent acquisitions or rebrands — ad tech companies frequently change ownership.
|
|
1035
|
-
|
|
1036
|
-
### Step 2: First-Party Privacy Docs
|
|
1037
|
-
|
|
1038
|
-
Find and read the company's privacy policy and/or cookie policy. Look for:
|
|
1039
|
-
- How they classify their own tracking
|
|
1040
|
-
- What data they collect
|
|
1041
|
-
- Stated purposes for data processing
|
|
1042
|
-
- Data retention periods
|
|
1043
|
-
|
|
1044
|
-
### Step 3: Service Description
|
|
1045
|
-
|
|
1046
|
-
Understand the business model:
|
|
1047
|
-
- Ad tech (DSP, SSP, ad exchange, header bidding)?
|
|
1048
|
-
- Analytics (pageview counters, session recording, A/B testing)?
|
|
1049
|
-
- CMP (consent management platform)?
|
|
1050
|
-
- CDN / performance (content delivery, image optimization)?
|
|
1051
|
-
- Functional (chat, support, preferences, authentication)?
|
|
1052
|
-
- Data broker (selling/sharing data with third parties)?
|
|
1053
|
-
|
|
1054
|
-
### Step 4: CMP Database Lookups
|
|
1055
|
-
|
|
1056
|
-
Search these databases for existing classifications:
|
|
1057
|
-
|
|
1058
|
-
| Database | URL | Use For |
|
|
1059
|
-
|----------|-----|---------|
|
|
1060
|
-
| CookieDatabase.org | https://cookiedatabase.org/ | Cookie name lookup |
|
|
1061
|
-
| better.fyi trackers | https://better.fyi/trackers/ | Domain-to-company lookup |
|
|
1062
|
-
| Ghostery TrackerDB | https://www.ghostery.com/trackerdb | Tracker classification |
|
|
1063
|
-
| Cookiepedia | https://cookiepedia.co.uk/ | Cookie purpose database |
|
|
1064
|
-
| BuiltWith | https://builtwith.com/ | Site technology stack |
|
|
1065
|
-
| urlscan.io | https://urlscan.io/ | Domain/infrastructure analysis |
|
|
1066
|
-
|
|
1067
|
-
### Step 5: Third-Party Cookie Policies
|
|
1068
|
-
|
|
1069
|
-
Find other companies' published cookie policies that classify this same tracker/service.
|
|
1070
|
-
Multiple independent classifications strengthen confidence.
|
|
1071
|
-
|
|
1072
|
-
### Step 6: Essential vs Non-Essential Determination
|
|
1073
|
-
|
|
1074
|
-
Based on all evidence:
|
|
1075
|
-
- Would the site break without this tracker? (Essential)
|
|
1076
|
-
- Is it required for core functionality like auth, security, or the CMP itself? (Essential)
|
|
1077
|
-
- Does it enhance features without being required? (Functional)
|
|
1078
|
-
- Does it measure usage or behavior? (Analytics)
|
|
1079
|
-
- Does it serve, target, or retarget ads? (Advertising)
|
|
1080
|
-
- Is data sold or shared with third parties for their own use? (SaleOfInfo)
|
|
1081
|
-
|
|
1082
|
-
Items can have multiple purposes (e.g. ["Advertising", "Analytics"] for an ad platform
|
|
1083
|
-
that also tracks impressions).
|
|
1084
|
-
|
|
1085
|
-
IMPORTANT: Only recommend purposes from the customer's configured list. If research
|
|
1086
|
-
suggests a purpose that doesn't exist for this customer, flag it and suggest the closest
|
|
1087
|
-
available match.
|
|
1088
|
-
|
|
1089
|
-
## Junk Indicators
|
|
1090
|
-
|
|
1091
|
-
Mark as JUNK (not a real tracker to classify) if:
|
|
1092
|
-
- From a browser extension (Grammarly, LastPass, ad blockers injecting scripts)
|
|
1093
|
-
- Malware or unwanted injection not placed by the site operator
|
|
1094
|
-
- A development/testing artifact (localhost, staging URLs)
|
|
1095
|
-
- A subdomain variant of an already-approved regex rule
|
|
1096
|
-
|
|
1097
|
-
## Confidence Levels
|
|
1098
|
-
|
|
1099
|
-
- **High**: First-party docs confirm, OR multiple CMPs agree, OR well-known tracker
|
|
1100
|
-
- **Medium**: Some evidence but no definitive first-party documentation
|
|
1101
|
-
- **Low**: No docs found, best-guess only — flag for manual review
|
|
1102
|
-
|
|
1103
|
-
## Output Format
|
|
1104
|
-
|
|
1105
|
-
Return a structured finding for each item:
|
|
1106
|
-
|
|
1107
|
-
\`\`\`json
|
|
1108
|
-
{
|
|
1109
|
-
"domain": "<domain or cookie name>",
|
|
1110
|
-
"company_name": "<identified company>",
|
|
1111
|
-
"company_description": "<what the company does, 1-2 sentences>",
|
|
1112
|
-
"service_url": "<company homepage>",
|
|
1113
|
-
"specific_product": "<what product/feature this domain serves>",
|
|
1114
|
-
"recommended_purposes": ["Advertising"],
|
|
1115
|
-
"confidence": "High",
|
|
1116
|
-
"is_junk": false,
|
|
1117
|
-
"evidence_summary": "<2-3 sentence summary with key facts>",
|
|
1118
|
-
"sources": ["<url1>", "<url2>"],
|
|
1119
|
-
"suggested_description": "<one-line description to save as Transcend note>",
|
|
1120
|
-
"first_party_privacy_url": "<URL of their privacy/cookie policy if found>",
|
|
1121
|
-
"other_cmps_classify_as": "<what other CMPs say>"
|
|
1122
|
-
}
|
|
1123
|
-
\`\`\``
|
|
1124
|
-
}
|
|
1125
|
-
}];
|
|
1126
|
-
}
|
|
1127
|
-
};
|
|
1128
|
-
//#endregion
|
|
1129
|
-
//#region src/prompts/consent_triage.ts
|
|
1130
|
-
const consentTriagePrompt = {
|
|
1131
|
-
name: "consent-triage",
|
|
1132
|
-
description: "Systematically triage cookies and data flows discovered by Transcend consent telemetry. Walks through setup, batch fetching, research, review, and classification push.",
|
|
1133
|
-
arguments: [{
|
|
1134
|
-
name: "triage_type",
|
|
1135
|
-
description: "What to triage: \"cookies\", \"data_flows\", or \"both\" (default: \"both\")",
|
|
1136
|
-
required: false
|
|
1137
|
-
}, {
|
|
1138
|
-
name: "batch_size",
|
|
1139
|
-
description: "Number of items per batch (default: 10)",
|
|
1140
|
-
required: false
|
|
1141
|
-
}],
|
|
1142
|
-
handler: (args) => {
|
|
1143
|
-
const triageType = args.triage_type || "both";
|
|
1144
|
-
const batchSize = args.batch_size || "10";
|
|
1145
|
-
return [{
|
|
1146
|
-
role: "user",
|
|
1147
|
-
content: {
|
|
1148
|
-
type: "text",
|
|
1149
|
-
text: `Triage ${triageType === "both" ? "cookies and data flows" : triageType} in batches of ${batchSize}, sorted by highest traffic first.`
|
|
1150
|
-
}
|
|
1151
|
-
}, {
|
|
1152
|
-
role: "assistant",
|
|
1153
|
-
content: {
|
|
1154
|
-
type: "text",
|
|
1155
|
-
text: `I'll walk through the consent triage workflow. Here's how it works:
|
|
1156
|
-
|
|
1157
|
-
## Phase 1: Setup
|
|
1158
|
-
|
|
1159
|
-
Gather the customer's consent configuration by calling these tools in parallel:
|
|
1160
|
-
|
|
1161
|
-
1. \`consent_list_airgap_bundles\` — get the consent manager info (bundle ID is auto-resolved)
|
|
1162
|
-
2. \`consent_get_inventory_stats\` — backlog overview
|
|
1163
|
-
3. \`consent_list_purposes\` — the customer's configured tracking purposes
|
|
1164
|
-
4. \`consent_list_regimes\` — consent experiences with regions, purposes, and opt-out defaults
|
|
1165
|
-
|
|
1166
|
-
CRITICAL: Each customer configures their own purposes. Do NOT assume defaults exist. Only use purposes returned by \`consent_list_purposes\` for classification.
|
|
1167
|
-
|
|
1168
|
-
From the regimes data, determine:
|
|
1169
|
-
- Which purposes can be opted out of per experience
|
|
1170
|
-
- Which purposes default to opted-out
|
|
1171
|
-
- The most permissive regime (fewest opted-out purposes) — needed for live site investigation
|
|
1172
|
-
|
|
1173
|
-
Present the customer's setup:
|
|
1174
|
-
|
|
1175
|
-
| Purpose | Slug | Used in Regimes |
|
|
1176
|
-
|---------|------|-----------------|
|
|
1177
|
-
| (from API) | (from API) | (cross-ref with regimes) |
|
|
1178
|
-
|
|
1179
|
-
Present triage stats from \`consent_get_inventory_stats\` (cookie and data-flow counts match the Consent Manager tables; CSP data flows are omitted like the UI):
|
|
1180
|
-
|
|
1181
|
-
| Metric | Cookies | Data Flows |
|
|
1182
|
-
|--------|---------|------------|
|
|
1183
|
-
| Needs Review | cookies.needReviewCount | dataFlows.needReviewCount |
|
|
1184
|
-
| Live (Approved) | cookies.liveCount | dataFlows.liveCount |
|
|
1185
|
-
| Junk | cookies.junkCount | dataFlows.junkCount |
|
|
1186
|
-
|
|
1187
|
-
## Phase 2: Fetch Batch
|
|
1188
|
-
|
|
1189
|
-
Fetch the next batch of items needing review, sorted by highest traffic:
|
|
1190
|
-
|
|
1191
|
-
${[triageType === "cookies" || triageType === "both" ? "- `consent_list_cookies { status: \"NEEDS_REVIEW\", first: " + batchSize + ", order_field: \"occurrences\", order_direction: \"DESC\" }`" : "", triageType === "data_flows" || triageType === "both" ? "- `consent_list_data_flows { status: \"NEEDS_REVIEW\", first: " + batchSize + ", order_field: \"occurrences\", order_direction: \"DESC\" }`" : ""].filter(Boolean).join("\n")}
|
|
1192
|
-
|
|
1193
|
-
Present in this table format:
|
|
1194
|
-
|
|
1195
|
-
| # | Name/Domain | Type | Service | Auto-Purposes | Occurrences | Sites | First Seen |
|
|
1196
|
-
|---|-------------|------|---------|---------------|-------------|-------|------------|
|
|
1197
|
-
|
|
1198
|
-
## Phase 3: Research
|
|
1199
|
-
|
|
1200
|
-
For each item in the batch, research its purpose using web search and CMP databases.
|
|
1201
|
-
Use the \`consent-research-tracker\` prompt for detailed research methodology.
|
|
1202
|
-
If browser/DevTools access is available, use the \`consent-inspect-site\` prompt for live site investigation.
|
|
1203
|
-
|
|
1204
|
-
Split items into parallel research groups of 3–5 items each for efficiency.
|
|
1205
|
-
|
|
1206
|
-
## Phase 4: Present Findings
|
|
1207
|
-
|
|
1208
|
-
For each researched item, present:
|
|
1209
|
-
|
|
1210
|
-
### {name/domain}
|
|
1211
|
-
| Field | Value |
|
|
1212
|
-
|-------|-------|
|
|
1213
|
-
| Type | Cookie / Data Flow (HOST/REGEX) |
|
|
1214
|
-
| Domain | \`example.com\` |
|
|
1215
|
-
| Service | Service Name (or "Unknown") |
|
|
1216
|
-
| Current Purposes | What Transcend auto-classified (if any) |
|
|
1217
|
-
| Recommended Purpose | Research-based recommendation |
|
|
1218
|
-
| Confidence | High / Medium / Low |
|
|
1219
|
-
| How Loaded | Direct script / Tag manager / iframe / Dynamic |
|
|
1220
|
-
| Occurrences | N |
|
|
1221
|
-
| Evidence | Brief summary + source URLs |
|
|
1222
|
-
| Recommended Action | APPROVE with purposes / JUNK / NEEDS MANUAL REVIEW |
|
|
1223
|
-
| Suggested Note | Description to save to Transcend |
|
|
1224
|
-
|
|
1225
|
-
Then show a summary action table:
|
|
1226
|
-
|
|
1227
|
-
| # | Name/Domain | Action | Purposes | Service | Note |
|
|
1228
|
-
|---|-------------|--------|----------|---------|------|
|
|
1229
|
-
|
|
1230
|
-
Ask the user to confirm, modify, or reject each recommendation before proceeding.
|
|
1231
|
-
|
|
1232
|
-
## Phase 5: Push Classifications
|
|
1233
|
-
|
|
1234
|
-
For confirmed items, update Transcend:
|
|
1235
|
-
|
|
1236
|
-
- Individual updates with notes: \`consent_update_data_flows\` / \`consent_update_cookies\` with id, tracking_purposes, description, service, status: "LIVE"
|
|
1237
|
-
- Bulk approve/junk: \`consent_bulk_triage\` with items array containing type, id, action, tracking_purposes
|
|
1238
|
-
- Mark junk items with action "JUNK" (no purposes needed)
|
|
1239
|
-
|
|
1240
|
-
After pushing, report what was updated and show the remaining triage count.
|
|
1241
|
-
|
|
1242
|
-
## Phase 6: Loop
|
|
1243
|
-
|
|
1244
|
-
Ask the user if they want to continue with the next batch. Repeat from Phase 2.
|
|
1245
|
-
|
|
1246
|
-
## Key References
|
|
1247
|
-
|
|
1248
|
-
When \`docs_list\` / \`docs_fetch\` are available (e.g. the unified \`@transcend-io/mcp\` server), prefer those for full markdown. Otherwise open the docs URLs directly:
|
|
1249
|
-
|
|
1250
|
-
- Triage guide: https://docs.transcend.io/docs/articles/consent-management/configuration/triage-cookies-and-dataflows-guide
|
|
1251
|
-
- Data flows & cookies: https://docs.transcend.io/docs/articles/consent-management/concepts/data-flows-and-cookies
|
|
1252
|
-
- Tracking purposes: https://docs.transcend.io/docs/articles/consent-management/concepts/tracking-purposes
|
|
1253
|
-
- Regional experiences: https://docs.transcend.io/docs/articles/consent-management/configuration/regional-experiences
|
|
1254
|
-
- Telemetry overview: https://docs.transcend.io/docs/articles/consent-management/configuration/telemetry-overview`
|
|
1255
|
-
}
|
|
1256
|
-
}];
|
|
1257
|
-
}
|
|
1258
|
-
};
|
|
1259
|
-
//#endregion
|
|
1260
|
-
//#region src/prompts/index.ts
|
|
1261
|
-
/**
|
|
1262
|
-
* Returns consent workflow prompt templates for MCP prompts/list and prompts/get.
|
|
1263
|
-
*
|
|
1264
|
-
* @param _clients - Unused; accepted so createMCPServer can pass the same factory shape as getTools
|
|
1265
|
-
*/
|
|
1266
|
-
function getConsentPrompts(_clients) {
|
|
1267
|
-
return [
|
|
1268
|
-
consentTriagePrompt,
|
|
1269
|
-
consentResearchTrackerPrompt,
|
|
1270
|
-
consentInspectSitePrompt
|
|
1271
|
-
];
|
|
1272
|
-
}
|
|
1273
|
-
//#endregion
|
|
1274
|
-
//#region src/scopes.ts
|
|
1275
|
-
/** OAuth scopes required for Consent MCP tools (offline_access added by base). */
|
|
1276
|
-
const CONSENT_OAUTH_SCOPES = [
|
|
1277
|
-
ScopeName.ViewConsentManager,
|
|
1278
|
-
ScopeName.ViewAssignedConsentManager,
|
|
1279
|
-
ScopeName.ManageConsentManager,
|
|
1280
|
-
ScopeName.ManageAssignedConsentManager,
|
|
1281
|
-
ScopeName.ViewDataFlow,
|
|
1282
|
-
ScopeName.ManageDataFlow
|
|
1283
|
-
];
|
|
1284
|
-
//#endregion
|
|
1285
|
-
export { resolveAirgapBundleId as C, BulkTriageSchema as S, GetInventoryStatsSchema as _, UpdateDataFlowsSchema as a, resolveAnalyticsDateRange as b, PurposeConsentSchema as c, ListPurposesSchema as d, ListDataFlowsSchema as f, GetPreferencesSchema as g, GetTimeseriesAnalyticsSchema as h, UpdateDataFlowItemSchema as i, SetPreferencesSchema as l, ListAirgapBundlesSchema as m, getConsentPrompts as n, UpdateCookieItemSchema as o, ListCookiesSchema as p, getConsentTools as r, UpdateCookiesSchema as s, CONSENT_OAUTH_SCOPES as t, ListRegimesSchema as u, GetAnalyticsDataSchema as v, BulkTriageItemSchema as x, GetAggregateAnalyticsSchema as y };
|
|
1286
|
-
|
|
1287
|
-
//# sourceMappingURL=scopes-UQvBqSH8.mjs.map
|