fraim 2.0.179 → 2.0.180

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.
Files changed (59) hide show
  1. package/dist/src/ai-hub/desktop-main.js +2 -2
  2. package/dist/src/api/admin/payments.js +33 -0
  3. package/dist/src/api/admin/sales-leads.js +21 -0
  4. package/dist/src/api/payment/create-session.js +338 -0
  5. package/dist/src/api/payment/dashboard-link.js +149 -0
  6. package/dist/src/api/payment/session-details.js +31 -0
  7. package/dist/src/api/payment/webhook.js +587 -0
  8. package/dist/src/api/personas/me.js +29 -0
  9. package/dist/src/api/pricing/get-config.js +25 -0
  10. package/dist/src/api/sales/contact.js +44 -0
  11. package/dist/src/cli/distribution/marketplace-bundles.js +5 -1
  12. package/dist/src/db/payment-repository.js +61 -0
  13. package/dist/src/fraim/config-loader.js +11 -0
  14. package/dist/src/fraim/db-service.js +2387 -0
  15. package/dist/src/fraim/issues.js +152 -0
  16. package/dist/src/fraim/template-processor.js +184 -0
  17. package/dist/src/fraim/utils/request-utils.js +23 -0
  18. package/dist/src/middleware/auth.js +266 -0
  19. package/dist/src/middleware/cors-config.js +111 -0
  20. package/dist/src/middleware/logger.js +116 -0
  21. package/dist/src/middleware/rate-limit.js +110 -0
  22. package/dist/src/middleware/reject-query-api-key.js +45 -0
  23. package/dist/src/middleware/security-headers.js +41 -0
  24. package/dist/src/middleware/telemetry.js +134 -0
  25. package/dist/src/models/payment.js +2 -0
  26. package/dist/src/routes/analytics.js +1447 -0
  27. package/dist/src/routes/app-routes.js +32 -0
  28. package/dist/src/routes/auth-routes.js +505 -0
  29. package/dist/src/routes/oauth-routes.js +325 -0
  30. package/dist/src/routes/payment-routes.js +186 -0
  31. package/dist/src/routes/persona-catalog-routes.js +84 -0
  32. package/dist/src/services/admin-service.js +229 -0
  33. package/dist/src/services/audit-log-persistence.js +60 -0
  34. package/dist/src/services/audit-log.js +69 -0
  35. package/dist/src/services/cookie-service.js +129 -0
  36. package/dist/src/services/dashboard-access.js +27 -0
  37. package/dist/src/services/demo-seed-service.js +139 -0
  38. package/dist/src/services/email-code.js +23 -0
  39. package/dist/src/services/email-service-clean.js +782 -0
  40. package/dist/src/services/email-service.js +951 -0
  41. package/dist/src/services/installer-service.js +131 -0
  42. package/dist/src/services/mcp-oauth-store.js +33 -0
  43. package/dist/src/services/mcp-service.js +823 -0
  44. package/dist/src/services/oauth-helpers.js +127 -0
  45. package/dist/src/services/org-service.js +89 -0
  46. package/dist/src/services/persona-entitlement-service.js +288 -0
  47. package/dist/src/services/provider-service.js +215 -0
  48. package/dist/src/services/registry-service.js +628 -0
  49. package/dist/src/services/session-service.js +86 -0
  50. package/dist/src/services/trial-reminder-service.js +120 -0
  51. package/dist/src/services/usage-analytics-service.js +419 -0
  52. package/dist/src/services/workspace-identity.js +21 -0
  53. package/dist/src/types/analytics.js +2 -0
  54. package/dist/src/utils/payment-calculator.js +52 -0
  55. package/extensions/office-word/favicon.ico +0 -0
  56. package/extensions/office-word/icon-64.png +0 -0
  57. package/extensions/office-word/manifest.xml +33 -0
  58. package/extensions/office-word/taskpane.html +242 -0
  59. package/package.json +12 -2
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildCorsOptions = buildCorsOptions;
4
+ exports.resolveSseAllowedOrigin = resolveSseAllowedOrigin;
5
+ const git_utils_1 = require("../core/utils/git-utils");
6
+ /**
7
+ * CORS allowlist configuration for the FRAIM server.
8
+ *
9
+ * Lives in its own module (not src/fraim-mcp-server.ts) so tests can import
10
+ * `buildCorsOptions` and `resolveSseAllowedOrigin` without running the
11
+ * top-level server bootstrap as a side effect.
12
+ *
13
+ * Allowlist sources (merged):
14
+ * - FRAIM_CORS_ALLOWED_ORIGINS (comma-separated absolute origins, e.g.
15
+ * "https://app.example.com,https://staging.example.com")
16
+ * - Built-in production origins for fraimworks.ai
17
+ * - Localhost dev origins when NODE_ENV !== 'production'
18
+ *
19
+ * Requests without an Origin header are allowed for curl/server-to-server
20
+ * callers. Browser POST/fetch requests can still include Origin on same-origin
21
+ * requests, so the active local FRAIM port must be allowlisted in development.
22
+ */
23
+ function buildAllowlist() {
24
+ const fromEnv = (process.env.FRAIM_CORS_ALLOWED_ORIGINS || '')
25
+ .split(',')
26
+ .map(o => o.trim())
27
+ .filter(Boolean);
28
+ const publicBaseUrl = (process.env.FRAIM_PUBLIC_BASE_URL || '').replace(/\/+$/, '').trim();
29
+ const allowed = new Set([
30
+ ...fromEnv,
31
+ 'https://fraimworks.ai',
32
+ 'https://www.fraimworks.ai',
33
+ 'https://fraim.wellnessatwork.me',
34
+ // Claude surfaces — browser-originated requests from claude.ai web app,
35
+ // Claude Design, and claude.ai settings pages (Connectors setup flow).
36
+ // Anthropic's backend MCP calls have no Origin header so don't need listing.
37
+ 'https://claude.ai',
38
+ 'https://www.claude.ai',
39
+ 'https://design.claude.ai',
40
+ 'https://api.anthropic.com',
41
+ ]);
42
+ if (publicBaseUrl && /^https?:\/\//i.test(publicBaseUrl)) {
43
+ allowed.add(publicBaseUrl);
44
+ }
45
+ if (process.env.NODE_ENV !== 'production') {
46
+ const localPorts = new Set([3000, 3002, 8080, 8765]);
47
+ const addPort = (value) => {
48
+ if (!value)
49
+ return;
50
+ const parsed = Number(value);
51
+ if (Number.isInteger(parsed) && parsed > 0) {
52
+ localPorts.add(parsed);
53
+ }
54
+ };
55
+ addPort(String((0, git_utils_1.getPort)()));
56
+ addPort(process.env.FRAIM_MCP_PORT);
57
+ addPort(process.env.PORT);
58
+ addPort(process.env.WEBSITES_PORT);
59
+ addPort(process.env.PLAYWRIGHT_FRAIM_MCP_PORT);
60
+ addPort(process.env.PLAYWRIGHT_WEBSITE_PORT);
61
+ const playwrightBaseUrl = (process.env.PLAYWRIGHT_BASE_URL || '').trim();
62
+ if (playwrightBaseUrl) {
63
+ try {
64
+ const parsed = new URL(playwrightBaseUrl);
65
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
66
+ allowed.add(parsed.origin);
67
+ }
68
+ }
69
+ catch {
70
+ // Ignore invalid PLAYWRIGHT_BASE_URL values and fall back to the static allowlist.
71
+ }
72
+ }
73
+ for (const localPort of localPorts) {
74
+ allowed.add(`http://localhost:${localPort}`);
75
+ allowed.add(`http://127.0.0.1:${localPort}`);
76
+ }
77
+ }
78
+ return allowed;
79
+ }
80
+ function buildCorsOptions() {
81
+ const allowed = buildAllowlist();
82
+ return {
83
+ origin: (origin, callback) => {
84
+ // Same-origin and non-browser tools have no Origin header.
85
+ // This covers Anthropic's cloud infrastructure making server-to-server
86
+ // MCP calls on behalf of claude.ai Connectors — no Origin header present.
87
+ if (!origin)
88
+ return callback(null, true);
89
+ if (allowed.has(origin))
90
+ return callback(null, true);
91
+ return callback(new Error(`CORS: origin ${origin} is not in the allowlist`));
92
+ },
93
+ credentials: false,
94
+ methods: ['GET', 'POST', 'OPTIONS'],
95
+ // Authorization: required by MCP HTTP transport for Bearer token auth
96
+ // mcp-session-id: required by MCP Streamable HTTP spec for session tracking
97
+ allowedHeaders: ['Content-Type', 'x-api-key', 'x-admin-key', 'x-fraim-auth-mode', 'x-fraim-local-version', 'x-fraim-session-id', 'Authorization', 'mcp-session-id'],
98
+ exposedHeaders: ['mcp-session-id'],
99
+ };
100
+ }
101
+ /**
102
+ * Resolve the SSE Access-Control-Allow-Origin value against the allowlist.
103
+ * Returns the request's Origin if it is allowed, otherwise undefined so the
104
+ * caller can omit the header entirely. Never returns the wildcard '*'.
105
+ */
106
+ function resolveSseAllowedOrigin(origin) {
107
+ if (!origin)
108
+ return undefined;
109
+ const allowed = buildAllowlist();
110
+ return allowed.has(origin) ? origin : undefined;
111
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.requestLogger = requestLogger;
37
+ const crypto_1 = require("crypto");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const LOG_FILE = path.join(process.cwd(), 'server-direct.log');
41
+ /**
42
+ * Extracts API key from request for logging purposes
43
+ */
44
+ function getApiKeyForLogging(req) {
45
+ const fromContext = req.apiKeyData?.key;
46
+ if (typeof fromContext === 'string' && fromContext.trim().length > 0) {
47
+ return fromContext;
48
+ }
49
+ const fromHeader = Array.isArray(req.headers['x-api-key'])
50
+ ? req.headers['x-api-key'][0]
51
+ : req.headers['x-api-key'];
52
+ if (typeof fromHeader === 'string' && fromHeader.trim().length > 0) {
53
+ return fromHeader;
54
+ }
55
+ const fromQuery = req.query?.['api-key'];
56
+ if (typeof fromQuery === 'string' && fromQuery.trim().length > 0) {
57
+ return fromQuery;
58
+ }
59
+ return 'ANONYMOUS';
60
+ }
61
+ /**
62
+ * Middleware to log all requests
63
+ */
64
+ function requestLogger(req, res, next) {
65
+ const start = Date.now();
66
+ const { method, path } = req;
67
+ const requestIdHeader = Array.isArray(req.headers['x-fraim-request-id'])
68
+ ? req.headers['x-fraim-request-id'][0]
69
+ : req.headers['x-fraim-request-id'];
70
+ const requestId = typeof requestIdHeader === 'string' && requestIdHeader.trim().length > 0
71
+ ? requestIdHeader
72
+ : (0, crypto_1.randomUUID)();
73
+ const localVersionHeader = Array.isArray(req.headers['x-fraim-local-version'])
74
+ ? req.headers['x-fraim-local-version'][0]
75
+ : req.headers['x-fraim-local-version'];
76
+ const localMcpVersion = typeof localVersionHeader === 'string' && localVersionHeader.trim().length > 0
77
+ ? localVersionHeader
78
+ : 'unknown';
79
+ req.requestId = requestId;
80
+ req.localMcpVersion = localMcpVersion;
81
+ res.setHeader('X-FRAIM-Request-ID', requestId);
82
+ let params = '';
83
+ if (method === 'POST') {
84
+ try {
85
+ params = JSON.stringify(req.body);
86
+ }
87
+ catch (e) {
88
+ params = '[Unserializable Body]';
89
+ }
90
+ }
91
+ const logEntry = (msg) => {
92
+ const timestamp = new Date().toISOString();
93
+ const entry = `[${timestamp}] ${msg}\n`;
94
+ try {
95
+ fs.appendFileSync(LOG_FILE, entry);
96
+ }
97
+ catch (e) {
98
+ console.error('Failed to write to log file:', e);
99
+ }
100
+ if (msg.includes('END') && msg.includes(' 400')) {
101
+ console.error(msg); // Still log errors to console
102
+ }
103
+ else {
104
+ console.info(msg);
105
+ }
106
+ };
107
+ logEntry(`[req:${requestId}] START ${method} ${path}${params ? ' ' + params : ''}`);
108
+ res.on('finish', () => {
109
+ const duration = Date.now() - start;
110
+ const status = res.statusCode;
111
+ const keyStr = getApiKeyForLogging(req);
112
+ const logMsg = `[req:${requestId}] [local:${localMcpVersion}] ${keyStr} END ${method} ${path} ${status} ${duration}ms`;
113
+ logEntry(logMsg);
114
+ });
115
+ next();
116
+ }
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sensitiveRateLimiter = exports.publicApiRateLimiter = void 0;
4
+ exports.createRateLimiter = createRateLimiter;
5
+ function createRateLimiter(options) {
6
+ const { windowMs, max, name } = options;
7
+ const buckets = new Map();
8
+ function cleanupExpired(now = Date.now()) {
9
+ let removed = 0;
10
+ for (const [key, bucket] of buckets) {
11
+ if (bucket.resetAt <= now) {
12
+ buckets.delete(key);
13
+ removed += 1;
14
+ }
15
+ }
16
+ return removed;
17
+ }
18
+ // Periodically sweep expired buckets so long-lived processes do not leak memory
19
+ // proportional to lifetime client-IP cardinality. unref() so the timer never holds
20
+ // the event loop open during shutdown.
21
+ let sweepTimer = null;
22
+ if (process.env.NODE_ENV !== 'test' || process.env.FRAIM_RATE_LIMIT_SWEEP === '1') {
23
+ sweepTimer = setInterval(() => {
24
+ const removed = cleanupExpired();
25
+ if (removed > 0) {
26
+ console.log(`[FRAIM RATE-LIMIT] ${name} swept ${removed} expired buckets (remaining=${buckets.size})`);
27
+ }
28
+ }, windowMs);
29
+ sweepTimer.unref();
30
+ }
31
+ const middleware = function rateLimitMiddleware(req, res, next) {
32
+ // Rate limits are skipped in test mode so the suite can run hundreds of requests
33
+ // against the same endpoint quickly. Production and dev still enforce them.
34
+ if (process.env.NODE_ENV === 'test' && process.env.FRAIM_TEST_RATE_LIMIT !== '1') {
35
+ return next();
36
+ }
37
+ const key = identifyClient(req);
38
+ const now = Date.now();
39
+ const bucket = buckets.get(key);
40
+ if (!bucket || bucket.resetAt <= now) {
41
+ buckets.set(key, { count: 1, resetAt: now + windowMs });
42
+ res.setHeader('X-RateLimit-Limit', String(max));
43
+ res.setHeader('X-RateLimit-Remaining', String(max - 1));
44
+ res.setHeader('X-RateLimit-Reset', String(Math.ceil((now + windowMs) / 1000)));
45
+ return next();
46
+ }
47
+ if (bucket.count >= max) {
48
+ const retryAfterSec = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));
49
+ res.setHeader('Retry-After', String(retryAfterSec));
50
+ res.setHeader('X-RateLimit-Limit', String(max));
51
+ res.setHeader('X-RateLimit-Remaining', '0');
52
+ res.setHeader('X-RateLimit-Reset', String(Math.ceil(bucket.resetAt / 1000)));
53
+ console.warn(`[FRAIM RATE-LIMIT] ${name} blocked ${key} on ${req.method} ${req.path}`);
54
+ return res.status(429).json({
55
+ error: 'Too many requests',
56
+ message: `Rate limit exceeded. Try again in ${retryAfterSec} seconds.`
57
+ });
58
+ }
59
+ bucket.count += 1;
60
+ res.setHeader('X-RateLimit-Limit', String(max));
61
+ res.setHeader('X-RateLimit-Remaining', String(max - bucket.count));
62
+ res.setHeader('X-RateLimit-Reset', String(Math.ceil(bucket.resetAt / 1000)));
63
+ return next();
64
+ };
65
+ middleware.reset = () => buckets.clear();
66
+ middleware.cleanupExpired = cleanupExpired;
67
+ middleware.stopCleanup = () => {
68
+ if (sweepTimer) {
69
+ clearInterval(sweepTimer);
70
+ sweepTimer = null;
71
+ }
72
+ };
73
+ Object.defineProperty(middleware, 'bucketCount', {
74
+ get: () => buckets.size
75
+ });
76
+ return middleware;
77
+ }
78
+ /**
79
+ * Identify the client for rate-limit bucketing.
80
+ * Order: explicit Express-resolved req.ip → first X-Forwarded-For hop → socket address → 'unknown'.
81
+ */
82
+ function identifyClient(req) {
83
+ if (req.ip)
84
+ return req.ip;
85
+ const xff = req.headers['x-forwarded-for'];
86
+ if (typeof xff === 'string' && xff.length > 0) {
87
+ const first = xff.split(',')[0].trim();
88
+ if (first)
89
+ return first;
90
+ }
91
+ else if (Array.isArray(xff) && xff.length > 0) {
92
+ const first = xff[0].split(',')[0].trim();
93
+ if (first)
94
+ return first;
95
+ }
96
+ return req.socket?.remoteAddress || 'unknown';
97
+ }
98
+ /**
99
+ * Standard public-API limiter: 60 requests per minute per IP.
100
+ * Suitable for read-only public endpoints (pricing config, session details).
101
+ */
102
+ const publicApiRateLimiter = () => createRateLimiter({ windowMs: 60_000, max: 60, name: 'public-api' });
103
+ exports.publicApiRateLimiter = publicApiRateLimiter;
104
+ /**
105
+ * Sensitive-action limiter: 10 requests per minute per IP.
106
+ * Suitable for signup, contact, bypass, key-retrieval, and other endpoints
107
+ * whose abuse has direct cost or fraud impact.
108
+ */
109
+ const sensitiveRateLimiter = () => createRateLimiter({ windowMs: 60_000, max: 10, name: 'sensitive' });
110
+ exports.sensitiveRateLimiter = sensitiveRateLimiter;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rejectQueryStringApiKey = rejectQueryStringApiKey;
4
+ /**
5
+ * Reject any API/MCP/admin request that carries an api-key in the query string.
6
+ *
7
+ * Why this exists separately from the AuthMiddleware's same check:
8
+ * The AuthMiddleware short-circuits on public prefixes (e.g. /api/pricing/config),
9
+ * which means a request like `/api/pricing/config?api-key=secret` previously slipped
10
+ * the credential into URLs and access logs even though the route did not actually
11
+ * authenticate it. This middleware runs BEFORE routing, so it catches that case.
12
+ *
13
+ * Why it is scoped to API paths and not applied globally:
14
+ * The analytics dashboard navigates between its public HTML pages with
15
+ * `?api-key=` in the URL so the front-end can store the key in sessionStorage
16
+ * and then send it via header on subsequent XHRs. That page-navigation pattern
17
+ * is intentional and preserved. The leak we close here is the API-call pattern
18
+ * where the credential ends up in proxy/access logs.
19
+ *
20
+ * Paths covered: /api/*, /mcp, /mcp/*, /admin, /admin/*.
21
+ */
22
+ function rejectQueryStringApiKey() {
23
+ return function (req, res, next) {
24
+ const path = req.path || '';
25
+ const isApiPath = path.startsWith('/api/')
26
+ || path === '/mcp'
27
+ || path.startsWith('/mcp/')
28
+ || path === '/admin'
29
+ || path.startsWith('/admin/');
30
+ if (!isApiPath)
31
+ return next();
32
+ if (req.query && typeof req.query['api-key'] !== 'undefined') {
33
+ console.error(`[FRAIM AUTH] Rejected query-string api-key (pre-routing) for ${req.method} ${path}`);
34
+ return res.status(401).json({
35
+ jsonrpc: '2.0',
36
+ error: {
37
+ code: -32001,
38
+ message: 'Unauthorized: API keys must be sent in the x-api-key header, not in the query string [AUTH-QUERY-FORBIDDEN]'
39
+ },
40
+ id: req.body?.id || null
41
+ });
42
+ }
43
+ return next();
44
+ };
45
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.securityHeaders = securityHeaders;
4
+ const DEFAULT_CSP = [
5
+ "default-src 'self'",
6
+ // Inline scripts are used by the analytics/installer dashboard; keep them.
7
+ // Third-party CDNs are explicitly listed rather than wildcarded.
8
+ "script-src 'self' 'unsafe-inline' https://js.stripe.com https://cdn.jsdelivr.net",
9
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
10
+ "img-src 'self' data: https:",
11
+ "font-src 'self' https://fonts.gstatic.com data:",
12
+ "connect-src 'self' https://api.stripe.com",
13
+ "frame-src 'self' https://js.stripe.com https://hooks.stripe.com",
14
+ "frame-ancestors 'self'",
15
+ "base-uri 'self'",
16
+ "form-action 'self' https://checkout.stripe.com"
17
+ ].join('; ');
18
+ function securityHeaders(options = {}) {
19
+ const enableHsts = options.enableHsts !== false;
20
+ const csp = options.contentSecurityPolicy === undefined
21
+ ? DEFAULT_CSP
22
+ : options.contentSecurityPolicy;
23
+ return function securityHeadersMiddleware(req, res, next) {
24
+ // Trust-proxy aware: req.secure is true if X-Forwarded-Proto is https
25
+ // and Express has been told to trust the upstream (see fraim-mcp-server.ts).
26
+ if (enableHsts && req.secure) {
27
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
28
+ }
29
+ res.setHeader('X-Content-Type-Options', 'nosniff');
30
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
31
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
32
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
33
+ res.setHeader('Cross-Origin-Resource-Policy', 'same-site');
34
+ res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
35
+ res.setHeader('X-DNS-Prefetch-Control', 'off');
36
+ if (csp) {
37
+ res.setHeader('Content-Security-Policy', csp);
38
+ }
39
+ next();
40
+ };
41
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TelemetryMiddleware = void 0;
4
+ class TelemetryMiddleware {
5
+ constructor(sessionManager) {
6
+ this.sessionManager = sessionManager;
7
+ }
8
+ parseSessionId(req) {
9
+ const fromHeader = Array.isArray(req.headers['x-fraim-session-id'])
10
+ ? req.headers['x-fraim-session-id'][0]
11
+ : req.headers['x-fraim-session-id'];
12
+ if (typeof fromHeader === 'string' && fromHeader.trim().length > 0) {
13
+ return fromHeader.trim();
14
+ }
15
+ const fromParams = req.body?.params?.sessionId;
16
+ if (typeof fromParams === 'string' && fromParams.trim().length > 0) {
17
+ return fromParams.trim();
18
+ }
19
+ const fromArgs = req.body?.params?.arguments?.sessionId;
20
+ if (typeof fromArgs === 'string' && fromArgs.trim().length > 0) {
21
+ return fromArgs.trim();
22
+ }
23
+ return undefined;
24
+ }
25
+ /**
26
+ * Middleware to enforce Session Handshake and track activity
27
+ */
28
+ async handle(req, res, next) {
29
+ try {
30
+ // 1. Standard MCP Lifecycle & Discovery methods (Initialize, List Tools/Resources/Prompts, etc.)
31
+ const exemptMethods = [
32
+ 'initialize',
33
+ 'notifications/initialized',
34
+ 'tools/list',
35
+ 'resources/list',
36
+ 'prompts/list',
37
+ 'prompts/get',
38
+ 'logging/setLevel'
39
+ ];
40
+ if (req.body && exemptMethods.includes(req.body.method)) {
41
+ return next();
42
+ }
43
+ // 2. FRAIM Bootstrap/Discovery Tools that don't need an active session
44
+ if (req.body && req.body.method === 'tools/call') {
45
+ const toolName = req.body.params?.name;
46
+ const safeTools = [
47
+ 'fraim_connect',
48
+ 'list_fraim_jobs'
49
+ ];
50
+ if (toolName && safeTools.includes(toolName)) {
51
+ return next();
52
+ }
53
+ }
54
+ // Skip for non-authenticated or exempt paths (GET only).
55
+ // Issue #359 - /api/auth/* and /api/account/* are human-session lifecycle
56
+ // endpoints (called by auth-bootstrap.js on every page load); they happen
57
+ // BEFORE any agent has fraim_connect'd, so they MUST be exempt from the
58
+ // telemetry session-active gate. Without this, /api/auth/session returns 400
59
+ // for every signed-in browser, which redirects users back to sign-in.
60
+ // Issue #563 - /api/org/* serves the org-cache sync performed by the
61
+ // `fraim sync` CLI, which runs outside any MCP session (same lifecycle
62
+ // position as /api/registry/sync). Auth still applies; only the
63
+ // session-active gate is exempted.
64
+ const exemptPaths = ['/health', '/admin', '/api/registry/sync', '/api/registry/github-workflows', '/api/registry/topology/personalized', '/api/registry/topology/usage-heatmap', '/api/org/', '/api/manager/', '/api/payment/', '/api/sales/', '/api/signup', '/api/providers', '/api/analytics', '/analytics', '/api/auth/', '/api/account/'];
65
+ if (exemptPaths.some(p => req.path.startsWith(p))) {
66
+ return next();
67
+ }
68
+ const apiKey = req.apiKeyData?.key;
69
+ if (!apiKey)
70
+ return next(); // Auth middleware handles 401 earlier
71
+ const sessionId = this.parseSessionId(req);
72
+ const localVersionFromHeader = Array.isArray(req.headers['x-fraim-local-version'])
73
+ ? req.headers['x-fraim-local-version'][0]
74
+ : req.headers['x-fraim-local-version'];
75
+ const localVersion = req.localMcpVersion ||
76
+ (typeof localVersionFromHeader === 'string' ? localVersionFromHeader : 'unknown');
77
+ const isProxiedAgentCall = typeof localVersion === 'string' && localVersion !== 'unknown';
78
+ // Enforce explicit session id for local proxy agent traffic.
79
+ if (isProxiedAgentCall && !sessionId) {
80
+ return res.status(400).json({
81
+ jsonrpc: '2.0',
82
+ error: {
83
+ code: -32600,
84
+ message: `Missing Session ID. REQUIRED ACTION:
85
+ 1. Call 'fraim_connect' first
86
+ 2. Capture the returned sessionId
87
+ 3. Include that sessionId on every subsequent FRAIM tool call (params.sessionId or params.arguments.sessionId).`
88
+ },
89
+ id: req.body?.id || null
90
+ });
91
+ }
92
+ // Check Session
93
+ const isActive = await this.sessionManager.updateActivity(apiKey, sessionId);
94
+ if (!isActive) {
95
+ const sessionStartGuidance = isProxiedAgentCall
96
+ ? `Session Not Started. REQUIRED ACTION: Call 'fraim_connect' tool first, then include the returned sessionId on every subsequent FRAIM tool call.`
97
+ : `Session Not Started. REQUIRED ACTION: Call 'fraim_connect' tool first with these parameters:
98
+
99
+ {
100
+ "agent": {
101
+ "name": "YourAgentName",
102
+ "model": "your-model-name"
103
+ },
104
+ "machine": {
105
+ "hostname": "use os.hostname()",
106
+ "platform": "use process.platform"
107
+ },
108
+ "repo": {
109
+ "url": "git remote URL",
110
+ "owner": "repo owner",
111
+ "name": "repo name"
112
+ }
113
+ }
114
+
115
+ After successful fraim_connect, all other FRAIM tools will work. This is required for telemetry and session management.`;
116
+ return res.status(400).json({
117
+ jsonrpc: '2.0',
118
+ error: {
119
+ code: -32600,
120
+ message: sessionStartGuidance
121
+ },
122
+ id: req.body?.id || null
123
+ });
124
+ }
125
+ console.log(`Telemetry active for ${apiKey}`);
126
+ return next();
127
+ }
128
+ catch (error) {
129
+ console.error('Telemetry middleware error:', error);
130
+ return next(error);
131
+ }
132
+ }
133
+ }
134
+ exports.TelemetryMiddleware = TelemetryMiddleware;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });