fraim-framework 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.
- package/dist/src/ai-hub/desktop-main.js +2 -2
- package/dist/src/api/admin/payments.js +33 -0
- package/dist/src/api/admin/sales-leads.js +21 -0
- package/dist/src/api/payment/create-session.js +338 -0
- package/dist/src/api/payment/dashboard-link.js +149 -0
- package/dist/src/api/payment/session-details.js +31 -0
- package/dist/src/api/payment/webhook.js +587 -0
- package/dist/src/api/personas/me.js +29 -0
- package/dist/src/api/pricing/get-config.js +25 -0
- package/dist/src/api/sales/contact.js +44 -0
- package/dist/src/cli/distribution/marketplace-bundles.js +5 -1
- package/dist/src/db/payment-repository.js +61 -0
- package/dist/src/fraim/config-loader.js +11 -0
- package/dist/src/fraim/db-service.js +2387 -0
- package/dist/src/fraim/issues.js +152 -0
- package/dist/src/fraim/template-processor.js +184 -0
- package/dist/src/fraim/utils/request-utils.js +23 -0
- package/dist/src/middleware/auth.js +266 -0
- package/dist/src/middleware/cors-config.js +111 -0
- package/dist/src/middleware/logger.js +116 -0
- package/dist/src/middleware/rate-limit.js +110 -0
- package/dist/src/middleware/reject-query-api-key.js +45 -0
- package/dist/src/middleware/security-headers.js +41 -0
- package/dist/src/middleware/telemetry.js +134 -0
- package/dist/src/models/payment.js +2 -0
- package/dist/src/routes/analytics.js +1447 -0
- package/dist/src/routes/app-routes.js +32 -0
- package/dist/src/routes/auth-routes.js +505 -0
- package/dist/src/routes/oauth-routes.js +325 -0
- package/dist/src/routes/payment-routes.js +186 -0
- package/dist/src/routes/persona-catalog-routes.js +84 -0
- package/dist/src/services/admin-service.js +229 -0
- package/dist/src/services/audit-log-persistence.js +60 -0
- package/dist/src/services/audit-log.js +69 -0
- package/dist/src/services/cookie-service.js +129 -0
- package/dist/src/services/dashboard-access.js +27 -0
- package/dist/src/services/demo-seed-service.js +139 -0
- package/dist/src/services/email-code.js +23 -0
- package/dist/src/services/email-service-clean.js +782 -0
- package/dist/src/services/email-service.js +951 -0
- package/dist/src/services/installer-service.js +131 -0
- package/dist/src/services/mcp-oauth-store.js +33 -0
- package/dist/src/services/mcp-service.js +823 -0
- package/dist/src/services/oauth-helpers.js +127 -0
- package/dist/src/services/org-service.js +89 -0
- package/dist/src/services/persona-entitlement-service.js +288 -0
- package/dist/src/services/provider-service.js +215 -0
- package/dist/src/services/registry-service.js +628 -0
- package/dist/src/services/session-service.js +86 -0
- package/dist/src/services/trial-reminder-service.js +120 -0
- package/dist/src/services/usage-analytics-service.js +419 -0
- package/dist/src/services/workspace-identity.js +21 -0
- package/dist/src/types/analytics.js +2 -0
- package/dist/src/utils/payment-calculator.js +52 -0
- package/extensions/office-word/favicon.ico +0 -0
- package/extensions/office-word/icon-64.png +0 -0
- package/extensions/office-word/manifest.xml +33 -0
- package/extensions/office-word/taskpane.html +242 -0
- package/package.json +12 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fileFraimIssue = fileFraimIssue;
|
|
7
|
+
exports.listFraimIssuesByReporter = listFraimIssuesByReporter;
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const FRAIM_REPO_OWNER = 'mathursrus';
|
|
10
|
+
const FRAIM_REPO_NAME = 'FRAIM';
|
|
11
|
+
const DEFAULT_LIST_LIMIT = 20;
|
|
12
|
+
const MAX_LIST_LIMIT = 100;
|
|
13
|
+
function normalizeLimit(limit) {
|
|
14
|
+
if (!Number.isFinite(limit))
|
|
15
|
+
return DEFAULT_LIST_LIMIT;
|
|
16
|
+
const parsed = Math.trunc(limit);
|
|
17
|
+
if (parsed <= 0)
|
|
18
|
+
return DEFAULT_LIST_LIMIT;
|
|
19
|
+
return Math.min(parsed, MAX_LIST_LIMIT);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* File an issue in the FRAIM repository
|
|
23
|
+
*
|
|
24
|
+
* This function creates issues in the FRAIM repository for bug reports,
|
|
25
|
+
* feature requests, and workflow contributions.
|
|
26
|
+
*/
|
|
27
|
+
async function fileFraimIssue(params) {
|
|
28
|
+
const { title, labels, dryRun, reporterEmail } = params;
|
|
29
|
+
// Prepend reporter line so the close-the-loop job can identify who filed the issue
|
|
30
|
+
const reporterLine = reporterEmail ? `**Reported by:** ${reporterEmail}\n\n` : '';
|
|
31
|
+
const body = reporterLine + params.body;
|
|
32
|
+
// Always target the FRAIM repository
|
|
33
|
+
const owner = FRAIM_REPO_OWNER;
|
|
34
|
+
const repo = FRAIM_REPO_NAME;
|
|
35
|
+
if (dryRun) {
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
dryRun: true,
|
|
39
|
+
message: `[DRY RUN] Would create GitHub issue: "${title}" in ${owner}/${repo}.`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
// Check for GitHub token
|
|
44
|
+
const token = process.env.GITHUB_TOKEN;
|
|
45
|
+
if (!token) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
message: `GitHub integration requires GITHUB_TOKEN environment variable.
|
|
49
|
+
|
|
50
|
+
Please set GITHUB_TOKEN environment variable to file issues in ${owner}/${repo}.`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Create the issue in FRAIM repository
|
|
54
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/issues`;
|
|
55
|
+
const payload = { title, body };
|
|
56
|
+
if (labels && labels.length > 0) {
|
|
57
|
+
payload.labels = labels;
|
|
58
|
+
}
|
|
59
|
+
const response = await axios_1.default.post(url, payload, {
|
|
60
|
+
headers: {
|
|
61
|
+
'Authorization': `Bearer ${token}`,
|
|
62
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
issueNumber: response.data.number,
|
|
69
|
+
htmlUrl: response.data.html_url
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
let errorMessage = 'Unknown error';
|
|
74
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
75
|
+
errorMessage = `Status: ${error.response?.status} - ${JSON.stringify(error.response?.data)}`;
|
|
76
|
+
}
|
|
77
|
+
else if (error instanceof Error) {
|
|
78
|
+
errorMessage = error.message;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
message: `Failed to create issue in FRAIM repository: ${errorMessage}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* List issues filed into the FRAIM repository by a specific reporter email.
|
|
88
|
+
*
|
|
89
|
+
* Uses GitHub search against the existing "Reported by:" marker in issue bodies
|
|
90
|
+
* so we do not need a second FRAIM-owned index.
|
|
91
|
+
*/
|
|
92
|
+
async function listFraimIssuesByReporter(params) {
|
|
93
|
+
const { reporterEmail } = params;
|
|
94
|
+
const limit = normalizeLimit(params.limit);
|
|
95
|
+
if (!reporterEmail) {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
message: 'Reporter email is required to list FRAIM GitHub issues.'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const token = process.env.GITHUB_TOKEN;
|
|
102
|
+
if (!token) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
message: `GitHub integration requires GITHUB_TOKEN environment variable.
|
|
106
|
+
|
|
107
|
+
Please set GITHUB_TOKEN environment variable to query issues in ${FRAIM_REPO_OWNER}/${FRAIM_REPO_NAME}.`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const query = `repo:${FRAIM_REPO_OWNER}/${FRAIM_REPO_NAME} is:issue "Reported by: ${reporterEmail}" in:body`;
|
|
111
|
+
try {
|
|
112
|
+
const response = await axios_1.default.get('https://api.github.com/search/issues', {
|
|
113
|
+
headers: {
|
|
114
|
+
'Authorization': `Bearer ${token}`,
|
|
115
|
+
'Accept': 'application/vnd.github+json',
|
|
116
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
117
|
+
},
|
|
118
|
+
params: {
|
|
119
|
+
q: query,
|
|
120
|
+
sort: 'created',
|
|
121
|
+
order: 'desc',
|
|
122
|
+
per_page: limit,
|
|
123
|
+
page: 1
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const issues = Array.isArray(response.data?.items)
|
|
127
|
+
? response.data.items.map((item) => ({
|
|
128
|
+
issueNumber: item.number,
|
|
129
|
+
title: item.title,
|
|
130
|
+
status: item.state === 'closed' ? 'closed' : 'open',
|
|
131
|
+
createdAt: item.created_at
|
|
132
|
+
}))
|
|
133
|
+
: [];
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
issues
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
let errorMessage = 'Unknown error';
|
|
141
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
142
|
+
errorMessage = `Status: ${error.response?.status} - ${JSON.stringify(error.response?.data)}`;
|
|
143
|
+
}
|
|
144
|
+
else if (error instanceof Error) {
|
|
145
|
+
errorMessage = error.message;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
message: `Failed to query issues in the FRAIM repository: ${errorMessage}`
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TemplateEngine = void 0;
|
|
4
|
+
exports.getTemplateEngine = getTemplateEngine;
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const provider_utils_1 = require("../core/utils/provider-utils");
|
|
8
|
+
const object_utils_1 = require("../core/utils/object-utils");
|
|
9
|
+
/**
|
|
10
|
+
* Multi-Provider Template Engine
|
|
11
|
+
* Processes workflow templates with provider-specific action mappings
|
|
12
|
+
*/
|
|
13
|
+
class TemplateEngine {
|
|
14
|
+
constructor(registryPath) {
|
|
15
|
+
this.providers = new Map();
|
|
16
|
+
this.registryPath = registryPath || (0, path_1.join)(__dirname, '../../registry');
|
|
17
|
+
this.loadProviders();
|
|
18
|
+
}
|
|
19
|
+
loadProviders() {
|
|
20
|
+
try {
|
|
21
|
+
// Load GitHub provider templates
|
|
22
|
+
const githubPath = (0, path_1.join)(this.registryPath, 'providers/github.json');
|
|
23
|
+
if ((0, fs_1.existsSync)(githubPath)) {
|
|
24
|
+
const githubTemplates = JSON.parse((0, fs_1.readFileSync)(githubPath, 'utf-8'));
|
|
25
|
+
this.providers.set('github', githubTemplates);
|
|
26
|
+
}
|
|
27
|
+
// Load ADO provider templates
|
|
28
|
+
const adoPath = (0, path_1.join)(this.registryPath, 'providers/ado.json');
|
|
29
|
+
if ((0, fs_1.existsSync)(adoPath)) {
|
|
30
|
+
const adoTemplates = JSON.parse((0, fs_1.readFileSync)(adoPath, 'utf-8'));
|
|
31
|
+
this.providers.set('ado', adoTemplates);
|
|
32
|
+
}
|
|
33
|
+
// Load Jira provider templates
|
|
34
|
+
const jiraPath = (0, path_1.join)(this.registryPath, 'providers/jira.json');
|
|
35
|
+
if ((0, fs_1.existsSync)(jiraPath)) {
|
|
36
|
+
const jiraTemplates = JSON.parse((0, fs_1.readFileSync)(jiraPath, 'utf-8'));
|
|
37
|
+
this.providers.set('jira', jiraTemplates);
|
|
38
|
+
}
|
|
39
|
+
// Load GitLab provider templates
|
|
40
|
+
const gitlabPath = (0, path_1.join)(this.registryPath, 'providers/gitlab.json');
|
|
41
|
+
if ((0, fs_1.existsSync)(gitlabPath)) {
|
|
42
|
+
const gitlabTemplates = JSON.parse((0, fs_1.readFileSync)(gitlabPath, 'utf-8'));
|
|
43
|
+
this.providers.set('gitlab', gitlabTemplates);
|
|
44
|
+
}
|
|
45
|
+
console.log(`✅ Loaded ${this.providers.size} provider templates`);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.warn('⚠️ Failed to load provider templates:', error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Process workflow content with provider actions only
|
|
53
|
+
*/
|
|
54
|
+
processWorkflow(workflowContent, repositoryInfo) {
|
|
55
|
+
// Only process provider actions, no client-side config variables
|
|
56
|
+
return this.processProviderActions(workflowContent, repositoryInfo);
|
|
57
|
+
}
|
|
58
|
+
processProviderActions(workflowContent, repositoryInfo) {
|
|
59
|
+
// Determine code provider from repository info using shared utility
|
|
60
|
+
const repoData = repositoryInfo?.repository || repositoryInfo;
|
|
61
|
+
const codeProvider = repoData?.provider || (0, provider_utils_1.detectProvider)(repoData?.url);
|
|
62
|
+
// Determine issue provider from explicit issueTracking context only
|
|
63
|
+
const issueProvider = repositoryInfo?.issueTracking?.provider;
|
|
64
|
+
// Get templates for both providers
|
|
65
|
+
const codeTemplates = this.providers.get(codeProvider);
|
|
66
|
+
const issueTemplates = issueProvider ? this.providers.get(issueProvider) : undefined;
|
|
67
|
+
if (!codeTemplates && !issueTemplates) {
|
|
68
|
+
console.warn(`⚠️ No templates found for providers: code=${codeProvider}, issue=${issueProvider}`);
|
|
69
|
+
return workflowContent;
|
|
70
|
+
}
|
|
71
|
+
// Process with both providers
|
|
72
|
+
return this.processWithSplitProviders(workflowContent, codeProvider, issueProvider, repositoryInfo);
|
|
73
|
+
}
|
|
74
|
+
processWithSplitProviders(workflowContent, codeProvider, issueProvider, repositoryInfo) {
|
|
75
|
+
const codeTemplates = this.providers.get(codeProvider);
|
|
76
|
+
const issueTemplates = issueProvider ? this.providers.get(issueProvider) : undefined;
|
|
77
|
+
let processed = workflowContent;
|
|
78
|
+
// Define which actions are code-related vs issue-related
|
|
79
|
+
const issueActions = new Set([
|
|
80
|
+
'get_issue', 'update_issue_status', 'add_issue_comment',
|
|
81
|
+
'create_issue', 'assign_issue', 'search_issues',
|
|
82
|
+
'close_issue', 'list_issues'
|
|
83
|
+
]);
|
|
84
|
+
const codeActions = new Set([
|
|
85
|
+
'create_pr', 'get_pr', 'get_pr_comments', 'get_pr_review_comments',
|
|
86
|
+
'get_pr_reviews', 'add_pr_comment', 'merge_pr', 'create_branch',
|
|
87
|
+
'switch_branch', 'commit_changes', 'push_branch'
|
|
88
|
+
]);
|
|
89
|
+
// Replace issue actions with issue provider templates
|
|
90
|
+
if (issueTemplates) {
|
|
91
|
+
for (const [action, template] of Object.entries(issueTemplates)) {
|
|
92
|
+
if (issueActions.has(action)) {
|
|
93
|
+
const regex = new RegExp(`{{${action}}}`, 'g');
|
|
94
|
+
const renderedTemplate = this.renderTemplate(template, repositoryInfo);
|
|
95
|
+
processed = processed.replace(regex, renderedTemplate);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Replace code actions with code provider templates
|
|
100
|
+
if (codeTemplates) {
|
|
101
|
+
for (const [action, template] of Object.entries(codeTemplates)) {
|
|
102
|
+
if (codeActions.has(action)) {
|
|
103
|
+
const regex = new RegExp(`{{${action}}}`, 'g');
|
|
104
|
+
const renderedTemplate = this.renderTemplate(template, repositoryInfo);
|
|
105
|
+
processed = processed.replace(regex, renderedTemplate);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return processed;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Render template with repository variable substitution
|
|
113
|
+
*/
|
|
114
|
+
renderTemplate(template, repositoryInfo) {
|
|
115
|
+
if (!repositoryInfo)
|
|
116
|
+
return template;
|
|
117
|
+
return template.replace(/{{([^}]+)}}/g, (match, path) => {
|
|
118
|
+
const trimmedPath = path.trim();
|
|
119
|
+
// Handle proxy.repository.* variables
|
|
120
|
+
if (trimmedPath.startsWith('proxy.repository.')) {
|
|
121
|
+
const repoPath = trimmedPath.substring('proxy.repository.'.length);
|
|
122
|
+
let repoData = repositoryInfo.repository || repositoryInfo;
|
|
123
|
+
const value = (0, object_utils_1.getNestedValue)(repoData, repoPath);
|
|
124
|
+
return value !== undefined ? String(value) : match;
|
|
125
|
+
}
|
|
126
|
+
// Handle proxy.issueTracking.* variables (for split provider mode)
|
|
127
|
+
if (trimmedPath.startsWith('proxy.issueTracking.')) {
|
|
128
|
+
const issueTrackingPath = trimmedPath.substring('proxy.issueTracking.'.length);
|
|
129
|
+
const issueTrackingData = repositoryInfo.issueTracking;
|
|
130
|
+
if (issueTrackingData) {
|
|
131
|
+
const value = (0, object_utils_1.getNestedValue)(issueTrackingData, issueTrackingPath);
|
|
132
|
+
return value !== undefined ? String(value) : match;
|
|
133
|
+
}
|
|
134
|
+
return match;
|
|
135
|
+
}
|
|
136
|
+
return match;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get available providers
|
|
141
|
+
*/
|
|
142
|
+
getAvailableProviders() {
|
|
143
|
+
return Array.from(this.providers.keys());
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get available actions for a provider
|
|
147
|
+
*/
|
|
148
|
+
getProviderActions(provider) {
|
|
149
|
+
const templates = this.providers.get(provider);
|
|
150
|
+
return templates ? Object.keys(templates) : [];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Validate that all template actions in content are supported
|
|
154
|
+
*/
|
|
155
|
+
validateWorkflow(workflowContent, provider) {
|
|
156
|
+
const actionRegex = /{{(\w+)}}/g;
|
|
157
|
+
const actions = new Set();
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = actionRegex.exec(workflowContent)) !== null) {
|
|
160
|
+
// Skip config variables
|
|
161
|
+
if (!match[1].startsWith('config.')) {
|
|
162
|
+
actions.add(match[1]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const providerActions = this.getProviderActions(provider);
|
|
166
|
+
const missingActions = Array.from(actions).filter(action => !providerActions.includes(action));
|
|
167
|
+
return {
|
|
168
|
+
valid: missingActions.length === 0,
|
|
169
|
+
missingActions
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exports.TemplateEngine = TemplateEngine;
|
|
174
|
+
// Global template engine instance
|
|
175
|
+
let globalTemplateEngine = null;
|
|
176
|
+
/**
|
|
177
|
+
* Get or create global template engine instance
|
|
178
|
+
*/
|
|
179
|
+
function getTemplateEngine(registryPath) {
|
|
180
|
+
if (!globalTemplateEngine || (registryPath && registryPath !== globalTemplateEngine['registryPath'])) {
|
|
181
|
+
globalTemplateEngine = new TemplateEngine(registryPath);
|
|
182
|
+
}
|
|
183
|
+
return globalTemplateEngine;
|
|
184
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMAIL_REGEX = void 0;
|
|
4
|
+
exports.validateEmail = validateEmail;
|
|
5
|
+
exports.getRequestMeta = getRequestMeta;
|
|
6
|
+
/**
|
|
7
|
+
* Standard email validation regex
|
|
8
|
+
*/
|
|
9
|
+
exports.EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
10
|
+
/**
|
|
11
|
+
* Validate an email address
|
|
12
|
+
*/
|
|
13
|
+
function validateEmail(email) {
|
|
14
|
+
return exports.EMAIL_REGEX.test(email);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extract IP and UserAgent from an Express Request
|
|
18
|
+
*/
|
|
19
|
+
function getRequestMeta(req) {
|
|
20
|
+
const ip = req.headers['x-forwarded-for'] || req.ip || 'unknown';
|
|
21
|
+
const userAgent = req.headers['user-agent'] || 'unknown';
|
|
22
|
+
return { ip, userAgent };
|
|
23
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthMiddleware = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const cookie_service_1 = require("../services/cookie-service");
|
|
7
|
+
const audit_log_1 = require("../services/audit-log");
|
|
8
|
+
function resolveFraimProFilePath(fileName) {
|
|
9
|
+
const candidates = [
|
|
10
|
+
(0, path_1.join)(process.cwd(), 'fraim-pro', fileName),
|
|
11
|
+
(0, path_1.resolve)(__dirname, '..', '..', 'fraim-pro', fileName),
|
|
12
|
+
(0, path_1.resolve)(__dirname, '..', '..', '..', 'fraim-pro', fileName),
|
|
13
|
+
];
|
|
14
|
+
for (const candidate of candidates) {
|
|
15
|
+
if ((0, fs_1.existsSync)(candidate))
|
|
16
|
+
return candidate;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
class AuthMiddleware {
|
|
21
|
+
constructor(dbService, skipTestBypass = false) {
|
|
22
|
+
this.dbService = dbService;
|
|
23
|
+
this.skipTestBypass = skipTestBypass;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Middleware to authenticate requests via API key
|
|
27
|
+
*/
|
|
28
|
+
async handle(req, res, next) {
|
|
29
|
+
// Note: Payment routes are registered BEFORE this middleware is applied
|
|
30
|
+
// Skip auth for public health check, admin routes, website signup, sales inquiries, and self-serve access flow
|
|
31
|
+
const rawPath = req.path ?? req.originalUrl ?? req.url ?? '';
|
|
32
|
+
const p = (typeof rawPath === 'string' ? rawPath : '').split('?')[0].replace(/\/$/, '') || '';
|
|
33
|
+
const publicPrefixes = ['/admin', '/dashboard', '/health', '/pricing', '/fraim-brain', '/api/signup', '/api/sales', '/api/request-access', '/api/installer-key', '/api/installer-download', '/api/installer-availability', '/api/payment/bypass', '/api/pricing', '/api/personas/catalog', '/auth', '/portfolio'];
|
|
34
|
+
// Analytics dashboard is public, but API routes are protected
|
|
35
|
+
const isAnalyticsPublic = p === '/analytics' || p === '/analytics/' || p.startsWith('/analytics/') && p.endsWith('.html');
|
|
36
|
+
// Homepage is public
|
|
37
|
+
if (p === '' || p === '/')
|
|
38
|
+
return next();
|
|
39
|
+
const publicExtensions = ['.css', '.js', '.png', '.jpg', '.jpeg', '.svg', '.ico', '.woff', '.woff2', '.ttf'];
|
|
40
|
+
const isPublic = publicPrefixes.some(prefix => p === prefix || p.startsWith(prefix + '/'))
|
|
41
|
+
|| publicExtensions.some(ext => p.endsWith(ext))
|
|
42
|
+
|| p.startsWith('/css/') || p.startsWith('/js/') || p.startsWith('/images/')
|
|
43
|
+
|| isAnalyticsPublic;
|
|
44
|
+
if (isPublic) {
|
|
45
|
+
return next();
|
|
46
|
+
}
|
|
47
|
+
// Header-only API key transport. Query-string transport was removed because credentials
|
|
48
|
+
// in URLs leak through access logs, browser history, and HTTP Referer headers.
|
|
49
|
+
// Reject any request that attempts to use ?api-key= so silent fallback to header doesn't mask the misuse.
|
|
50
|
+
if (req.query && typeof req.query['api-key'] !== 'undefined') {
|
|
51
|
+
console.error(`[FRAIM AUTH] Rejected query-string api-key for ${req.method} ${req.path}`);
|
|
52
|
+
return res.status(401).json({
|
|
53
|
+
jsonrpc: '2.0',
|
|
54
|
+
error: { code: -32001, message: 'Unauthorized: API keys must be sent in the x-api-key header, not in the query string [AUTH-QUERY-FORBIDDEN]' },
|
|
55
|
+
id: req.body?.id || null
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// Issue #359 — accept FraimAuthSession via cookie or Authorization: Bearer in addition to x-api-key.
|
|
59
|
+
// Priority: x-api-key (programmatic clients) → cookie (web) → bearer (mobile/native).
|
|
60
|
+
const apiKey = req.headers['x-api-key'];
|
|
61
|
+
const sessionFromCookie = (0, cookie_service_1.readSessionCookie)(req);
|
|
62
|
+
const sessionFromBearer = (0, cookie_service_1.readBearerToken)(req);
|
|
63
|
+
const sessionToken = sessionFromCookie || sessionFromBearer;
|
|
64
|
+
// If a session token is present and there's no API key, resolve it.
|
|
65
|
+
if (!apiKey && sessionToken) {
|
|
66
|
+
try {
|
|
67
|
+
const session = await this.dbService.getAuthSession(sessionToken);
|
|
68
|
+
if (!session || session.revoked || session.expiresAt < new Date()) {
|
|
69
|
+
// MCP clients (claude.ai Connectors, Office add-ins, Claude Design) send the
|
|
70
|
+
// FRAIM API key as Authorization: Bearer <key>. The bearer value arrives here
|
|
71
|
+
// as sessionToken but is not a session UUID — it's the raw key. Try it as a
|
|
72
|
+
// direct API key before returning 401.
|
|
73
|
+
if (sessionFromBearer) {
|
|
74
|
+
const directKeyData = await this.dbService.verifyApiKey(sessionFromBearer).catch(() => null);
|
|
75
|
+
const isDirectKeyValid = directKeyData
|
|
76
|
+
&& directKeyData.status !== 'suspended'
|
|
77
|
+
&& !(directKeyData.status === 'expired' || (directKeyData.expiresAt && new Date() > directKeyData.expiresAt));
|
|
78
|
+
if (isDirectKeyValid) {
|
|
79
|
+
this.dbService.updateApiKeyLastUsed(directKeyData.key).catch(() => undefined);
|
|
80
|
+
req.apiKeyData = directKeyData;
|
|
81
|
+
req.authSource = 'bearer-api-key';
|
|
82
|
+
return next();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.error(`[FRAIM AUTH] Invalid/revoked/expired session for ${req.method} ${req.path}`);
|
|
86
|
+
if (sessionFromCookie)
|
|
87
|
+
(0, cookie_service_1.clearSessionCookie)(res);
|
|
88
|
+
(0, audit_log_1.auditLog)('SESSION_REVOKED', {
|
|
89
|
+
userId: session?.userId ?? null,
|
|
90
|
+
sessionId: sessionToken,
|
|
91
|
+
reason: !session ? 'not_found' : session.revoked ? 'revoked' : 'expired',
|
|
92
|
+
outcome: 'failure',
|
|
93
|
+
}).catch(() => undefined);
|
|
94
|
+
return res.status(401).json({
|
|
95
|
+
jsonrpc: '2.0',
|
|
96
|
+
error: { code: -32001, message: 'Unauthorized: Session expired or revoked [AUTH-SESSION-INVALID]' },
|
|
97
|
+
id: req.body?.id || null
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const apiKeyData = await this.dbService.getApiKeyByUserId(session.userId, false);
|
|
101
|
+
if (!apiKeyData) {
|
|
102
|
+
console.error(`[FRAIM AUTH] Session valid but no API key for user ${session.userId}`);
|
|
103
|
+
return res.status(401).json({
|
|
104
|
+
jsonrpc: '2.0',
|
|
105
|
+
error: { code: -32001, message: 'Unauthorized: No identity for session [AUTH-NO-IDENTITY]' },
|
|
106
|
+
id: req.body?.id || null
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
this.dbService.touchAuthSession(session.sessionId).catch(err => {
|
|
110
|
+
console.warn('[FRAIM AUTH] Failed to update auth session lastSeenAt:', err);
|
|
111
|
+
});
|
|
112
|
+
req.apiKeyData = apiKeyData;
|
|
113
|
+
req.authSession = session;
|
|
114
|
+
req.authSource = sessionFromCookie ? 'session-cookie' : 'session-bearer';
|
|
115
|
+
return next();
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
119
|
+
console.error('[FRAIM AUTH] Error resolving session:', msg);
|
|
120
|
+
return res.status(500).json({ error: 'Internal Server Error', details: msg });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Mixed-identity check: if both a session AND an api key are presented, they must agree.
|
|
124
|
+
if (apiKey && sessionToken) {
|
|
125
|
+
try {
|
|
126
|
+
const [keyData, session] = await Promise.all([
|
|
127
|
+
this.dbService.verifyApiKey(apiKey),
|
|
128
|
+
this.dbService.getAuthSession(sessionToken),
|
|
129
|
+
]);
|
|
130
|
+
if (keyData && session && !session.revoked && keyData.userId !== session.userId) {
|
|
131
|
+
console.error(`[FRAIM AUTH] MIXED_IDENTITY: api-key user=${keyData.userId} session user=${session.userId}`);
|
|
132
|
+
(0, audit_log_1.auditLog)('MIXED_IDENTITY', {
|
|
133
|
+
userId: keyData.userId,
|
|
134
|
+
sessionId: sessionToken,
|
|
135
|
+
outcome: 'failure',
|
|
136
|
+
reason: `session_user=${session.userId}`,
|
|
137
|
+
}).catch(() => undefined);
|
|
138
|
+
return res.status(401).json({
|
|
139
|
+
jsonrpc: '2.0',
|
|
140
|
+
error: { code: -32001, message: 'Unauthorized: Mixed identity [AUTH-MIXED-IDENTITY]' },
|
|
141
|
+
id: req.body?.id || null
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.warn('[FRAIM AUTH] Mixed-identity check failed:', err instanceof Error ? err.message : String(err));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Check for missing API key first
|
|
150
|
+
if (!apiKey) {
|
|
151
|
+
// For browser navigation (HTML Accept, non-API path), serve 404 page instead of 401 JSON
|
|
152
|
+
const acceptsHtml = (req.headers.accept || '').includes('text/html');
|
|
153
|
+
const isApiPath = p.startsWith('/api/') || p.startsWith('/mcp') || p.startsWith('/admin');
|
|
154
|
+
if (acceptsHtml && !isApiPath) {
|
|
155
|
+
const page404 = resolveFraimProFilePath('404.html');
|
|
156
|
+
if (page404)
|
|
157
|
+
return res.status(404).sendFile(page404);
|
|
158
|
+
return res.status(404).send('<h1>404</h1><p>Page not found.</p>');
|
|
159
|
+
}
|
|
160
|
+
console.error(`[FRAIM AUTH] Missing API key for ${req.method} ${req.path}`);
|
|
161
|
+
return res.status(401).json({
|
|
162
|
+
jsonrpc: '2.0',
|
|
163
|
+
error: { code: -32001, message: 'Unauthorized: Missing API key [AUTH-MISSING]' },
|
|
164
|
+
id: req.body?.id || null
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Auth bypass for Local Sync and Tests
|
|
168
|
+
const rawAuthMode = req.headers['x-fraim-auth-mode'];
|
|
169
|
+
const authMode = Array.isArray(rawAuthMode) ? rawAuthMode[0] : rawAuthMode;
|
|
170
|
+
const strictTestAuth = typeof authMode === 'string' && authMode.toLowerCase() === 'strict';
|
|
171
|
+
const isTestBypass = process.env.NODE_ENV === 'test' && !this.skipTestBypass && !strictTestAuth && apiKey !== 'invalid-key';
|
|
172
|
+
const isLocalDev = apiKey === 'local-dev' && process.env.NODE_ENV !== 'production';
|
|
173
|
+
if (isLocalDev || isTestBypass) {
|
|
174
|
+
req.apiKeyData = {
|
|
175
|
+
key: apiKey,
|
|
176
|
+
userId: isLocalDev ? 'local-dev-user' : 'test-user',
|
|
177
|
+
orgId: isLocalDev ? 'local-dev-org' : 'test-org',
|
|
178
|
+
isActive: true
|
|
179
|
+
};
|
|
180
|
+
return next();
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const apiKeyData = await this.dbService.verifyApiKey(apiKey);
|
|
184
|
+
if (!apiKeyData) {
|
|
185
|
+
console.error(`❌ FRAIM AUTH: Invalid API key: ${apiKey}`);
|
|
186
|
+
return res.status(401).json({
|
|
187
|
+
jsonrpc: '2.0',
|
|
188
|
+
error: { code: -32001, message: 'Unauthorized: Invalid x-api-key [AUTH-INVALID]' },
|
|
189
|
+
id: req.body?.id || null
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// Check if key is suspended
|
|
193
|
+
if (apiKeyData.status === 'suspended') {
|
|
194
|
+
const reason = apiKeyData.suspensionReason || 'payment failure';
|
|
195
|
+
console.error(`❌ FRAIM AUTH: Suspended API key: ${apiKey} (reason: ${reason})`);
|
|
196
|
+
return res.status(402).json({
|
|
197
|
+
jsonrpc: '2.0',
|
|
198
|
+
error: {
|
|
199
|
+
code: -32002,
|
|
200
|
+
message: `API key suspended due to ${reason}. Please update your payment method.`,
|
|
201
|
+
data: {
|
|
202
|
+
status: 'suspended',
|
|
203
|
+
reason,
|
|
204
|
+
paymentUrl: process.env.STRIPE_BILLING_PORTAL_URL || 'https://fraimworks.ai/billing'
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
id: req.body?.id || null
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// Check if key is expired
|
|
211
|
+
if (apiKeyData.status === 'expired' || (apiKeyData.expiresAt && new Date() > apiKeyData.expiresAt)) {
|
|
212
|
+
// Update status to expired if not already
|
|
213
|
+
if (apiKeyData.status !== 'expired') {
|
|
214
|
+
await this.dbService.updateApiKey(apiKey, { status: 'expired' });
|
|
215
|
+
}
|
|
216
|
+
const tier = apiKeyData.tier;
|
|
217
|
+
const message = tier === 'trial'
|
|
218
|
+
? 'Your 14-day free trial has expired. Please upgrade to continue using FRAIM.'
|
|
219
|
+
: 'Your subscription has expired. Please renew to continue using FRAIM.';
|
|
220
|
+
console.error(`❌ FRAIM AUTH: Expired API key: ${apiKey} (tier: ${tier})`);
|
|
221
|
+
return res.status(402).json({
|
|
222
|
+
jsonrpc: '2.0',
|
|
223
|
+
error: {
|
|
224
|
+
code: -32003,
|
|
225
|
+
message,
|
|
226
|
+
data: {
|
|
227
|
+
status: 'expired',
|
|
228
|
+
tier,
|
|
229
|
+
expiresAt: apiKeyData.expiresAt?.toISOString(),
|
|
230
|
+
paymentUrl: tier === 'trial'
|
|
231
|
+
? (process.env.STRIPE_CHECKOUT_URL || 'https://fraimworks.ai/pricing')
|
|
232
|
+
: (process.env.STRIPE_BILLING_PORTAL_URL || 'https://fraimworks.ai/billing')
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
id: req.body?.id || null
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// Update last used timestamp (async, don't await)
|
|
239
|
+
this.dbService.updateApiKeyLastUsed(apiKey).catch(err => {
|
|
240
|
+
console.warn('[FRAIM AUTH] Failed to update lastUsedAt:', err);
|
|
241
|
+
});
|
|
242
|
+
req.apiKeyData = apiKeyData;
|
|
243
|
+
return next();
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
247
|
+
console.error('❌ FRAIM AUTH: Error during verification:', msg);
|
|
248
|
+
if (error instanceof Error && error.stack)
|
|
249
|
+
console.error(error.stack);
|
|
250
|
+
return res.status(500).json({ error: 'Internal Server Error', details: msg });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
handleAdmin(req, res, next) {
|
|
254
|
+
const adminKey = process.env.FRAIM_ADMIN_KEY;
|
|
255
|
+
if (!adminKey) {
|
|
256
|
+
console.error('⚠️ FRAIM_ADMIN_KEY not configured on server');
|
|
257
|
+
return res.status(503).json({ error: 'Management API disabled (key not set)' });
|
|
258
|
+
}
|
|
259
|
+
const providedKey = req.headers['x-admin-key'];
|
|
260
|
+
if (providedKey !== adminKey) {
|
|
261
|
+
return res.status(403).json({ error: 'Forbidden: Invalid admin key' });
|
|
262
|
+
}
|
|
263
|
+
return next();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
exports.AuthMiddleware = AuthMiddleware;
|