fraim 2.0.179 → 2.0.182

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 (64) 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/commands/add-ide.js +9 -2
  12. package/dist/src/cli/commands/setup.js +14 -44
  13. package/dist/src/cli/distribution/marketplace-bundles.js +5 -1
  14. package/dist/src/cli/setup/ide-detector.js +7 -2
  15. package/dist/src/core/config-loader.js +10 -8
  16. package/dist/src/core/types.js +2 -1
  17. package/dist/src/db/payment-repository.js +61 -0
  18. package/dist/src/fraim/config-loader.js +11 -0
  19. package/dist/src/fraim/db-service.js +2387 -0
  20. package/dist/src/fraim/issues.js +152 -0
  21. package/dist/src/fraim/template-processor.js +184 -0
  22. package/dist/src/fraim/utils/request-utils.js +23 -0
  23. package/dist/src/middleware/auth.js +266 -0
  24. package/dist/src/middleware/cors-config.js +111 -0
  25. package/dist/src/middleware/logger.js +116 -0
  26. package/dist/src/middleware/rate-limit.js +110 -0
  27. package/dist/src/middleware/reject-query-api-key.js +45 -0
  28. package/dist/src/middleware/security-headers.js +41 -0
  29. package/dist/src/middleware/telemetry.js +134 -0
  30. package/dist/src/models/payment.js +2 -0
  31. package/dist/src/routes/analytics.js +1447 -0
  32. package/dist/src/routes/app-routes.js +32 -0
  33. package/dist/src/routes/auth-routes.js +505 -0
  34. package/dist/src/routes/oauth-routes.js +325 -0
  35. package/dist/src/routes/payment-routes.js +186 -0
  36. package/dist/src/routes/persona-catalog-routes.js +84 -0
  37. package/dist/src/services/admin-service.js +229 -0
  38. package/dist/src/services/audit-log-persistence.js +60 -0
  39. package/dist/src/services/audit-log.js +69 -0
  40. package/dist/src/services/cookie-service.js +129 -0
  41. package/dist/src/services/dashboard-access.js +27 -0
  42. package/dist/src/services/demo-seed-service.js +139 -0
  43. package/dist/src/services/email-code.js +23 -0
  44. package/dist/src/services/email-service-clean.js +782 -0
  45. package/dist/src/services/email-service.js +951 -0
  46. package/dist/src/services/installer-service.js +131 -0
  47. package/dist/src/services/mcp-oauth-store.js +33 -0
  48. package/dist/src/services/mcp-service.js +823 -0
  49. package/dist/src/services/oauth-helpers.js +127 -0
  50. package/dist/src/services/org-service.js +89 -0
  51. package/dist/src/services/persona-entitlement-service.js +288 -0
  52. package/dist/src/services/provider-service.js +215 -0
  53. package/dist/src/services/registry-service.js +628 -0
  54. package/dist/src/services/session-service.js +86 -0
  55. package/dist/src/services/trial-reminder-service.js +120 -0
  56. package/dist/src/services/usage-analytics-service.js +419 -0
  57. package/dist/src/services/workspace-identity.js +21 -0
  58. package/dist/src/types/analytics.js +2 -0
  59. package/dist/src/utils/payment-calculator.js +52 -0
  60. package/extensions/office-word/favicon.ico +0 -0
  61. package/extensions/office-word/icon-64.png +0 -0
  62. package/extensions/office-word/manifest.xml +33 -0
  63. package/extensions/office-word/taskpane.html +242 -0
  64. package/package.json +12 -2
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AdminService = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const request_utils_1 = require("../fraim/utils/request-utils");
6
+ const email_service_1 = require("./email-service");
7
+ const dashboard_access_1 = require("./dashboard-access");
8
+ const feature_flags_1 = require("../config/feature-flags");
9
+ const email_code_1 = require("./email-code");
10
+ class AdminService {
11
+ constructor(dbService) {
12
+ this.dbService = dbService;
13
+ }
14
+ /**
15
+ * Handle sales inquiry submission
16
+ */
17
+ async handleSalesInquiry(body, req) {
18
+ const { email, company, projectDetails, teamSize, timeline, budget, source } = body;
19
+ if (!email || !company || !projectDetails) {
20
+ throw new Error('Email, company, and project details are required');
21
+ }
22
+ if (!(0, request_utils_1.validateEmail)(email)) {
23
+ throw new Error('Invalid email address');
24
+ }
25
+ const { ip: ipAddress, userAgent } = (0, request_utils_1.getRequestMeta)(req);
26
+ const inquiry = {
27
+ email: email.toLowerCase().trim(),
28
+ company: company.trim(),
29
+ projectDetails: projectDetails.trim(),
30
+ teamSize: teamSize?.trim() || 'Not specified',
31
+ timeline: timeline?.trim() || undefined,
32
+ budget: budget?.trim() || undefined,
33
+ source: source || 'website',
34
+ timestamp: new Date(),
35
+ ipAddress,
36
+ userAgent
37
+ };
38
+ await this.dbService.createSalesInquiry(inquiry);
39
+ return inquiry;
40
+ }
41
+ /**
42
+ * Handle website signup submission
43
+ */
44
+ async handleWebsiteSignup(body, req) {
45
+ const { email, name, company, useCase, source } = body;
46
+ if (!email || !name || !company) {
47
+ throw new Error('Email, name, and company are required');
48
+ }
49
+ if (!(0, request_utils_1.validateEmail)(email)) {
50
+ throw new Error('Invalid email address');
51
+ }
52
+ const { ip: ipAddress, userAgent } = (0, request_utils_1.getRequestMeta)(req);
53
+ const signup = {
54
+ email: email.toLowerCase().trim(),
55
+ name: name.trim(),
56
+ company: company.trim(),
57
+ useCase: useCase?.trim(),
58
+ source: source || 'website',
59
+ timestamp: new Date(),
60
+ ipAddress,
61
+ userAgent
62
+ };
63
+ await this.dbService.createWebsiteSignup(signup);
64
+ return signup;
65
+ }
66
+ /**
67
+ * List all API keys
68
+ */
69
+ async listApiKeys() {
70
+ return await this.dbService.listApiKeys();
71
+ }
72
+ /**
73
+ * Create a new API key
74
+ */
75
+ async createApiKey(userId, orgId) {
76
+ const key = this.dbService.generateApiKey(userId, orgId);
77
+ const trialExpiresAt = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000); // 14 days
78
+ await this.dbService.createApiKey({
79
+ key,
80
+ userId,
81
+ orgId,
82
+ tier: 'trial',
83
+ status: 'active',
84
+ expiresAt: trialExpiresAt,
85
+ stripeCustomerId: null,
86
+ stripeSubscriptionId: null,
87
+ currentPeriodEnd: null,
88
+ cancelAt: null,
89
+ suspendedAt: null,
90
+ suspensionReason: null,
91
+ lastUsedAt: null,
92
+ apiCallCount: 0,
93
+ personaSystemActive: (0, feature_flags_1.isPersonaEntitlementsEnabled)() || undefined
94
+ });
95
+ return key;
96
+ }
97
+ /**
98
+ * Revoke an API key
99
+ */
100
+ async revokeApiKey(key) {
101
+ return await this.dbService.revokeApiKey(key);
102
+ }
103
+ /**
104
+ * Update expiration for one or more API keys.
105
+ */
106
+ async updateApiKeyExpirations(keys, userIds, expiresAt, labels = []) {
107
+ return await this.dbService.updateApiKeyExpirations(keys, userIds, expiresAt, labels);
108
+ }
109
+ /**
110
+ * Add or remove labels for one or more API keys.
111
+ */
112
+ async updateApiKeyLabels(keys, userIds, labels, operation) {
113
+ return await this.dbService.updateApiKeyLabels(keys, userIds, labels, operation);
114
+ }
115
+ /**
116
+ * List website signups
117
+ */
118
+ async getSignups(limit = 100) {
119
+ return await this.dbService.getWebsiteSignups(limit);
120
+ }
121
+ /**
122
+ * List sales inquiries
123
+ */
124
+ async getSalesInquiries(limit = 100) {
125
+ return await this.dbService.getSalesInquiries(limit);
126
+ }
127
+ /**
128
+ * Request access: send a one-time email code before issuing a trial key.
129
+ * Security: prevents unauthorized access by requiring email ownership verification.
130
+ */
131
+ async requestAccess(body, req) {
132
+ const { email, name, company, useCase } = body;
133
+ if (!email || !name || !company) {
134
+ throw new Error('Email, name, and company are required');
135
+ }
136
+ if (!(0, request_utils_1.validateEmail)(email))
137
+ throw new Error('Invalid email address');
138
+ const emailLower = email.toLowerCase().trim();
139
+ try {
140
+ const { ip: ipAddress, userAgent } = (0, request_utils_1.getRequestMeta)(req);
141
+ await this.dbService.createWebsiteSignup({
142
+ email: emailLower,
143
+ name: name.trim(),
144
+ company: company.trim(),
145
+ useCase: useCase?.trim(),
146
+ source: 'request-access',
147
+ timestamp: new Date(),
148
+ ipAddress,
149
+ userAgent
150
+ });
151
+ }
152
+ catch {
153
+ // Ignore duplicate or other signup errors; email verification still proceeds.
154
+ }
155
+ const verificationToken = (0, crypto_1.randomBytes)(32).toString('hex');
156
+ const verificationCode = (0, email_code_1.generateEmailCode)();
157
+ const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
158
+ await this.dbService.createPendingVerification({
159
+ token: verificationToken,
160
+ codeHash: (0, email_code_1.hashEmailCode)(emailLower, verificationCode),
161
+ email: emailLower,
162
+ verified: false,
163
+ expiresAt,
164
+ createdAt: new Date(),
165
+ usedAt: null
166
+ });
167
+ const emailService = new email_service_1.EmailService();
168
+ const baseUrl = process.env.BASE_URL || 'https://fraimworks.ai';
169
+ await emailService.sendEmailCode(emailLower, `${baseUrl}/get-started`, verificationCode, { purpose: 'request-access' });
170
+ console.log('[FRAIM] request_access email_code_sent', { userId: emailLower });
171
+ return {
172
+ success: true,
173
+ message: 'Check your email for a verification code to start your 14-day free trial'
174
+ };
175
+ }
176
+ async verifyRequestAccessCode(email, code) {
177
+ if (!(0, request_utils_1.validateEmail)(email))
178
+ return { success: false, error: 'Invalid email address' };
179
+ const emailLower = email.toLowerCase().trim();
180
+ const normalizedCode = (0, email_code_1.normalizeEmailCode)(code);
181
+ if (normalizedCode.length < 6)
182
+ return { success: false, error: 'Invalid or expired verification code' };
183
+ const verification = await this.dbService.consumePendingVerificationByCode(emailLower, (0, email_code_1.hashEmailCode)(emailLower, normalizedCode));
184
+ if (!verification) {
185
+ return { success: false, error: 'Invalid or expired verification code' };
186
+ }
187
+ const orgId = 'default';
188
+ const existingKey = await this.dbService.getApiKeyByUserId(emailLower, false);
189
+ let apiKey;
190
+ if (existingKey) {
191
+ apiKey = existingKey.key;
192
+ console.log('[FRAIM] verify_email_code existing_key_found', { userId: emailLower });
193
+ }
194
+ else {
195
+ apiKey = this.dbService.generateApiKey(emailLower, orgId);
196
+ const trialExpiresAt = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
197
+ await this.dbService.createApiKey({
198
+ key: apiKey,
199
+ userId: emailLower,
200
+ orgId,
201
+ tier: 'trial',
202
+ status: 'active',
203
+ expiresAt: trialExpiresAt,
204
+ stripeCustomerId: null,
205
+ stripeSubscriptionId: null,
206
+ currentPeriodEnd: null,
207
+ cancelAt: null,
208
+ suspendedAt: null,
209
+ suspensionReason: null,
210
+ lastUsedAt: null,
211
+ apiCallCount: 0,
212
+ personaSystemActive: (0, feature_flags_1.isPersonaEntitlementsEnabled)() || undefined
213
+ });
214
+ console.log('[FRAIM] verify_email_code trial_key_created', {
215
+ userId: emailLower,
216
+ expiresAt: trialExpiresAt.toISOString()
217
+ });
218
+ }
219
+ const access = await (0, dashboard_access_1.createDashboardAccessLink)(this.dbService, emailLower, apiKey);
220
+ console.log('[FRAIM] verify_email_code verification_completed', { userId: emailLower });
221
+ return {
222
+ success: true,
223
+ key: apiKey,
224
+ dashboardToken: access.token,
225
+ redirectUrl: access.redirectPath
226
+ };
227
+ }
228
+ }
229
+ exports.AdminService = AdminService;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dbBackedAuditLogPersistence = dbBackedAuditLogPersistence;
4
+ function toFraimAuditRecord(record) {
5
+ const f = record.fields;
6
+ const userId = typeof f.userId === 'string' ? f.userId : null;
7
+ const sessionId = typeof f.sessionId === 'string' ? f.sessionId : null;
8
+ const provider = typeof f.provider === 'string' ? f.provider : undefined;
9
+ const surface = typeof f.surface === 'string' ? f.surface : undefined;
10
+ const ip = typeof f.ip === 'string' ? f.ip : null;
11
+ const userAgent = typeof f.userAgent === 'string' ? f.userAgent : null;
12
+ const reason = typeof f.reason === 'string' ? f.reason : undefined;
13
+ const outcome = f.outcome === 'success' || f.outcome === 'failure' ? f.outcome : undefined;
14
+ const known = new Set(['userId', 'sessionId', 'provider', 'surface', 'ip', 'userAgent', 'reason', 'outcome']);
15
+ const extra = {};
16
+ for (const [k, v] of Object.entries(f)) {
17
+ if (!known.has(k))
18
+ extra[k] = v;
19
+ }
20
+ return {
21
+ sequence: record.sequence,
22
+ ts: new Date(record.ts),
23
+ event: record.event,
24
+ userId,
25
+ sessionId,
26
+ provider,
27
+ surface,
28
+ ip,
29
+ userAgent,
30
+ reason,
31
+ outcome,
32
+ extra: Object.keys(extra).length > 0 ? extra : undefined,
33
+ };
34
+ }
35
+ function dbBackedAuditLogPersistence(dbService) {
36
+ return {
37
+ async append(record) {
38
+ await dbService.appendAuditRecord(toFraimAuditRecord(record));
39
+ },
40
+ async listForUser(userId, sinceMs, limit) {
41
+ const rows = await dbService.listAuditRecordsForUser(userId, sinceMs, limit);
42
+ return rows.map(r => ({
43
+ sequence: r.sequence,
44
+ ts: r.ts.toISOString(),
45
+ event: r.event,
46
+ fields: {
47
+ userId: r.userId,
48
+ sessionId: r.sessionId,
49
+ provider: r.provider,
50
+ surface: r.surface,
51
+ ip: r.ip,
52
+ userAgent: r.userAgent,
53
+ reason: r.reason,
54
+ outcome: r.outcome,
55
+ ...(r.extra ?? {}),
56
+ },
57
+ }));
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setAuditLogPersistence = setAuditLogPersistence;
4
+ exports.resetAuditLogForTests = resetAuditLogForTests;
5
+ exports.getInMemoryAuditRecords = getInMemoryAuditRecords;
6
+ exports.auditLog = auditLog;
7
+ exports.listAuditEventsForUser = listAuditEventsForUser;
8
+ const inMemorySink = [];
9
+ const inMemoryPersistence = {
10
+ async append(record) {
11
+ inMemorySink.push(record);
12
+ },
13
+ async listForUser(userId, sinceMs, limit) {
14
+ const since = Date.now() - sinceMs;
15
+ return inMemorySink
16
+ .filter(r => r.fields.userId === userId && new Date(r.ts).getTime() >= since)
17
+ .slice(-limit);
18
+ },
19
+ };
20
+ let activePersistence = inMemoryPersistence;
21
+ let sequenceCounter = 0;
22
+ function setAuditLogPersistence(persistence) {
23
+ activePersistence = persistence;
24
+ }
25
+ function resetAuditLogForTests() {
26
+ activePersistence = inMemoryPersistence;
27
+ inMemorySink.length = 0;
28
+ sequenceCounter = 0;
29
+ }
30
+ function getInMemoryAuditRecords() {
31
+ return [...inMemorySink];
32
+ }
33
+ async function auditLog(event, fields = {}) {
34
+ const record = {
35
+ sequence: ++sequenceCounter,
36
+ ts: new Date().toISOString(),
37
+ event,
38
+ fields,
39
+ };
40
+ try {
41
+ await activePersistence.append(record);
42
+ }
43
+ catch (err) {
44
+ console.error('[FRAIM AUDIT] Failed to persist audit record:', err);
45
+ }
46
+ const safeFields = { ...fields };
47
+ if (typeof safeFields.userId === 'string') {
48
+ safeFields.userId = redactEmail(safeFields.userId);
49
+ }
50
+ console.log(JSON.stringify({
51
+ kind: 'fraim_audit',
52
+ sequence: record.sequence,
53
+ ts: record.ts,
54
+ event: record.event,
55
+ ...safeFields,
56
+ }));
57
+ }
58
+ async function listAuditEventsForUser(userId, sinceMs = 30 * 24 * 60 * 60 * 1000, limit = 200) {
59
+ return activePersistence.listForUser(userId, sinceMs, limit);
60
+ }
61
+ function redactEmail(email) {
62
+ const at = email.indexOf('@');
63
+ if (at < 1)
64
+ return '***';
65
+ const local = email.slice(0, at);
66
+ const domain = email.slice(at + 1);
67
+ const visible = local.length <= 2 ? local : `${local.slice(0, 2)}***`;
68
+ return `${visible}@${domain}`;
69
+ }
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.__testing = exports.OAUTH_PENDING_COOKIE_NAME = exports.SESSION_COOKIE_NAME = void 0;
4
+ exports.setSessionCookie = setSessionCookie;
5
+ exports.clearSessionCookie = clearSessionCookie;
6
+ exports.setOAuthPendingCookie = setOAuthPendingCookie;
7
+ exports.clearOAuthPendingCookie = clearOAuthPendingCookie;
8
+ exports.readSessionCookie = readSessionCookie;
9
+ exports.readOAuthPendingCookie = readOAuthPendingCookie;
10
+ exports.readBearerToken = readBearerToken;
11
+ exports.SESSION_COOKIE_NAME = 'fraim_session';
12
+ exports.OAUTH_PENDING_COOKIE_NAME = 'fraim_oauth_pending';
13
+ const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
14
+ const OAUTH_PENDING_MAX_AGE_SECONDS = 10 * 60;
15
+ function buildCookieHeader(attrs) {
16
+ const parts = [`${attrs.name}=${attrs.value}`];
17
+ parts.push(`Max-Age=${attrs.maxAgeSeconds}`);
18
+ parts.push(`Path=${attrs.path ?? '/'}`);
19
+ if (attrs.domain)
20
+ parts.push(`Domain=${attrs.domain}`);
21
+ if (attrs.httpOnly !== false)
22
+ parts.push('HttpOnly');
23
+ if (attrs.secure !== false)
24
+ parts.push('Secure');
25
+ parts.push(`SameSite=${attrs.sameSite ?? 'Lax'}`);
26
+ return parts.join('; ');
27
+ }
28
+ function isProduction() {
29
+ return process.env.NODE_ENV === 'production';
30
+ }
31
+ function getCookieDomain() {
32
+ if (!isProduction())
33
+ return undefined;
34
+ // In production, only use a domain if explicitly set. This allows the cookie
35
+ // to work on any domain (fraimworks.ai, azurewebsites.net, etc.) without
36
+ // needing domain-specific configuration.
37
+ return process.env.FRAIM_COOKIE_DOMAIN || undefined;
38
+ }
39
+ function getCookieSecure() {
40
+ return isProduction();
41
+ }
42
+ function appendSetCookie(res, header) {
43
+ const existing = res.getHeader('Set-Cookie');
44
+ if (!existing) {
45
+ res.setHeader('Set-Cookie', header);
46
+ return;
47
+ }
48
+ if (Array.isArray(existing)) {
49
+ res.setHeader('Set-Cookie', [...existing, header]);
50
+ return;
51
+ }
52
+ res.setHeader('Set-Cookie', [String(existing), header]);
53
+ }
54
+ function setSessionCookie(res, sessionId) {
55
+ appendSetCookie(res, buildCookieHeader({
56
+ name: exports.SESSION_COOKIE_NAME,
57
+ value: sessionId,
58
+ maxAgeSeconds: SESSION_MAX_AGE_SECONDS,
59
+ domain: getCookieDomain(),
60
+ secure: getCookieSecure(),
61
+ sameSite: 'Lax',
62
+ }));
63
+ }
64
+ function clearSessionCookie(res) {
65
+ appendSetCookie(res, buildCookieHeader({
66
+ name: exports.SESSION_COOKIE_NAME,
67
+ value: '',
68
+ maxAgeSeconds: 0,
69
+ domain: getCookieDomain(),
70
+ secure: getCookieSecure(),
71
+ sameSite: 'Lax',
72
+ }));
73
+ }
74
+ function setOAuthPendingCookie(res, pendingId) {
75
+ appendSetCookie(res, buildCookieHeader({
76
+ name: exports.OAUTH_PENDING_COOKIE_NAME,
77
+ value: pendingId,
78
+ maxAgeSeconds: OAUTH_PENDING_MAX_AGE_SECONDS,
79
+ domain: getCookieDomain(),
80
+ secure: getCookieSecure(),
81
+ sameSite: 'Lax',
82
+ }));
83
+ }
84
+ function clearOAuthPendingCookie(res) {
85
+ appendSetCookie(res, buildCookieHeader({
86
+ name: exports.OAUTH_PENDING_COOKIE_NAME,
87
+ value: '',
88
+ maxAgeSeconds: 0,
89
+ domain: getCookieDomain(),
90
+ secure: getCookieSecure(),
91
+ sameSite: 'Lax',
92
+ }));
93
+ }
94
+ function parseCookieHeader(header) {
95
+ const out = new Map();
96
+ if (!header)
97
+ return out;
98
+ for (const pair of header.split(';')) {
99
+ const eq = pair.indexOf('=');
100
+ if (eq < 0)
101
+ continue;
102
+ const name = pair.slice(0, eq).trim();
103
+ const value = pair.slice(eq + 1).trim();
104
+ if (name)
105
+ out.set(name, decodeURIComponent(value));
106
+ }
107
+ return out;
108
+ }
109
+ function readSessionCookie(req) {
110
+ const cookies = parseCookieHeader(req.headers.cookie);
111
+ return cookies.get(exports.SESSION_COOKIE_NAME) || null;
112
+ }
113
+ function readOAuthPendingCookie(req) {
114
+ const cookies = parseCookieHeader(req.headers.cookie);
115
+ return cookies.get(exports.OAUTH_PENDING_COOKIE_NAME) || null;
116
+ }
117
+ function readBearerToken(req) {
118
+ const auth = req.headers.authorization;
119
+ if (!auth || typeof auth !== 'string')
120
+ return null;
121
+ const match = /^Bearer\s+(.+)$/i.exec(auth);
122
+ return match ? match[1].trim() : null;
123
+ }
124
+ exports.__testing = {
125
+ buildCookieHeader,
126
+ parseCookieHeader,
127
+ SESSION_MAX_AGE_SECONDS,
128
+ OAUTH_PENDING_MAX_AGE_SECONDS,
129
+ };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDashboardAccessLink = createDashboardAccessLink;
4
+ exports.createDashboardAccessUrl = createDashboardAccessUrl;
5
+ const crypto_1 = require("crypto");
6
+ async function createDashboardAccessLink(dbService, email, apiKey, tokenFactory = () => (0, crypto_1.randomBytes)(32).toString('hex')) {
7
+ const token = tokenFactory();
8
+ const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
9
+ await dbService.createAccessToken({
10
+ token,
11
+ email,
12
+ key: apiKey,
13
+ expiresAt,
14
+ createdAt: new Date()
15
+ });
16
+ const baseUrl = (process.env.BASE_URL || 'https://fraimworks.ai').replace(/\/$/, '');
17
+ return {
18
+ token,
19
+ expiresAt,
20
+ dashboardUrl: `${baseUrl}/dashboard?token=${token}`,
21
+ redirectPath: `/dashboard?token=${token}`
22
+ };
23
+ }
24
+ async function createDashboardAccessUrl(dbService, email, apiKey, tokenFactory) {
25
+ const { dashboardUrl } = await createDashboardAccessLink(dbService, email, apiKey, tokenFactory);
26
+ return dashboardUrl;
27
+ }
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.seedDemoDataForUser = seedDemoDataForUser;
4
+ const crypto_1 = require("crypto");
5
+ const STAGE_JOBS = [
6
+ { name: 'brainstorming', category: 'brainstorming', jobs: ['idea-exploration', 'opportunity-mapping'] },
7
+ { name: 'customer-development', category: 'customer-development', jobs: ['interview-preparation', 'process-interview-notes', 'review-customer-development'] },
8
+ { name: 'product-building', category: 'product-building', jobs: ['feature-specification', 'technical-design', 'feature-implementation', 'pr-iteration'] },
9
+ { name: 'fundraising', category: 'fundraising', jobs: ['investor-outreach', 'deck-iteration'] },
10
+ { name: 'gtm', category: 'gtm', jobs: ['gtm-plan', 'channel-tests'] },
11
+ { name: 'marketing', category: 'marketing', jobs: ['brand-creation', 'whitepaper-thought-leadership'] },
12
+ { name: 'biz-dev', category: 'biz-dev', jobs: ['partnership-outreach'] },
13
+ { name: 'ai-management', category: 'ai-management', jobs: ['analyze-why-you-messed-up'] },
14
+ { name: 'other', category: 'other', jobs: ['follow-your-mentor'] },
15
+ ];
16
+ const SKILLS = [
17
+ { name: 'plan/feature-spec', category: 'product-building' },
18
+ { name: 'plan/technical-design', category: 'product-building' },
19
+ { name: 'execute/feature-implementation', category: 'product-building' },
20
+ { name: 'review/code-review', category: 'product-building' },
21
+ { name: 'customer/interview', category: 'customer-development' },
22
+ { name: 'customer/synthesis', category: 'customer-development' },
23
+ { name: 'gtm/channel-test', category: 'gtm' },
24
+ { name: 'marketing/copy-iteration', category: 'marketing' },
25
+ ];
26
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
27
+ async function insertWithBackoff(collection, doc, attempt = 0) {
28
+ try {
29
+ await collection.insertOne(doc);
30
+ }
31
+ catch (error) {
32
+ const msg = String(error?.message || error || '');
33
+ const is429 = msg.includes('TooManyRequests') || msg.includes('429');
34
+ if (is429 && attempt < 6) {
35
+ const wait = 200 * Math.pow(2, attempt) + Math.floor(Math.random() * 100);
36
+ await sleep(wait);
37
+ return insertWithBackoff(collection, doc, attempt + 1);
38
+ }
39
+ throw error;
40
+ }
41
+ }
42
+ async function seedDemoDataForUser(dbService, userId) {
43
+ const now = Date.now();
44
+ const day = 24 * 60 * 60 * 1000;
45
+ const db = dbService.db;
46
+ if (!db)
47
+ throw new Error('DB not connected');
48
+ const usageCollectionName = dbService.usageEventsCollectionName || 'fraim_usage_events';
49
+ const usageCollection = db.collection(usageCollectionName);
50
+ const qualityCollection = db.collection('fraim_quality_scores');
51
+ // Make seeding idempotent — wipe any prior partial seed for this user before inserting.
52
+ await usageCollection.deleteMany({ userId }).catch(() => undefined);
53
+ await qualityCollection.deleteMany({ userId }).catch(() => undefined);
54
+ // ~120 job runs spread over 28 days, weighted toward product-building so the doughnut + bars look populated.
55
+ let jobCount = 0;
56
+ for (let i = 0; i < 120; i++) {
57
+ const stage = STAGE_JOBS[i % STAGE_JOBS.length];
58
+ const jobName = stage.jobs[i % stage.jobs.length];
59
+ const daysAgo = Math.floor(Math.random() * 28);
60
+ const hoursAgo = Math.floor(Math.random() * 23);
61
+ const createdAt = new Date(now - daysAgo * day - hoursAgo * 3600 * 1000);
62
+ const success = Math.random() > 0.12;
63
+ const sessionId = `demo-session-${Math.floor(i / 4)}`;
64
+ const event = {
65
+ type: 'job',
66
+ name: jobName,
67
+ userId,
68
+ sessionId,
69
+ success,
70
+ // Both top-level and args.category — the heatmap aggregation reads args.category.
71
+ category: stage.category,
72
+ args: { category: stage.category },
73
+ jobId: (0, crypto_1.randomUUID)(),
74
+ jobPhase: 'submission',
75
+ createdAt,
76
+ };
77
+ if (i % 3 === 0)
78
+ event.repoIdentifier = 'demo-org/demo-app';
79
+ await insertWithBackoff(usageCollection, event);
80
+ jobCount += 1;
81
+ }
82
+ // ~40 skill invocations to light up the brain heatmap across regions.
83
+ let skillCount = 0;
84
+ for (let i = 0; i < 40; i++) {
85
+ const skill = SKILLS[i % SKILLS.length];
86
+ const daysAgo = Math.floor(Math.random() * 28);
87
+ const createdAt = new Date(now - daysAgo * day - 3600 * 1000 * (i % 23));
88
+ const sessionId = `demo-session-${Math.floor(i / 3)}`;
89
+ await insertWithBackoff(usageCollection, {
90
+ type: 'skill',
91
+ name: skill.name,
92
+ userId,
93
+ sessionId,
94
+ success: true,
95
+ category: skill.category,
96
+ args: { category: skill.category },
97
+ createdAt,
98
+ });
99
+ skillCount += 1;
100
+ }
101
+ // Quality assessments use the dashboard's canonical stage categories.
102
+ const QUALITY_STAGES = [
103
+ { category: 'customer-development', jobName: 'process-interview-notes' },
104
+ { category: 'business-strategy', jobName: 'review-business-strategy' },
105
+ { category: 'branding', jobName: 'branding-quality-audit' },
106
+ { category: 'product-quality', jobName: 'code-quality-assessment' },
107
+ { category: 'test-quality', jobName: 'test-quality-assessment' },
108
+ { category: 'security', jobName: 'security-review' },
109
+ { category: 'production-readiness', jobName: 'production-readiness-review' },
110
+ { category: 'fundraising', jobName: 'fundraising-evidence-review' },
111
+ { category: 'go-to-market', jobName: 'marketing-strategy-definition' },
112
+ ];
113
+ let qualityCount = 0;
114
+ for (const stage of QUALITY_STAGES) {
115
+ for (let n = 0; n < 3; n++) {
116
+ const composite = 4.5 + n * 1.5 + Math.random();
117
+ const createdAt = new Date(now - (21 - n * 7) * day);
118
+ const record = {
119
+ userId,
120
+ jobName: stage.jobName,
121
+ jobId: (0, crypto_1.randomUUID)(),
122
+ sessionId: `demo-session-q-${stage.category}-${n}`,
123
+ stageCategory: stage.category,
124
+ scores: {
125
+ composite: Number(composite.toFixed(2)),
126
+ market_evidence: { score: Number((composite - 0.4).toFixed(2)), rationale: 'Synthetic demo data.' },
127
+ strategic_coherence: { score: Number((composite - 0.2).toFixed(2)), rationale: 'Synthetic demo data.' },
128
+ standards_compliance: Number((composite + 0.1).toFixed(2)),
129
+ coaching: 'Synthetic demo coaching note. Keep iterating.',
130
+ },
131
+ artifactPath: `docs/${stage.category}/${stage.jobName}-demo-${n}.md`,
132
+ createdAt,
133
+ };
134
+ await insertWithBackoff(qualityCollection, record);
135
+ qualityCount += 1;
136
+ }
137
+ }
138
+ return { jobEventsInserted: jobCount, skillEventsInserted: skillCount, qualityRecordsInserted: qualityCount };
139
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateEmailCode = generateEmailCode;
4
+ exports.normalizeEmailCode = normalizeEmailCode;
5
+ exports.hashEmailCode = hashEmailCode;
6
+ const crypto_1 = require("crypto");
7
+ const EMAIL_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
8
+ function generateEmailCode() {
9
+ const bytes = (0, crypto_1.randomBytes)(8);
10
+ let code = '';
11
+ for (const byte of bytes) {
12
+ code += EMAIL_CODE_ALPHABET[byte % EMAIL_CODE_ALPHABET.length];
13
+ }
14
+ return code;
15
+ }
16
+ function normalizeEmailCode(input) {
17
+ return String(input ?? '').toUpperCase().replace(/[^A-Z0-9]/g, '');
18
+ }
19
+ function hashEmailCode(email, code) {
20
+ return (0, crypto_1.createHash)('sha256')
21
+ .update(`${email.toLowerCase()}:${normalizeEmailCode(code)}`)
22
+ .digest('hex');
23
+ }