@ductape/mcp 0.3.4 → 0.3.6
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.
|
@@ -4,6 +4,7 @@ interface AnalyticsDecision {
|
|
|
4
4
|
rationale?: string;
|
|
5
5
|
autoCapture?: 'disabled' | 'reviewed';
|
|
6
6
|
privacyReviewed?: boolean;
|
|
7
|
+
settingsContractsReviewed?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface FrontendAnalyticsValidationResult {
|
|
9
10
|
valid: boolean;
|
|
@@ -18,6 +19,7 @@ export interface FrontendAnalyticsValidationResult {
|
|
|
18
19
|
framework: 'react' | 'vue' | 'client';
|
|
19
20
|
decision?: AnalyticsDecision;
|
|
20
21
|
signals: Record<string, boolean>;
|
|
22
|
+
evidence: Record<string, string[]>;
|
|
21
23
|
errors: string[];
|
|
22
24
|
warnings: string[];
|
|
23
25
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontend-analytics-validator.d.ts","sourceRoot":"","sources":["../src/frontend-analytics-validator.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,CAAC;AAE5E,UAAU,iBAAiB;IACzB,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACtC,eAAe,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"frontend-analytics-validator.d.ts","sourceRoot":"","sources":["../src/frontend-analytics-validator.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,CAAC;AAE5E,UAAU,iBAAiB;IACzB,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACtC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAMD,MAAM,WAAW,iCAAiC;IAChD,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,YAAY,EAAE,KAAK,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;QACtC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACnC,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC,CAAC;IACH,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AA0DD,wBAAgB,gCAAgC,CAC9C,UAAU,EAAE,MAAM,GACjB,iCAAiC,CAyInC"}
|
|
@@ -47,17 +47,18 @@ function packageFramework(manifest) {
|
|
|
47
47
|
return 'client';
|
|
48
48
|
return undefined;
|
|
49
49
|
}
|
|
50
|
-
function scanSource(
|
|
50
|
+
function scanSource(projectRoot, roots) {
|
|
51
51
|
const chunks = [];
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
for (const sourceRoot of roots)
|
|
53
|
+
walk(sourceRoot, (file) => {
|
|
54
|
+
if (!SOURCE_EXTENSIONS.has(path.extname(file)))
|
|
55
|
+
return;
|
|
56
|
+
const stats = fs.statSync(file);
|
|
57
|
+
if (stats.size > MAX_SOURCE_BYTES)
|
|
58
|
+
return;
|
|
59
|
+
chunks.push({ file: normalizedRelative(projectRoot, file), source: fs.readFileSync(file, 'utf8') });
|
|
60
|
+
});
|
|
61
|
+
return chunks;
|
|
61
62
|
}
|
|
62
63
|
function normalizedRelative(root, target) {
|
|
63
64
|
return path.relative(root, target).split(path.sep).join('/') || '.';
|
|
@@ -87,6 +88,15 @@ export function validateFrontendAnalyticsProject(projectDir) {
|
|
|
87
88
|
if (path.basename(file) === 'package.json')
|
|
88
89
|
packageFiles.push(file);
|
|
89
90
|
});
|
|
91
|
+
const packages = new Map();
|
|
92
|
+
for (const manifestFile of packageFiles) {
|
|
93
|
+
try {
|
|
94
|
+
const manifest = readJson(manifestFile);
|
|
95
|
+
if (typeof manifest.name === 'string')
|
|
96
|
+
packages.set(manifest.name, { root: path.dirname(manifestFile), manifest });
|
|
97
|
+
}
|
|
98
|
+
catch { /* Invalid manifests are ignored as application candidates below. */ }
|
|
99
|
+
}
|
|
90
100
|
const applications = packageFiles.flatMap((manifestFile) => {
|
|
91
101
|
let manifest;
|
|
92
102
|
try {
|
|
@@ -100,19 +110,45 @@ export function validateFrontendAnalyticsProject(projectDir) {
|
|
|
100
110
|
return [];
|
|
101
111
|
const applicationRoot = path.dirname(manifestFile);
|
|
102
112
|
const applicationPath = normalizedRelative(root, applicationRoot);
|
|
103
|
-
const
|
|
113
|
+
const sourceRoots = [applicationRoot];
|
|
114
|
+
const visited = new Set([applicationRoot]);
|
|
115
|
+
for (let index = 0; index < sourceRoots.length; index += 1) {
|
|
116
|
+
const currentRoot = sourceRoots[index];
|
|
117
|
+
const current = index === 0
|
|
118
|
+
? manifest
|
|
119
|
+
: [...packages.values()].find((entry) => entry.root === currentRoot)?.manifest;
|
|
120
|
+
if (!current)
|
|
121
|
+
continue;
|
|
122
|
+
const dependencies = { ...(current.dependencies ?? {}), ...(current.devDependencies ?? {}) };
|
|
123
|
+
const directSource = scanSource(root, [currentRoot]).map((item) => item.source).join('\n');
|
|
124
|
+
for (const dependencyName of Object.keys(dependencies)) {
|
|
125
|
+
const dependency = packages.get(dependencyName);
|
|
126
|
+
if (!dependency || visited.has(dependency.root))
|
|
127
|
+
continue;
|
|
128
|
+
const escaped = dependencyName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
129
|
+
if (!new RegExp(`['\"]${escaped}(?:/[^'\"]*)?['\"]`).test(directSource))
|
|
130
|
+
continue;
|
|
131
|
+
visited.add(dependency.root);
|
|
132
|
+
sourceRoots.push(dependency.root);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const files = scanSource(root, sourceRoots);
|
|
104
136
|
const decision = decisions.applications?.[applicationPath];
|
|
105
|
-
const
|
|
106
|
-
provider: /DuctapeProvider
|
|
107
|
-
|
|
108
|
-
analytics: /
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
137
|
+
const patterns = {
|
|
138
|
+
provider: /DuctapeProvider/, authenticatedSessions: /useSession|\.sessions\.|sessionToken|refreshToken/,
|
|
139
|
+
analytics: /useAnalytics|\.analytics\./, identify: /(?:analytics\.)?identify\s*\(/,
|
|
140
|
+
clearSession: /(?:analytics\.)?clearSession\s*\(/, pageview: /(?:analytics\.)?pageview\s*\(/,
|
|
141
|
+
namedEvents: /(?:analytics\.)?track\s*\(/, autoCapture: /enableAutoCapture\s*\(/,
|
|
142
|
+
clientFailure: /client[_\-. ]error|frontend[_\-. ]error|error_boundary|unhandledrejection/i,
|
|
143
|
+
settingsSurface: /settings|preferences|notificationpreferences/i,
|
|
144
|
+
settingsLiveRead: /\b(?:get|load|fetch|request)\s*[:=(]|method\s*:\s*['"]GET['"]/i,
|
|
145
|
+
settingsLiveWrite: /\b(?:update|save|mutate|request)\s*[:=(]|method\s*:\s*['"](?:POST|PUT|PATCH)['"]/i,
|
|
146
|
+
settingsLoading: /\b(?:loading|isLoading|pending|isPending)\b/i,
|
|
147
|
+
settingsFailure: /\b(?:error|failure|failed)\b/i,
|
|
148
|
+
settingsRetry: /\b(?:retry|refetch|reload|tryAgain)\b/i,
|
|
115
149
|
};
|
|
150
|
+
const evidence = Object.fromEntries(Object.entries(patterns).map(([name, pattern]) => [name, files.filter((item) => pattern.test(item.source)).map((item) => item.file)]));
|
|
151
|
+
const signals = Object.fromEntries(Object.entries(evidence).map(([name, matches]) => [name, matches.length > 0]));
|
|
116
152
|
const errors = [];
|
|
117
153
|
const warnings = [];
|
|
118
154
|
if (decisionError)
|
|
@@ -147,8 +183,22 @@ export function validateFrontendAnalyticsProject(projectDir) {
|
|
|
147
183
|
if (!signals.clientFailure) {
|
|
148
184
|
warnings.push('No sanitized client-failure analytics signal was detected; review error boundaries and unhandled failures.');
|
|
149
185
|
}
|
|
186
|
+
if (signals.settingsSurface) {
|
|
187
|
+
if (!signals.settingsLiveRead)
|
|
188
|
+
errors.push('A settings surface was found without a statically discoverable canonical read path.');
|
|
189
|
+
if (!signals.settingsLiveWrite)
|
|
190
|
+
errors.push('A settings surface was found without a statically discoverable persisted mutation path.');
|
|
191
|
+
if (!signals.settingsLoading)
|
|
192
|
+
errors.push('A settings surface was found without a loading/pending state.');
|
|
193
|
+
if (!signals.settingsFailure)
|
|
194
|
+
errors.push('A settings surface was found without a failure state.');
|
|
195
|
+
if (!signals.settingsRetry)
|
|
196
|
+
errors.push('A settings surface was found without a retry/refetch path.');
|
|
197
|
+
if (!decision.settingsContractsReviewed)
|
|
198
|
+
errors.push('Settings payload keys must be reviewed against the current registered Feature input schema; record settingsContractsReviewed: true after that review.');
|
|
199
|
+
}
|
|
150
200
|
}
|
|
151
|
-
return [{ path: applicationPath, framework, decision, signals, errors, warnings }];
|
|
201
|
+
return [{ path: applicationPath, framework, decision, signals, evidence, errors, warnings }];
|
|
152
202
|
});
|
|
153
203
|
const errors = applications.reduce((count, app) => count + app.errors.length, 0);
|
|
154
204
|
const warnings = applications.reduce((count, app) => count + app.warnings.length, 0);
|
package/dist/index.js
CHANGED
|
@@ -1723,6 +1723,9 @@ const featuresProjectValidationInputSchema = z.object({
|
|
|
1723
1723
|
const frontendAnalyticsProjectValidationInputSchema = z.object({
|
|
1724
1724
|
project_dir: z.string().describe('Absolute path to a frontend project or monorepo. The validator is read-only and never modifies application code.'),
|
|
1725
1725
|
});
|
|
1726
|
+
const sessionProjectValidationInputSchema = z.object({
|
|
1727
|
+
project_dir: z.string().describe('Absolute path to the project. Performs a read-only SDK session-propagation audit.'),
|
|
1728
|
+
});
|
|
1726
1729
|
const readOnlyLocalAnnotations = {
|
|
1727
1730
|
readOnlyHint: true,
|
|
1728
1731
|
destructiveHint: false,
|
|
@@ -2388,6 +2391,23 @@ PRODUCT ANALYTICS
|
|
|
2388
2391
|
failures, privacy masking, hidden-state safety, and trace correlation. Keep auto-capture off until
|
|
2389
2392
|
consent, selectors, masking, and sensitive states have been reviewed.
|
|
2390
2393
|
|
|
2394
|
+
LIVE SETTINGS AND FEATURE CONTRACTS
|
|
2395
|
+
A settings screen is incomplete if it renders static/mock values, mutates only local state, or
|
|
2396
|
+
sends independently invented payload keys. Before declaring any persisted preference/settings
|
|
2397
|
+
requirement complete:
|
|
2398
|
+
1. Identify the owning registered Feature and inspect its exact current input schema. Do not
|
|
2399
|
+
infer the schema from UI labels, old examples, TypeScript names, or domain expectations.
|
|
2400
|
+
2. Generate or consume the frontend input type from that Feature contract. Literal payload keys
|
|
2401
|
+
must be a subset of declared properties and every required property must be supplied.
|
|
2402
|
+
3. Use the session-aware Ductape client path (directly or through a reviewed application API),
|
|
2403
|
+
load canonical state on entry, persist every mutation, and render loading, failure, and retry.
|
|
2404
|
+
4. Verify a real read-after-write round trip in the target environment. A successful animation,
|
|
2405
|
+
toast, local state change, mock response, or TypeScript compilation is not persistence proof.
|
|
2406
|
+
5. For notification preferences, distinguish channel consent (email, sms, push) from event-category
|
|
2407
|
+
subscriptions. Never submit category labels where the Feature contract accepts channel keys.
|
|
2408
|
+
Completion review MUST reject static/mock settings, missing read or write paths, absent failure/retry
|
|
2409
|
+
states, and payload keys that were not checked against the registered Feature schema.
|
|
2410
|
+
|
|
2391
2411
|
AUTHENTICATION AND SECURITY
|
|
2392
2412
|
- Use publishableKey in browser applications. Never ship workspace private keys or privileged
|
|
2393
2413
|
access keys in frontend bundles.
|
|
@@ -6106,6 +6126,10 @@ const frontendAnalyticsProjectValidationHandler = async (args) => {
|
|
|
6106
6126
|
};
|
|
6107
6127
|
}
|
|
6108
6128
|
};
|
|
6129
|
+
const sessionProjectValidationHandler = async (args) => {
|
|
6130
|
+
const result = runCli('sessions validate --json', args.project_dir);
|
|
6131
|
+
return { content: [{ type: 'text', text: result.output || '(no output)' }], ...(result.success ? {} : { isError: true }) };
|
|
6132
|
+
};
|
|
6109
6133
|
const cliInputSchema = z.object({
|
|
6110
6134
|
command: z.string().describe('The ductape CLI command to run, without the leading "ductape" word. ' +
|
|
6111
6135
|
'Examples: "products list", "products create --name \\"My Product\\" --tag my-product", ' +
|
|
@@ -6966,6 +6990,12 @@ async function main() {
|
|
|
6966
6990
|
inputSchema: frontendAnalyticsProjectValidationInputSchema,
|
|
6967
6991
|
annotations: readOnlyLocalAnnotations,
|
|
6968
6992
|
}, frontendAnalyticsProjectValidationHandler);
|
|
6993
|
+
server.registerTool('ductape_sessions_validate_project', {
|
|
6994
|
+
title: 'Validate Ductape Session Propagation',
|
|
6995
|
+
description: 'Read-only project-wide audit. Every supported SDK call must explicitly pass a session, inherit it from Feature context, or carry a reasoned delegated/system marker.',
|
|
6996
|
+
inputSchema: sessionProjectValidationInputSchema,
|
|
6997
|
+
annotations: readOnlyLocalAnnotations,
|
|
6998
|
+
}, sessionProjectValidationHandler);
|
|
6969
6999
|
server.registerTool('ductape_function_setup', {
|
|
6970
7000
|
title: 'Ductape Portable Function Setup',
|
|
6971
7001
|
description: 'Read-only: generate (without applying) the mandatory secure local + remote runtime setup for application functions used by Features. ' +
|
|
@@ -7217,6 +7247,7 @@ async function main() {
|
|
|
7217
7247
|
server.tool('ductape_events_validate_project', eventsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, eventsProjectValidationHandler);
|
|
7218
7248
|
server.tool('ductape_features_validate_project', featuresProjectValidationInputSchema.shape, readOnlyLocalAnnotations, featuresProjectValidationHandler);
|
|
7219
7249
|
server.tool('ductape_frontend_analytics_validate_project', frontendAnalyticsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, frontendAnalyticsProjectValidationHandler);
|
|
7250
|
+
server.tool('ductape_sessions_validate_project', sessionProjectValidationInputSchema.shape, readOnlyLocalAnnotations, sessionProjectValidationHandler);
|
|
7220
7251
|
server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupAnnotations, portableFunctionSetupHandler);
|
|
7221
7252
|
server.tool('ductape_redis_setup', redisSetupInputSchema.shape, redisSetupHandler);
|
|
7222
7253
|
server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
|