@ductape/mcp 0.3.3 → 0.3.4
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,28 @@
|
|
|
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
|
+
errors: string[];
|
|
22
|
+
warnings: string[];
|
|
23
|
+
}>;
|
|
24
|
+
remediation: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function validateFrontendAnalyticsProject(projectDir: string): FrontendAnalyticsValidationResult;
|
|
27
|
+
export {};
|
|
28
|
+
//# 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,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,CAkGnC"}
|
|
@@ -0,0 +1,168 @@
|
|
|
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(applicationRoot) {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
walk(applicationRoot, (file) => {
|
|
53
|
+
if (!SOURCE_EXTENSIONS.has(path.extname(file)))
|
|
54
|
+
return;
|
|
55
|
+
const stats = fs.statSync(file);
|
|
56
|
+
if (stats.size > MAX_SOURCE_BYTES)
|
|
57
|
+
return;
|
|
58
|
+
chunks.push(fs.readFileSync(file, 'utf8'));
|
|
59
|
+
});
|
|
60
|
+
return chunks.join('\n');
|
|
61
|
+
}
|
|
62
|
+
function normalizedRelative(root, target) {
|
|
63
|
+
return path.relative(root, target).split(path.sep).join('/') || '.';
|
|
64
|
+
}
|
|
65
|
+
export function validateFrontendAnalyticsProject(projectDir) {
|
|
66
|
+
const root = path.resolve(projectDir);
|
|
67
|
+
if (!path.isAbsolute(projectDir) || !fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
68
|
+
throw new Error('project_dir must be an existing absolute project directory.');
|
|
69
|
+
}
|
|
70
|
+
const decisionFile = path.join(root, 'ductape', 'analytics', 'frontend.json');
|
|
71
|
+
let decisions = {};
|
|
72
|
+
let decisionError;
|
|
73
|
+
if (fs.existsSync(decisionFile)) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = readJson(decisionFile);
|
|
76
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
77
|
+
throw new Error('top level must be an object');
|
|
78
|
+
}
|
|
79
|
+
decisions = parsed;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
decisionError = `Invalid decision file: ${error instanceof Error ? error.message : String(error)}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const packageFiles = [];
|
|
86
|
+
walk(root, (file) => {
|
|
87
|
+
if (path.basename(file) === 'package.json')
|
|
88
|
+
packageFiles.push(file);
|
|
89
|
+
});
|
|
90
|
+
const applications = packageFiles.flatMap((manifestFile) => {
|
|
91
|
+
let manifest;
|
|
92
|
+
try {
|
|
93
|
+
manifest = readJson(manifestFile);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const framework = packageFramework(manifest);
|
|
99
|
+
if (!framework)
|
|
100
|
+
return [];
|
|
101
|
+
const applicationRoot = path.dirname(manifestFile);
|
|
102
|
+
const applicationPath = normalizedRelative(root, applicationRoot);
|
|
103
|
+
const source = scanSource(applicationRoot);
|
|
104
|
+
const decision = decisions.applications?.[applicationPath];
|
|
105
|
+
const signals = {
|
|
106
|
+
provider: /DuctapeProvider/.test(source),
|
|
107
|
+
authenticatedSessions: /useSession|\.sessions\.|sessionToken|refreshToken/.test(source),
|
|
108
|
+
analytics: /useAnalytics|\.analytics\./.test(source),
|
|
109
|
+
identify: /(?:analytics\.)?identify\s*\(/.test(source),
|
|
110
|
+
clearSession: /(?:analytics\.)?clearSession\s*\(/.test(source),
|
|
111
|
+
pageview: /(?:analytics\.)?pageview\s*\(/.test(source),
|
|
112
|
+
namedEvents: /(?:analytics\.)?track\s*\(/.test(source),
|
|
113
|
+
autoCapture: /enableAutoCapture\s*\(/.test(source),
|
|
114
|
+
clientFailure: /client[_\-. ]error|frontend[_\-. ]error|error_boundary|unhandledrejection/i.test(source),
|
|
115
|
+
};
|
|
116
|
+
const errors = [];
|
|
117
|
+
const warnings = [];
|
|
118
|
+
if (decisionError)
|
|
119
|
+
errors.push(decisionError);
|
|
120
|
+
if (!decision) {
|
|
121
|
+
errors.push(`Record this application in ${normalizedRelative(root, decisionFile)}.`);
|
|
122
|
+
}
|
|
123
|
+
else if (!['enabled', 'deferred', 'prohibited'].includes(String(decision.status))) {
|
|
124
|
+
errors.push('Decision status must be enabled, deferred, or prohibited.');
|
|
125
|
+
}
|
|
126
|
+
else if (decision.status !== 'enabled' && !decision.rationale?.trim()) {
|
|
127
|
+
errors.push(`${decision.status} analytics requires a non-empty rationale.`);
|
|
128
|
+
}
|
|
129
|
+
else if (decision.status === 'enabled') {
|
|
130
|
+
if (!decision.privacyReviewed)
|
|
131
|
+
errors.push('Enabled analytics requires privacyReviewed: true.');
|
|
132
|
+
if (!signals.analytics)
|
|
133
|
+
errors.push('No Ductape analytics API usage was found.');
|
|
134
|
+
if (!signals.pageview)
|
|
135
|
+
errors.push('No initial/router pageview lifecycle was found.');
|
|
136
|
+
if (!signals.namedEvents)
|
|
137
|
+
errors.push('No reviewed named product event was found.');
|
|
138
|
+
if (signals.authenticatedSessions && !signals.identify) {
|
|
139
|
+
errors.push('Authenticated sessions exist but identify(fullSessionToken) was not found.');
|
|
140
|
+
}
|
|
141
|
+
if (signals.authenticatedSessions && !signals.clearSession) {
|
|
142
|
+
errors.push('Authenticated sessions exist but clearSession() was not found.');
|
|
143
|
+
}
|
|
144
|
+
if (signals.autoCapture && decision.autoCapture !== 'reviewed') {
|
|
145
|
+
errors.push('Auto-capture is enabled without a recorded reviewed decision.');
|
|
146
|
+
}
|
|
147
|
+
if (!signals.clientFailure) {
|
|
148
|
+
warnings.push('No sanitized client-failure analytics signal was detected; review error boundaries and unhandled failures.');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return [{ path: applicationPath, framework, decision, signals, errors, warnings }];
|
|
152
|
+
});
|
|
153
|
+
const errors = applications.reduce((count, app) => count + app.errors.length, 0);
|
|
154
|
+
const warnings = applications.reduce((count, app) => count + app.warnings.length, 0);
|
|
155
|
+
return {
|
|
156
|
+
valid: errors === 0,
|
|
157
|
+
decisionFile,
|
|
158
|
+
summary: { applications: applications.length, errors, warnings },
|
|
159
|
+
applications,
|
|
160
|
+
remediation: [
|
|
161
|
+
'Read ductape_docs({ topic: "frontend-analytics" }) before changing application code.',
|
|
162
|
+
'Create ductape/analytics/frontend.json with one applications entry per reported relative path.',
|
|
163
|
+
'Choose enabled, deferred, or prohibited explicitly; never treat silent omission as completion.',
|
|
164
|
+
'When enabled, keep auto-capture off until consent, masking, selectors, and sensitive states are reviewed.',
|
|
165
|
+
'This validator is read-only and must not be used as an automatic codemod.',
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
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,9 @@ 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
|
+
});
|
|
1706
1726
|
const readOnlyLocalAnnotations = {
|
|
1707
1727
|
readOnlyHint: true,
|
|
1708
1728
|
destructiveHint: false,
|
|
@@ -2328,13 +2348,13 @@ PARITY CLAIMS
|
|
|
2328
2348
|
DUCTAPE FRONTEND SDK GUIDE
|
|
2329
2349
|
|
|
2330
2350
|
Choose one integration package:
|
|
2331
|
-
React 17+ — npm install @ductape/react
|
|
2351
|
+
React 17+ — npm install @ductape/react@latest
|
|
2332
2352
|
Provider and hooks built on @ductape/client.
|
|
2333
2353
|
Continue with ductape_docs({ topic: "react" }).
|
|
2334
|
-
Vue 3+ — npm install @ductape/vue
|
|
2354
|
+
Vue 3+ — npm install @ductape/vue@latest
|
|
2335
2355
|
Plugin and composables built on @ductape/client.
|
|
2336
2356
|
Continue with ductape_docs({ topic: "vue" }).
|
|
2337
|
-
Other UI — npm install @ductape/client
|
|
2357
|
+
Other UI — npm install @ductape/client@latest
|
|
2338
2358
|
frameworks Use directly with Svelte, Angular, vanilla JavaScript, or a custom adapter.
|
|
2339
2359
|
Continue with ductape_docs({ topic: "client" }).
|
|
2340
2360
|
|
|
@@ -2359,8 +2379,14 @@ SHARED APPLICATION LIFECYCLE
|
|
|
2359
2379
|
|
|
2360
2380
|
PRODUCT ANALYTICS
|
|
2361
2381
|
Frontend product analytics complements backend session propagation; neither replaces the other.
|
|
2362
|
-
|
|
2363
|
-
|
|
2382
|
+
This is a required design decision for every Ductape frontend, not an optional afterthought.
|
|
2383
|
+
Always continue with ductape_docs({ topic: "frontend-analytics" }), then run
|
|
2384
|
+
ductape_frontend_analytics_validate_project({ project_dir: "<absolute-project-root>" }). Record
|
|
2385
|
+
one explicit enabled/deferred/prohibited choice per detected application in
|
|
2386
|
+
ductape/analytics/frontend.json. Silent omission is a failed frontend build/audit outcome.
|
|
2387
|
+
When enabled, wire identity lifecycle, router pageviews, reviewed named events, sanitized client
|
|
2388
|
+
failures, privacy masking, hidden-state safety, and trace correlation. Keep auto-capture off until
|
|
2389
|
+
consent, selectors, masking, and sensitive states have been reviewed.
|
|
2364
2390
|
|
|
2365
2391
|
AUTHENTICATION AND SECURITY
|
|
2366
2392
|
- Use publishableKey in browser applications. Never ship workspace private keys or privileged
|
|
@@ -3545,6 +3571,21 @@ SESSION PROPAGATION
|
|
|
3545
3571
|
Feature execution should receive the same explicit actor classification. Never log, serialize
|
|
3546
3572
|
into business payloads, or attach the raw session/refresh token to traces or error messages.
|
|
3547
3573
|
|
|
3574
|
+
FAIL-CLOSED NORMAL SDK RULE
|
|
3575
|
+
Before emitting or approving ordinary @ductape/sdk code for immediate authenticated work, verify
|
|
3576
|
+
that every session-capable runtime call receives the exact ActorContext.session through its
|
|
3577
|
+
top-level session option. This includes api.run/api.dispatch, feature.execute/feature.dispatch,
|
|
3578
|
+
database primitives and saved actions, graph/vector primitives and saved actions, storage,
|
|
3579
|
+
Events, notifications, agents, quotas, and fallbacks where the installed types accept session.
|
|
3580
|
+
Never substitute a session tag, decoded claims, user ID, Authorization header, refresh token, or
|
|
3581
|
+
business-input field. If the original opaque token is unavailable, stop and report the missing
|
|
3582
|
+
propagation boundary; do not silently emit an unattributed user-context call. The only omission
|
|
3583
|
+
allowed is an explicitly classified system context or a version-verified approved delegated flow.
|
|
3584
|
+
|
|
3585
|
+
A Feature receives the session at its execution boundary. Its nested ctx.api, ctx.database,
|
|
3586
|
+
ctx.graph, ctx.vector, ctx.storage, ctx.events, and ctx.notifications calls inherit ctx.session;
|
|
3587
|
+
do not serialize ctx.session into their input objects or hardcode it in the Feature definition.
|
|
3588
|
+
|
|
3548
3589
|
SECURITY BOUNDARY FOR DURABLE WORK
|
|
3549
3590
|
The current SDK accepts a session string on dispatch, but it does not expose an immutable
|
|
3550
3591
|
attribution snapshot API, an expired-token attribution contract, or a general delegated-session
|
|
@@ -5067,7 +5108,7 @@ connect to the proxy's /realtime gateway. Never use this package on the server
|
|
|
5067
5108
|
for server-side code.
|
|
5068
5109
|
|
|
5069
5110
|
INSTALL
|
|
5070
|
-
npm install @ductape/client
|
|
5111
|
+
npm install @ductape/client@latest
|
|
5071
5112
|
|
|
5072
5113
|
INITIALIZATION
|
|
5073
5114
|
import { createClient } from '@ductape/client';
|
|
@@ -5230,7 +5271,7 @@ React hooks and context provider for Ductape. Wraps @ductape/client.
|
|
|
5230
5271
|
Requires react >= 17.
|
|
5231
5272
|
|
|
5232
5273
|
INSTALL
|
|
5233
|
-
npm install @ductape/react
|
|
5274
|
+
npm install @ductape/react@latest
|
|
5234
5275
|
|
|
5235
5276
|
SETUP — wrap your app root with DuctapeProvider
|
|
5236
5277
|
import { DuctapeProvider } from '@ductape/react';
|
|
@@ -5434,7 +5475,7 @@ Vue 3 composables and plugin for Ductape. Wraps @ductape/client.
|
|
|
5434
5475
|
Requires vue >= 3.
|
|
5435
5476
|
|
|
5436
5477
|
INSTALL
|
|
5437
|
-
npm install @ductape/vue
|
|
5478
|
+
npm install @ductape/vue@latest
|
|
5438
5479
|
|
|
5439
5480
|
SETUP — install the plugin at app root
|
|
5440
5481
|
// main.ts
|
|
@@ -6047,6 +6088,24 @@ const featuresProjectValidationHandler = async (args) => {
|
|
|
6047
6088
|
...(result.success ? {} : { isError: true }),
|
|
6048
6089
|
};
|
|
6049
6090
|
};
|
|
6091
|
+
const frontendAnalyticsProjectValidationHandler = async (args) => {
|
|
6092
|
+
try {
|
|
6093
|
+
const result = validateFrontendAnalyticsProject(args.project_dir);
|
|
6094
|
+
return {
|
|
6095
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
6096
|
+
...(result.valid ? {} : { isError: true }),
|
|
6097
|
+
};
|
|
6098
|
+
}
|
|
6099
|
+
catch (error) {
|
|
6100
|
+
return {
|
|
6101
|
+
content: [{
|
|
6102
|
+
type: 'text',
|
|
6103
|
+
text: error instanceof Error ? error.message : String(error),
|
|
6104
|
+
}],
|
|
6105
|
+
isError: true,
|
|
6106
|
+
};
|
|
6107
|
+
}
|
|
6108
|
+
};
|
|
6050
6109
|
const cliInputSchema = z.object({
|
|
6051
6110
|
command: z.string().describe('The ductape CLI command to run, without the leading "ductape" word. ' +
|
|
6052
6111
|
'Examples: "products list", "products create --name \\"My Product\\" --tag my-product", ' +
|
|
@@ -6898,6 +6957,15 @@ async function main() {
|
|
|
6898
6957
|
inputSchema: featuresProjectValidationInputSchema,
|
|
6899
6958
|
annotations: readOnlyLocalAnnotations,
|
|
6900
6959
|
}, featuresProjectValidationHandler);
|
|
6960
|
+
server.registerTool('ductape_frontend_analytics_validate_project', {
|
|
6961
|
+
title: 'Validate Ductape Frontend Analytics',
|
|
6962
|
+
description: 'Read-only audit of every frontend package using @ductape/react, @ductape/vue, or the browser client. ' +
|
|
6963
|
+
'Requires an explicit enabled/deferred/prohibited decision per application and validates analytics identity, ' +
|
|
6964
|
+
'logout, pageview, named-event, client-failure, and auto-capture privacy signals when enabled. ' +
|
|
6965
|
+
'Returns actionable findings and never modifies source.',
|
|
6966
|
+
inputSchema: frontendAnalyticsProjectValidationInputSchema,
|
|
6967
|
+
annotations: readOnlyLocalAnnotations,
|
|
6968
|
+
}, frontendAnalyticsProjectValidationHandler);
|
|
6901
6969
|
server.registerTool('ductape_function_setup', {
|
|
6902
6970
|
title: 'Ductape Portable Function Setup',
|
|
6903
6971
|
description: 'Read-only: generate (without applying) the mandatory secure local + remote runtime setup for application functions used by Features. ' +
|
|
@@ -7148,6 +7216,7 @@ async function main() {
|
|
|
7148
7216
|
server.tool('ductape_events_topic_setup', eventsTopicSetupInputSchema.shape, readOnlyLocalAnnotations, eventsTopicSetupHandler);
|
|
7149
7217
|
server.tool('ductape_events_validate_project', eventsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, eventsProjectValidationHandler);
|
|
7150
7218
|
server.tool('ductape_features_validate_project', featuresProjectValidationInputSchema.shape, readOnlyLocalAnnotations, featuresProjectValidationHandler);
|
|
7219
|
+
server.tool('ductape_frontend_analytics_validate_project', frontendAnalyticsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, frontendAnalyticsProjectValidationHandler);
|
|
7151
7220
|
server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupAnnotations, portableFunctionSetupHandler);
|
|
7152
7221
|
server.tool('ductape_redis_setup', redisSetupInputSchema.shape, redisSetupHandler);
|
|
7153
7222
|
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.4",
|
|
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
|
},
|