@ductape/mcp 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -95,6 +95,12 @@ The server exposes runtime, schema, documentation, CLI, discovery, migration, an
|
|
|
95
95
|
- Agents must implement and verify the route; they must not claim remote availability from local registration alone.
|
|
96
96
|
- Agents must first extract native Ductape primitives from migrated code and reserve Functions for irreducible residual domain logic while preserving original transaction boundaries.
|
|
97
97
|
|
|
98
|
+
6. **`ductape_frontend_analytics_validate_project`**:
|
|
99
|
+
- Read-only audit for deployable applications using `@ductape/react`, `@ductape/vue`, or the browser client.
|
|
100
|
+
- Requires an enabled, deferred, or prohibited decision per application in `ductape/analytics/frontend.json`.
|
|
101
|
+
- When enabled, checks analytics usage, authenticated identity lifecycle, router pageviews, named events, client-failure signals, and auto-capture privacy review.
|
|
102
|
+
- Reports actionable findings without rewriting application source.
|
|
103
|
+
|
|
98
104
|
The `ductape_cli` MCP tool also exposes public app discovery:
|
|
99
105
|
`marketplace search <capability>`, `marketplace categories`, and
|
|
100
106
|
`marketplace get <app_tag>`. Inspect the app before generating or executing an action payload.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type AnalyticsDecisionStatus = 'enabled' | 'deferred' | 'prohibited';
|
|
2
|
+
interface AnalyticsDecision {
|
|
3
|
+
status?: AnalyticsDecisionStatus;
|
|
4
|
+
rationale?: string;
|
|
5
|
+
autoCapture?: 'disabled' | 'reviewed';
|
|
6
|
+
privacyReviewed?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface FrontendAnalyticsValidationResult {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
decisionFile: string;
|
|
11
|
+
summary: {
|
|
12
|
+
applications: number;
|
|
13
|
+
errors: number;
|
|
14
|
+
warnings: number;
|
|
15
|
+
};
|
|
16
|
+
applications: Array<{
|
|
17
|
+
path: string;
|
|
18
|
+
framework: 'react' | 'vue' | 'client';
|
|
19
|
+
decision?: AnalyticsDecision;
|
|
20
|
+
signals: Record<string, boolean>;
|
|
21
|
+
evidence: Record<string, string[]>;
|
|
22
|
+
errors: string[];
|
|
23
|
+
warnings: string[];
|
|
24
|
+
}>;
|
|
25
|
+
remediation: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function validateFrontendAnalyticsProject(projectDir: string): FrontendAnalyticsValidationResult;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=frontend-analytics-validator.d.ts.map
|
|
@@ -0,0 +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;CAC3B;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,CA2HnC"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
4
|
+
'.git', '.next', '.nuxt', '.output', 'build', 'coverage', 'dist', 'node_modules',
|
|
5
|
+
]);
|
|
6
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.vue']);
|
|
7
|
+
const MAX_SOURCE_BYTES = 1_000_000;
|
|
8
|
+
function walk(root, visit) {
|
|
9
|
+
if (!fs.existsSync(root))
|
|
10
|
+
return;
|
|
11
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
12
|
+
if (entry.isSymbolicLink())
|
|
13
|
+
continue;
|
|
14
|
+
const absolute = path.join(root, entry.name);
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
if (!IGNORED_DIRECTORIES.has(entry.name))
|
|
17
|
+
walk(absolute, visit);
|
|
18
|
+
}
|
|
19
|
+
else if (entry.isFile()) {
|
|
20
|
+
visit(absolute);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function readJson(file) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
function packageFramework(manifest) {
|
|
28
|
+
const runtimeDependencies = manifest.dependencies ?? {};
|
|
29
|
+
const dependencies = {
|
|
30
|
+
...runtimeDependencies,
|
|
31
|
+
...(manifest.devDependencies ?? {}),
|
|
32
|
+
};
|
|
33
|
+
const scripts = Object.values(manifest.scripts ?? {}).join(' ');
|
|
34
|
+
const isApplication = Boolean(runtimeDependencies['react-dom'] ||
|
|
35
|
+
runtimeDependencies.vue ||
|
|
36
|
+
runtimeDependencies.svelte ||
|
|
37
|
+
runtimeDependencies['@angular/core'] ||
|
|
38
|
+
/(?:^|\s)(?:vite|next|nuxt|react-scripts)(?:\s|$)/.test(scripts) ||
|
|
39
|
+
manifest.browser);
|
|
40
|
+
if (!isApplication)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (dependencies['@ductape/react'])
|
|
43
|
+
return 'react';
|
|
44
|
+
if (dependencies['@ductape/vue'])
|
|
45
|
+
return 'vue';
|
|
46
|
+
if (dependencies['@ductape/client'] && (dependencies.react || dependencies.vue))
|
|
47
|
+
return 'client';
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function scanSource(projectRoot, roots) {
|
|
51
|
+
const chunks = [];
|
|
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;
|
|
62
|
+
}
|
|
63
|
+
function normalizedRelative(root, target) {
|
|
64
|
+
return path.relative(root, target).split(path.sep).join('/') || '.';
|
|
65
|
+
}
|
|
66
|
+
export function validateFrontendAnalyticsProject(projectDir) {
|
|
67
|
+
const root = path.resolve(projectDir);
|
|
68
|
+
if (!path.isAbsolute(projectDir) || !fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
69
|
+
throw new Error('project_dir must be an existing absolute project directory.');
|
|
70
|
+
}
|
|
71
|
+
const decisionFile = path.join(root, 'ductape', 'analytics', 'frontend.json');
|
|
72
|
+
let decisions = {};
|
|
73
|
+
let decisionError;
|
|
74
|
+
if (fs.existsSync(decisionFile)) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = readJson(decisionFile);
|
|
77
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
78
|
+
throw new Error('top level must be an object');
|
|
79
|
+
}
|
|
80
|
+
decisions = parsed;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
decisionError = `Invalid decision file: ${error instanceof Error ? error.message : String(error)}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const packageFiles = [];
|
|
87
|
+
walk(root, (file) => {
|
|
88
|
+
if (path.basename(file) === 'package.json')
|
|
89
|
+
packageFiles.push(file);
|
|
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
|
+
}
|
|
100
|
+
const applications = packageFiles.flatMap((manifestFile) => {
|
|
101
|
+
let manifest;
|
|
102
|
+
try {
|
|
103
|
+
manifest = readJson(manifestFile);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const framework = packageFramework(manifest);
|
|
109
|
+
if (!framework)
|
|
110
|
+
return [];
|
|
111
|
+
const applicationRoot = path.dirname(manifestFile);
|
|
112
|
+
const applicationPath = normalizedRelative(root, applicationRoot);
|
|
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);
|
|
136
|
+
const decision = decisions.applications?.[applicationPath];
|
|
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
|
+
};
|
|
144
|
+
const evidence = Object.fromEntries(Object.entries(patterns).map(([name, pattern]) => [name, files.filter((item) => pattern.test(item.source)).map((item) => item.file)]));
|
|
145
|
+
const signals = Object.fromEntries(Object.entries(evidence).map(([name, matches]) => [name, matches.length > 0]));
|
|
146
|
+
const errors = [];
|
|
147
|
+
const warnings = [];
|
|
148
|
+
if (decisionError)
|
|
149
|
+
errors.push(decisionError);
|
|
150
|
+
if (!decision) {
|
|
151
|
+
errors.push(`Record this application in ${normalizedRelative(root, decisionFile)}.`);
|
|
152
|
+
}
|
|
153
|
+
else if (!['enabled', 'deferred', 'prohibited'].includes(String(decision.status))) {
|
|
154
|
+
errors.push('Decision status must be enabled, deferred, or prohibited.');
|
|
155
|
+
}
|
|
156
|
+
else if (decision.status !== 'enabled' && !decision.rationale?.trim()) {
|
|
157
|
+
errors.push(`${decision.status} analytics requires a non-empty rationale.`);
|
|
158
|
+
}
|
|
159
|
+
else if (decision.status === 'enabled') {
|
|
160
|
+
if (!decision.privacyReviewed)
|
|
161
|
+
errors.push('Enabled analytics requires privacyReviewed: true.');
|
|
162
|
+
if (!signals.analytics)
|
|
163
|
+
errors.push('No Ductape analytics API usage was found.');
|
|
164
|
+
if (!signals.pageview)
|
|
165
|
+
errors.push('No initial/router pageview lifecycle was found.');
|
|
166
|
+
if (!signals.namedEvents)
|
|
167
|
+
errors.push('No reviewed named product event was found.');
|
|
168
|
+
if (signals.authenticatedSessions && !signals.identify) {
|
|
169
|
+
errors.push('Authenticated sessions exist but identify(fullSessionToken) was not found.');
|
|
170
|
+
}
|
|
171
|
+
if (signals.authenticatedSessions && !signals.clearSession) {
|
|
172
|
+
errors.push('Authenticated sessions exist but clearSession() was not found.');
|
|
173
|
+
}
|
|
174
|
+
if (signals.autoCapture && decision.autoCapture !== 'reviewed') {
|
|
175
|
+
errors.push('Auto-capture is enabled without a recorded reviewed decision.');
|
|
176
|
+
}
|
|
177
|
+
if (!signals.clientFailure) {
|
|
178
|
+
warnings.push('No sanitized client-failure analytics signal was detected; review error boundaries and unhandled failures.');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return [{ path: applicationPath, framework, decision, signals, evidence, errors, warnings }];
|
|
182
|
+
});
|
|
183
|
+
const errors = applications.reduce((count, app) => count + app.errors.length, 0);
|
|
184
|
+
const warnings = applications.reduce((count, app) => count + app.warnings.length, 0);
|
|
185
|
+
return {
|
|
186
|
+
valid: errors === 0,
|
|
187
|
+
decisionFile,
|
|
188
|
+
summary: { applications: applications.length, errors, warnings },
|
|
189
|
+
applications,
|
|
190
|
+
remediation: [
|
|
191
|
+
'Read ductape_docs({ topic: "frontend-analytics" }) before changing application code.',
|
|
192
|
+
'Create ductape/analytics/frontend.json with one applications entry per reported relative path.',
|
|
193
|
+
'Choose enabled, deferred, or prohibited explicitly; never treat silent omission as completion.',
|
|
194
|
+
'When enabled, keep auto-capture off until consent, masking, selectors, and sensitive states are reviewed.',
|
|
195
|
+
'This validator is read-only and must not be used as an automatic codemod.',
|
|
196
|
+
],
|
|
197
|
+
};
|
|
198
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { normalizeLiveActionContract } from './action-contract.js';
|
|
|
19
19
|
import { runtimeInputRecovery } from './runtime-recovery.js';
|
|
20
20
|
import { executeViaProxy, generateExecutablePayload, getAssetSchemas, } from './proxy-client.js';
|
|
21
21
|
import { EVENTS_DELIVERY_SEMANTICS, EVENTS_IMPLEMENTATION_WARNING, eventsCapabilityOverview, inspectEventsComponent, searchEventsCapabilities, } from './events-capabilities.js';
|
|
22
|
+
import { validateFrontendAnalyticsProject } from './frontend-analytics-validator.js';
|
|
22
23
|
const MODULES = [
|
|
23
24
|
'product', 'app', 'databases', 'graph', 'webhooks', 'notifications',
|
|
24
25
|
'events', 'messageBrokers', 'storage', 'vector', 'caches', 'sessions', 'quotas',
|
|
@@ -365,6 +366,9 @@ SETUP — register once in AppModule:
|
|
|
365
366
|
env: process.env.NODE_ENV === 'production' ? 'prd' : 'snd',
|
|
366
367
|
redisUrl: process.env.DUCTAPE_REDIS_URL, // required — no dispatch() works without this
|
|
367
368
|
runtimeSync: { intervalMs: 30_000, jitter: 0.2 },
|
|
369
|
+
// Resolve only the original opaque session retained by the authentication guard.
|
|
370
|
+
sessionResolver: (context) =>
|
|
371
|
+
context.switchToHttp().getRequest().actor?.ductapeSession,
|
|
368
372
|
}),
|
|
369
373
|
],
|
|
370
374
|
})
|
|
@@ -380,9 +384,22 @@ SETUP — register once in AppModule:
|
|
|
380
384
|
env: cfg.get('DUCTAPE_ENV'),
|
|
381
385
|
redisUrl: cfg.get('DUCTAPE_REDIS_URL'), // required — no dispatch() works without this
|
|
382
386
|
runtimeSync: { intervalMs: 30_000, jitter: 0.2 },
|
|
387
|
+
sessionResolver: (context) =>
|
|
388
|
+
context.switchToHttp().getRequest().actor?.ductapeSession,
|
|
383
389
|
}),
|
|
384
390
|
})
|
|
385
391
|
|
|
392
|
+
SESSION-AWARE NESTJS EXECUTION:
|
|
393
|
+
Configure sessionResolver once. It runs after guards and stores the returned opaque token in
|
|
394
|
+
request-scoped context. @Feature.Execute, @Feature.Dispatch, @Api, @ApiDispatch, Events,
|
|
395
|
+
notifications, jobs, agents, and session-capable injected resource handles inherit it without
|
|
396
|
+
adding it to business input. Do not return a session from a decorated method or put it in DTOs.
|
|
397
|
+
The resolver must return the exact original "session-tag:jwt", not decoded claims or a bearer
|
|
398
|
+
header. For intentional webhook, scheduled, or broker system work, set
|
|
399
|
+
sessionContext: "system" on session-capable decorators; this suppresses request inheritance.
|
|
400
|
+
Check the installed @ductape/nestjs types before generating this configuration: 0.1.12 does not
|
|
401
|
+
yet contain sessionResolver/sessionContext and must be upgraded to the next published release.
|
|
402
|
+
|
|
386
403
|
Environment variable (add to .env and deployment secrets):
|
|
387
404
|
DUCTAPE_REDIS_URL=redis://localhost:6379 # local dev
|
|
388
405
|
DUCTAPE_REDIS_URL=rediss://:<password>@host:6380 # managed Redis (TLS)
|
|
@@ -1317,7 +1334,7 @@ function buildSdkInvocationArgs(payload) {
|
|
|
1317
1334
|
function operationAcceptsSession(operationFamily, method) {
|
|
1318
1335
|
const family = operationFamily.toLowerCase();
|
|
1319
1336
|
const sessionFamilies = new Set([
|
|
1320
|
-
'action', 'features', 'feature', 'database', 'graph', 'vector', 'storage',
|
|
1337
|
+
'action', 'api', 'app', 'apps', 'features', 'feature', 'database', 'graph', 'vector', 'storage',
|
|
1321
1338
|
'notification', 'messaging', 'broker', 'events', 'event', 'quota', 'fallback',
|
|
1322
1339
|
]);
|
|
1323
1340
|
if (!sessionFamilies.has(family))
|
|
@@ -1450,7 +1467,7 @@ function buildDatabaseActionFeatureExample(targets, contract) {
|
|
|
1450
1467
|
let authState = 'unknown';
|
|
1451
1468
|
let workspaceSynced = false;
|
|
1452
1469
|
const ADMIN_SUBCOMMANDS = [
|
|
1453
|
-
'login', 'logout', 'whoami',
|
|
1470
|
+
'login', 'logout', 'whoami', 'doctor',
|
|
1454
1471
|
'profiles',
|
|
1455
1472
|
'workspaces',
|
|
1456
1473
|
'link', 'unlink', 'init',
|
|
@@ -1703,6 +1720,12 @@ const eventsProjectValidationInputSchema = z.object({
|
|
|
1703
1720
|
const featuresProjectValidationInputSchema = z.object({
|
|
1704
1721
|
project_dir: z.string().describe('Absolute path to the linked Ductape project containing ductape/features/. The validator is read-only.'),
|
|
1705
1722
|
});
|
|
1723
|
+
const frontendAnalyticsProjectValidationInputSchema = z.object({
|
|
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
|
+
});
|
|
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
|
+
});
|
|
1706
1729
|
const readOnlyLocalAnnotations = {
|
|
1707
1730
|
readOnlyHint: true,
|
|
1708
1731
|
destructiveHint: false,
|
|
@@ -2328,13 +2351,13 @@ PARITY CLAIMS
|
|
|
2328
2351
|
DUCTAPE FRONTEND SDK GUIDE
|
|
2329
2352
|
|
|
2330
2353
|
Choose one integration package:
|
|
2331
|
-
React 17+ — npm install @ductape/react
|
|
2354
|
+
React 17+ — npm install @ductape/react@latest
|
|
2332
2355
|
Provider and hooks built on @ductape/client.
|
|
2333
2356
|
Continue with ductape_docs({ topic: "react" }).
|
|
2334
|
-
Vue 3+ — npm install @ductape/vue
|
|
2357
|
+
Vue 3+ — npm install @ductape/vue@latest
|
|
2335
2358
|
Plugin and composables built on @ductape/client.
|
|
2336
2359
|
Continue with ductape_docs({ topic: "vue" }).
|
|
2337
|
-
Other UI — npm install @ductape/client
|
|
2360
|
+
Other UI — npm install @ductape/client@latest
|
|
2338
2361
|
frameworks Use directly with Svelte, Angular, vanilla JavaScript, or a custom adapter.
|
|
2339
2362
|
Continue with ductape_docs({ topic: "client" }).
|
|
2340
2363
|
|
|
@@ -2359,8 +2382,14 @@ SHARED APPLICATION LIFECYCLE
|
|
|
2359
2382
|
|
|
2360
2383
|
PRODUCT ANALYTICS
|
|
2361
2384
|
Frontend product analytics complements backend session propagation; neither replaces the other.
|
|
2362
|
-
|
|
2363
|
-
|
|
2385
|
+
This is a required design decision for every Ductape frontend, not an optional afterthought.
|
|
2386
|
+
Always continue with ductape_docs({ topic: "frontend-analytics" }), then run
|
|
2387
|
+
ductape_frontend_analytics_validate_project({ project_dir: "<absolute-project-root>" }). Record
|
|
2388
|
+
one explicit enabled/deferred/prohibited choice per detected application in
|
|
2389
|
+
ductape/analytics/frontend.json. Silent omission is a failed frontend build/audit outcome.
|
|
2390
|
+
When enabled, wire identity lifecycle, router pageviews, reviewed named events, sanitized client
|
|
2391
|
+
failures, privacy masking, hidden-state safety, and trace correlation. Keep auto-capture off until
|
|
2392
|
+
consent, selectors, masking, and sensitive states have been reviewed.
|
|
2364
2393
|
|
|
2365
2394
|
AUTHENTICATION AND SECURITY
|
|
2366
2395
|
- Use publishableKey in browser applications. Never ship workspace private keys or privileged
|
|
@@ -3545,6 +3574,21 @@ SESSION PROPAGATION
|
|
|
3545
3574
|
Feature execution should receive the same explicit actor classification. Never log, serialize
|
|
3546
3575
|
into business payloads, or attach the raw session/refresh token to traces or error messages.
|
|
3547
3576
|
|
|
3577
|
+
FAIL-CLOSED NORMAL SDK RULE
|
|
3578
|
+
Before emitting or approving ordinary @ductape/sdk code for immediate authenticated work, verify
|
|
3579
|
+
that every session-capable runtime call receives the exact ActorContext.session through its
|
|
3580
|
+
top-level session option. This includes api.run/api.dispatch, feature.execute/feature.dispatch,
|
|
3581
|
+
database primitives and saved actions, graph/vector primitives and saved actions, storage,
|
|
3582
|
+
Events, notifications, agents, quotas, and fallbacks where the installed types accept session.
|
|
3583
|
+
Never substitute a session tag, decoded claims, user ID, Authorization header, refresh token, or
|
|
3584
|
+
business-input field. If the original opaque token is unavailable, stop and report the missing
|
|
3585
|
+
propagation boundary; do not silently emit an unattributed user-context call. The only omission
|
|
3586
|
+
allowed is an explicitly classified system context or a version-verified approved delegated flow.
|
|
3587
|
+
|
|
3588
|
+
A Feature receives the session at its execution boundary. Its nested ctx.api, ctx.database,
|
|
3589
|
+
ctx.graph, ctx.vector, ctx.storage, ctx.events, and ctx.notifications calls inherit ctx.session;
|
|
3590
|
+
do not serialize ctx.session into their input objects or hardcode it in the Feature definition.
|
|
3591
|
+
|
|
3548
3592
|
SECURITY BOUNDARY FOR DURABLE WORK
|
|
3549
3593
|
The current SDK accepts a session string on dispatch, but it does not expose an immutable
|
|
3550
3594
|
attribution snapshot API, an expired-token attribution contract, or a general delegated-session
|
|
@@ -5067,7 +5111,7 @@ connect to the proxy's /realtime gateway. Never use this package on the server
|
|
|
5067
5111
|
for server-side code.
|
|
5068
5112
|
|
|
5069
5113
|
INSTALL
|
|
5070
|
-
npm install @ductape/client
|
|
5114
|
+
npm install @ductape/client@latest
|
|
5071
5115
|
|
|
5072
5116
|
INITIALIZATION
|
|
5073
5117
|
import { createClient } from '@ductape/client';
|
|
@@ -5230,7 +5274,7 @@ React hooks and context provider for Ductape. Wraps @ductape/client.
|
|
|
5230
5274
|
Requires react >= 17.
|
|
5231
5275
|
|
|
5232
5276
|
INSTALL
|
|
5233
|
-
npm install @ductape/react
|
|
5277
|
+
npm install @ductape/react@latest
|
|
5234
5278
|
|
|
5235
5279
|
SETUP — wrap your app root with DuctapeProvider
|
|
5236
5280
|
import { DuctapeProvider } from '@ductape/react';
|
|
@@ -5434,7 +5478,7 @@ Vue 3 composables and plugin for Ductape. Wraps @ductape/client.
|
|
|
5434
5478
|
Requires vue >= 3.
|
|
5435
5479
|
|
|
5436
5480
|
INSTALL
|
|
5437
|
-
npm install @ductape/vue
|
|
5481
|
+
npm install @ductape/vue@latest
|
|
5438
5482
|
|
|
5439
5483
|
SETUP — install the plugin at app root
|
|
5440
5484
|
// main.ts
|
|
@@ -6047,6 +6091,28 @@ const featuresProjectValidationHandler = async (args) => {
|
|
|
6047
6091
|
...(result.success ? {} : { isError: true }),
|
|
6048
6092
|
};
|
|
6049
6093
|
};
|
|
6094
|
+
const frontendAnalyticsProjectValidationHandler = async (args) => {
|
|
6095
|
+
try {
|
|
6096
|
+
const result = validateFrontendAnalyticsProject(args.project_dir);
|
|
6097
|
+
return {
|
|
6098
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
6099
|
+
...(result.valid ? {} : { isError: true }),
|
|
6100
|
+
};
|
|
6101
|
+
}
|
|
6102
|
+
catch (error) {
|
|
6103
|
+
return {
|
|
6104
|
+
content: [{
|
|
6105
|
+
type: 'text',
|
|
6106
|
+
text: error instanceof Error ? error.message : String(error),
|
|
6107
|
+
}],
|
|
6108
|
+
isError: true,
|
|
6109
|
+
};
|
|
6110
|
+
}
|
|
6111
|
+
};
|
|
6112
|
+
const sessionProjectValidationHandler = async (args) => {
|
|
6113
|
+
const result = runCli('sessions validate --json', args.project_dir);
|
|
6114
|
+
return { content: [{ type: 'text', text: result.output || '(no output)' }], ...(result.success ? {} : { isError: true }) };
|
|
6115
|
+
};
|
|
6050
6116
|
const cliInputSchema = z.object({
|
|
6051
6117
|
command: z.string().describe('The ductape CLI command to run, without the leading "ductape" word. ' +
|
|
6052
6118
|
'Examples: "products list", "products create --name \\"My Product\\" --tag my-product", ' +
|
|
@@ -6898,6 +6964,21 @@ async function main() {
|
|
|
6898
6964
|
inputSchema: featuresProjectValidationInputSchema,
|
|
6899
6965
|
annotations: readOnlyLocalAnnotations,
|
|
6900
6966
|
}, featuresProjectValidationHandler);
|
|
6967
|
+
server.registerTool('ductape_frontend_analytics_validate_project', {
|
|
6968
|
+
title: 'Validate Ductape Frontend Analytics',
|
|
6969
|
+
description: 'Read-only audit of every frontend package using @ductape/react, @ductape/vue, or the browser client. ' +
|
|
6970
|
+
'Requires an explicit enabled/deferred/prohibited decision per application and validates analytics identity, ' +
|
|
6971
|
+
'logout, pageview, named-event, client-failure, and auto-capture privacy signals when enabled. ' +
|
|
6972
|
+
'Returns actionable findings and never modifies source.',
|
|
6973
|
+
inputSchema: frontendAnalyticsProjectValidationInputSchema,
|
|
6974
|
+
annotations: readOnlyLocalAnnotations,
|
|
6975
|
+
}, frontendAnalyticsProjectValidationHandler);
|
|
6976
|
+
server.registerTool('ductape_sessions_validate_project', {
|
|
6977
|
+
title: 'Validate Ductape Session Propagation',
|
|
6978
|
+
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.',
|
|
6979
|
+
inputSchema: sessionProjectValidationInputSchema,
|
|
6980
|
+
annotations: readOnlyLocalAnnotations,
|
|
6981
|
+
}, sessionProjectValidationHandler);
|
|
6901
6982
|
server.registerTool('ductape_function_setup', {
|
|
6902
6983
|
title: 'Ductape Portable Function Setup',
|
|
6903
6984
|
description: 'Read-only: generate (without applying) the mandatory secure local + remote runtime setup for application functions used by Features. ' +
|
|
@@ -7148,6 +7229,8 @@ async function main() {
|
|
|
7148
7229
|
server.tool('ductape_events_topic_setup', eventsTopicSetupInputSchema.shape, readOnlyLocalAnnotations, eventsTopicSetupHandler);
|
|
7149
7230
|
server.tool('ductape_events_validate_project', eventsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, eventsProjectValidationHandler);
|
|
7150
7231
|
server.tool('ductape_features_validate_project', featuresProjectValidationInputSchema.shape, readOnlyLocalAnnotations, featuresProjectValidationHandler);
|
|
7232
|
+
server.tool('ductape_frontend_analytics_validate_project', frontendAnalyticsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, frontendAnalyticsProjectValidationHandler);
|
|
7233
|
+
server.tool('ductape_sessions_validate_project', sessionProjectValidationInputSchema.shape, readOnlyLocalAnnotations, sessionProjectValidationHandler);
|
|
7151
7234
|
server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupAnnotations, portableFunctionSetupHandler);
|
|
7152
7235
|
server.tool('ductape_redis_setup', redisSetupInputSchema.shape, redisSetupHandler);
|
|
7153
7236
|
server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
|
package/docs/TOOLS.md
CHANGED
|
@@ -74,6 +74,19 @@ manifest, envelope registry, or custom event registry.
|
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
77
|
+
## Tool: `ductape_frontend_analytics_validate_project`
|
|
78
|
+
|
|
79
|
+
Audits every deployable Ductape React, Vue, or browser-client application under an absolute project
|
|
80
|
+
directory. Each application must have an explicit entry in `ductape/analytics/frontend.json` with
|
|
81
|
+
status `enabled`, `deferred`, or `prohibited`. Deferred and prohibited choices require a rationale.
|
|
82
|
+
|
|
83
|
+
Enabled applications are checked for Ductape analytics usage, router pageviews, named events,
|
|
84
|
+
`identify(fullSessionToken)` after authenticated session creation/refresh, `clearSession()` on
|
|
85
|
+
logout/account switching, sanitized client-failure telemetry, and reviewed auto-capture. The tool
|
|
86
|
+
is read-only and never edits application source.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
77
90
|
## Tool: `ductape_function_setup`
|
|
78
91
|
|
|
79
92
|
Produces the mandatory local registry and signed HTTPS exposure plan for portable application
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ductape/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "MCP server that exposes Ductape SDK operations via the backend proxy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc",
|
|
18
|
-
"test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-paystack-action-schema.mjs && node scripts/check-portable-functions.mjs && node scripts/check-feature-control-flow.mjs && node scripts/check-project-link-guidance.mjs && node scripts/check-graph-vector-projection-guidance.mjs && node scripts/check-asset-file-guidance.mjs && node scripts/check-database-action-contract-guidance.mjs && node scripts/check-runtime-sync-guidance.mjs && node scripts/check-runtime-input-recovery.mjs && node scripts/check-resilience-health-guidance.mjs",
|
|
18
|
+
"test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-doctor-e2e.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-frontend-analytics-validator.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-paystack-action-schema.mjs && node scripts/check-portable-functions.mjs && node scripts/check-feature-control-flow.mjs && node scripts/check-project-link-guidance.mjs && node scripts/check-graph-vector-projection-guidance.mjs && node scripts/check-asset-file-guidance.mjs && node scripts/check-database-action-contract-guidance.mjs && node scripts/check-runtime-sync-guidance.mjs && node scripts/check-runtime-input-recovery.mjs && node scripts/check-resilience-health-guidance.mjs && node scripts/check-session-propagation-guidance.mjs",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"dev": "tsx src/index.ts"
|
|
21
21
|
},
|