fraim-framework 2.0.183 → 2.0.185
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 -10
- package/dist/src/ai-hub/hosts.js +1 -1
- package/dist/src/ai-hub/manager-turns.js +2 -2
- package/dist/src/ai-hub/preferences.js +3 -2
- package/dist/src/ai-hub/remote-hub-gateway.js +88 -0
- package/dist/src/ai-hub/server.js +119 -184
- package/dist/src/api/ai-hub/manager-team.js +93 -0
- package/dist/src/cli/distribution/marketplace-bundles.js +26 -27
- package/dist/src/fraim/db-service.js +25 -3
- package/dist/src/routes/oauth-routes.js +40 -5
- package/dist/src/services/account-provisioning.js +38 -0
- package/dist/src/services/admin-service.js +10 -36
- package/package.json +1 -1
- package/public/ai-hub/index.html +77 -90
- package/public/ai-hub/review.css +4 -2
- package/public/ai-hub/script.js +526 -562
- package/public/ai-hub/styles.css +151 -35
|
@@ -95,11 +95,6 @@ function asArray(value) {
|
|
|
95
95
|
function stringValue(value) {
|
|
96
96
|
return typeof value === 'string' && value.trim() ? value : null;
|
|
97
97
|
}
|
|
98
|
-
function readPackageVersion(repoRoot, result) {
|
|
99
|
-
const relPath = 'package.json';
|
|
100
|
-
const packageJson = readJsonObject(repoRoot, relPath, result);
|
|
101
|
-
return packageJson ? stringValue(packageJson.version) : null;
|
|
102
|
-
}
|
|
103
98
|
function requireHttpsUrl(value, relPath, field, result) {
|
|
104
99
|
const url = stringValue(value);
|
|
105
100
|
if (!url) {
|
|
@@ -202,7 +197,7 @@ function validateTargetMatrix(repoRoot, result) {
|
|
|
202
197
|
const relPath = path_1.default.join(TARGET_ROOT, 'marketplace-targets.json');
|
|
203
198
|
const targetMatrix = readJsonObject(repoRoot, relPath, result);
|
|
204
199
|
if (!targetMatrix) {
|
|
205
|
-
return { remoteMcpUrl: null, oauthAuthorizationServer: null,
|
|
200
|
+
return { remoteMcpUrl: null, oauthAuthorizationServer: null, contactEmail: null };
|
|
206
201
|
}
|
|
207
202
|
assertEqual(targetMatrix.schema, 'fraim-marketplace-targets-v1', relPath, 'schema', result);
|
|
208
203
|
assertEqual(targetMatrix.issue, 674, relPath, 'issue', result);
|
|
@@ -214,10 +209,6 @@ function validateTargetMatrix(repoRoot, result) {
|
|
|
214
209
|
if (!remoteMcp) {
|
|
215
210
|
addIssue(result.errors, relPath, 'remoteMcp must be an object');
|
|
216
211
|
}
|
|
217
|
-
const productVersion = product ? stringValue(product.packageVersion) : null;
|
|
218
|
-
if (!productVersion) {
|
|
219
|
-
addIssue(result.errors, relPath, 'product.packageVersion must be set');
|
|
220
|
-
}
|
|
221
212
|
const contactEmail = product ? stringValue(product.contactEmail) : null;
|
|
222
213
|
if (!contactEmail) {
|
|
223
214
|
addIssue(result.errors, relPath, 'product.contactEmail must be set');
|
|
@@ -225,10 +216,6 @@ function validateTargetMatrix(repoRoot, result) {
|
|
|
225
216
|
if (contactEmail && product) {
|
|
226
217
|
assertEqual(product.support, contactEmail, relPath, 'product.support', result);
|
|
227
218
|
}
|
|
228
|
-
const packageVersion = readPackageVersion(repoRoot, result);
|
|
229
|
-
if (productVersion && packageVersion) {
|
|
230
|
-
assertEqual(productVersion, packageVersion, relPath, 'product.packageVersion', result);
|
|
231
|
-
}
|
|
232
219
|
const remoteMcpUrl = remoteMcp
|
|
233
220
|
? requireHttpsUrl(remoteMcp.url, relPath, 'remoteMcp.url', result)
|
|
234
221
|
: null;
|
|
@@ -236,7 +223,7 @@ function validateTargetMatrix(repoRoot, result) {
|
|
|
236
223
|
? requireHttpsUrl(remoteMcp.oauthAuthorizationServer, relPath, 'remoteMcp.oauthAuthorizationServer', result)
|
|
237
224
|
: null;
|
|
238
225
|
validateTargetEntries(repoRoot, relPath, asArray(targetMatrix.targets), result);
|
|
239
|
-
return { remoteMcpUrl, oauthAuthorizationServer,
|
|
226
|
+
return { remoteMcpUrl, oauthAuthorizationServer, contactEmail };
|
|
240
227
|
}
|
|
241
228
|
function validateOpenAiAssets(repoRoot, openAiRoot, result) {
|
|
242
229
|
assertFile(repoRoot, path_1.default.join(openAiRoot, 'README.md'), result);
|
|
@@ -345,11 +332,13 @@ function validateCodexManifestInterface(repoRoot, pluginRoot, manifestRelPath, m
|
|
|
345
332
|
assertRelativeAsset(repoRoot, pluginRoot, screenshot, manifestRelPath, `interface.screenshots[${index}]`, result);
|
|
346
333
|
}
|
|
347
334
|
}
|
|
348
|
-
function validateCodexManifest(repoRoot, pluginRoot, manifest, manifestRelPath,
|
|
335
|
+
function validateCodexManifest(repoRoot, pluginRoot, manifest, manifestRelPath, contactEmail, result) {
|
|
349
336
|
if (!manifest)
|
|
350
337
|
return;
|
|
351
338
|
assertEqual(manifest.name, 'fraim', manifestRelPath, 'name', result);
|
|
352
|
-
|
|
339
|
+
if (!stringValue(manifest.version)) {
|
|
340
|
+
addIssue(result.errors, manifestRelPath, 'version must be a non-empty string');
|
|
341
|
+
}
|
|
353
342
|
assertEqual(asObject(manifest.author)?.email, contactEmail, manifestRelPath, 'author.email', result);
|
|
354
343
|
assertEqual(manifest.skills, './skills/', manifestRelPath, 'skills', result);
|
|
355
344
|
assertEqual(manifest.mcpServers, './.mcp.json', manifestRelPath, 'mcpServers', result);
|
|
@@ -409,11 +398,11 @@ function validateCodexPackage(repoRoot, result, targetDetails) {
|
|
|
409
398
|
const manifest = readJsonObject(repoRoot, manifestRelPath, result);
|
|
410
399
|
const mcpConfig = readJsonObject(repoRoot, mcpRelPath, result);
|
|
411
400
|
validateCodexMarketplace(marketplace, marketplaceRelPath, result);
|
|
412
|
-
validateCodexManifest(repoRoot, pluginRoot, manifest, manifestRelPath, targetDetails.
|
|
401
|
+
validateCodexManifest(repoRoot, pluginRoot, manifest, manifestRelPath, targetDetails.contactEmail, result);
|
|
413
402
|
validateCodexMcpConfig(mcpConfig, mcpRelPath, targetDetails.remoteMcpUrl, result);
|
|
414
403
|
validateCodexSkill(repoRoot, skillRelPath, result);
|
|
415
404
|
}
|
|
416
|
-
function validateSimpleMarketplaceManifest(marketplace, marketplaceRelPath, expectedName, expectedSource,
|
|
405
|
+
function validateSimpleMarketplaceManifest(marketplace, marketplaceRelPath, expectedName, expectedSource, contactEmail, result) {
|
|
417
406
|
if (!marketplace)
|
|
418
407
|
return;
|
|
419
408
|
assertEqual(marketplace.name, expectedName, marketplaceRelPath, 'name', result);
|
|
@@ -433,7 +422,9 @@ function validateSimpleMarketplaceManifest(marketplace, marketplaceRelPath, expe
|
|
|
433
422
|
return;
|
|
434
423
|
}
|
|
435
424
|
assertEqual(fraimEntry.source, expectedSource, marketplaceRelPath, 'fraim.source', result);
|
|
436
|
-
|
|
425
|
+
if (!stringValue(fraimEntry.version)) {
|
|
426
|
+
addIssue(result.errors, marketplaceRelPath, 'fraim.version must be a non-empty string');
|
|
427
|
+
}
|
|
437
428
|
const author = asObject(fraimEntry.author);
|
|
438
429
|
if (author) {
|
|
439
430
|
assertEqual(author.email, contactEmail, marketplaceRelPath, 'fraim.author.email', result);
|
|
@@ -453,7 +444,9 @@ function validateMcpRegistryPackage(repoRoot, result, targetDetails) {
|
|
|
453
444
|
if (!description || description.length > 100) {
|
|
454
445
|
addIssue(result.errors, serverRelPath, 'description must be a non-empty string of at most 100 characters');
|
|
455
446
|
}
|
|
456
|
-
|
|
447
|
+
if (!stringValue(serverJson.version)) {
|
|
448
|
+
addIssue(result.errors, serverRelPath, 'version must be a non-empty string');
|
|
449
|
+
}
|
|
457
450
|
requireHttpsUrl(serverJson.websiteUrl, serverRelPath, 'websiteUrl', result);
|
|
458
451
|
const repository = asObject(serverJson.repository);
|
|
459
452
|
assertEqual(repository?.url, 'https://github.com/mathursrus/FRAIM', serverRelPath, 'repository.url', result);
|
|
@@ -484,11 +477,13 @@ function validateCursorPackage(repoRoot, result, targetDetails) {
|
|
|
484
477
|
const mcpConfig = readJsonObject(repoRoot, mcpRelPath, result);
|
|
485
478
|
assertFile(repoRoot, path_1.default.join(cursorRoot, 'README.md'), result);
|
|
486
479
|
assertFile(repoRoot, path_1.default.join(pluginRoot, 'README.md'), result);
|
|
487
|
-
validateSimpleMarketplaceManifest(rootMarketplace, ROOT_CURSOR_MARKETPLACE_PATH, 'fraim-marketplace', 'marketplaces/fraim/cursor/plugins/fraim', targetDetails.
|
|
488
|
-
validateSimpleMarketplaceManifest(targetMarketplace, targetMarketplaceRelPath, 'fraim-marketplace', 'plugins/fraim', targetDetails.
|
|
480
|
+
validateSimpleMarketplaceManifest(rootMarketplace, ROOT_CURSOR_MARKETPLACE_PATH, 'fraim-marketplace', 'marketplaces/fraim/cursor/plugins/fraim', targetDetails.contactEmail, result);
|
|
481
|
+
validateSimpleMarketplaceManifest(targetMarketplace, targetMarketplaceRelPath, 'fraim-marketplace', 'plugins/fraim', targetDetails.contactEmail, result);
|
|
489
482
|
if (manifest) {
|
|
490
483
|
assertEqual(manifest.name, 'fraim', manifestRelPath, 'name', result);
|
|
491
|
-
|
|
484
|
+
if (!stringValue(manifest.version)) {
|
|
485
|
+
addIssue(result.errors, manifestRelPath, 'version must be a non-empty string');
|
|
486
|
+
}
|
|
492
487
|
assertEqual(asObject(manifest.author)?.email, targetDetails.contactEmail, manifestRelPath, 'author.email', result);
|
|
493
488
|
assertEqual(manifest.rules, 'rules/', manifestRelPath, 'rules', result);
|
|
494
489
|
assertEqual(manifest.skills, 'skills/', manifestRelPath, 'skills', result);
|
|
@@ -532,10 +527,12 @@ function validateCopilotPackage(repoRoot, result, targetDetails) {
|
|
|
532
527
|
const mcpConfig = readJsonObject(repoRoot, mcpRelPath, result);
|
|
533
528
|
assertFile(repoRoot, path_1.default.join(vscodeRoot, 'README.md'), result);
|
|
534
529
|
assertFile(repoRoot, path_1.default.join(pluginRoot, 'README.md'), result);
|
|
535
|
-
validateSimpleMarketplaceManifest(rootMarketplace, ROOT_COPILOT_MARKETPLACE_PATH, 'fraim-copilot-marketplace', 'marketplaces/fraim/vscode/plugins/fraim', targetDetails.
|
|
530
|
+
validateSimpleMarketplaceManifest(rootMarketplace, ROOT_COPILOT_MARKETPLACE_PATH, 'fraim-copilot-marketplace', 'marketplaces/fraim/vscode/plugins/fraim', targetDetails.contactEmail, result);
|
|
536
531
|
if (manifest) {
|
|
537
532
|
assertEqual(manifest.name, 'fraim', manifestRelPath, 'name', result);
|
|
538
|
-
|
|
533
|
+
if (!stringValue(manifest.version)) {
|
|
534
|
+
addIssue(result.errors, manifestRelPath, 'version must be a non-empty string');
|
|
535
|
+
}
|
|
539
536
|
assertEqual(asObject(manifest.author)?.email, targetDetails.contactEmail, manifestRelPath, 'author.email', result);
|
|
540
537
|
assertEqual(manifest.skills, 'skills/', manifestRelPath, 'skills', result);
|
|
541
538
|
assertEqual(manifest.commands, 'commands/', manifestRelPath, 'commands', result);
|
|
@@ -559,7 +556,9 @@ function validateGeminiPackage(repoRoot, result, targetDetails) {
|
|
|
559
556
|
if (!manifest)
|
|
560
557
|
return;
|
|
561
558
|
assertEqual(manifest.name, 'fraim', manifestRelPath, 'name', result);
|
|
562
|
-
|
|
559
|
+
if (!stringValue(manifest.version)) {
|
|
560
|
+
addIssue(result.errors, manifestRelPath, 'version must be a non-empty string');
|
|
561
|
+
}
|
|
563
562
|
assertEqual(manifest.contextFileName, 'GEMINI.md', manifestRelPath, 'contextFileName', result);
|
|
564
563
|
const servers = asObject(manifest.mcpServers);
|
|
565
564
|
const fraimServer = asObject(servers?.fraim);
|
|
@@ -74,6 +74,17 @@ function normalizeLabels(labels) {
|
|
|
74
74
|
function escapeRegex(value) {
|
|
75
75
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Ordering for persona entitlements, applied in memory. Azure Cosmos DB (Mongo API)
|
|
79
|
+
* rejects an order-by on these fields without a composite index, so the sort must not
|
|
80
|
+
* be pushed into the query. Mirrors the former `.sort({ personaKey: 1, hireMode: 1 })`.
|
|
81
|
+
*/
|
|
82
|
+
function comparePersonaEntitlements(a, b) {
|
|
83
|
+
const byKey = (a.personaKey || '').localeCompare(b.personaKey || '');
|
|
84
|
+
if (byKey !== 0)
|
|
85
|
+
return byKey;
|
|
86
|
+
return (a.hireMode || '').localeCompare(b.hireMode || '');
|
|
87
|
+
}
|
|
77
88
|
function buildLabelMatch(label) {
|
|
78
89
|
return { labels: { $regex: `^${escapeRegex(label)}$`, $options: 'i' } };
|
|
79
90
|
}
|
|
@@ -728,12 +739,21 @@ class FraimDbService {
|
|
|
728
739
|
if (activeOnly) {
|
|
729
740
|
query.status = 'active';
|
|
730
741
|
}
|
|
731
|
-
|
|
742
|
+
// Sort in memory, not in the query: Azure Cosmos DB (Mongo API) rejects an
|
|
743
|
+
// order-by on { personaKey, hireMode } with BadRequest 400 unless a matching
|
|
744
|
+
// composite index exists, and the initializeIndexes() attempt to create one
|
|
745
|
+
// is swallowed on Cosmos. A throw here bubbles up into getWorkspacePersonaState
|
|
746
|
+
// → the AI Hub silently falls back to ALL personas locked (no hired employees,
|
|
747
|
+
// empty manager team, empty project tree). Sorting client-side is index-free.
|
|
748
|
+
const rows = await this.personaEntitlementsCollection.find(query).toArray();
|
|
749
|
+
return rows.sort(comparePersonaEntitlements);
|
|
732
750
|
}
|
|
733
751
|
async listPersonaEntitlementsByStripeCustomerId(customerId) {
|
|
734
752
|
if (!this.personaEntitlementsCollection)
|
|
735
753
|
throw new Error('DB not connected');
|
|
736
|
-
|
|
754
|
+
// In-memory sort — see getPersonaEntitlementsByWorkspaceId (Cosmos order-by).
|
|
755
|
+
const rows = await this.personaEntitlementsCollection.find({ stripeCustomerId: customerId }).toArray();
|
|
756
|
+
return rows.sort((a, b) => (b.updatedAt?.getTime() ?? 0) - (a.updatedAt?.getTime() ?? 0));
|
|
737
757
|
}
|
|
738
758
|
async getPersonaEntitlementsByUserId(userId, activeOnly = true) {
|
|
739
759
|
if (!this.personaEntitlementsCollection)
|
|
@@ -742,7 +762,9 @@ class FraimDbService {
|
|
|
742
762
|
if (activeOnly) {
|
|
743
763
|
query.status = 'active';
|
|
744
764
|
}
|
|
745
|
-
|
|
765
|
+
// In-memory sort — see getPersonaEntitlementsByWorkspaceId (Cosmos order-by).
|
|
766
|
+
const rows = await this.personaEntitlementsCollection.find(query).toArray();
|
|
767
|
+
return rows.sort(comparePersonaEntitlements);
|
|
746
768
|
}
|
|
747
769
|
async upsertPersonaEntitlement(entitlement) {
|
|
748
770
|
if (!this.personaEntitlementsCollection)
|
|
@@ -4,6 +4,16 @@ exports.registerOAuthRoutes = registerOAuthRoutes;
|
|
|
4
4
|
const cookie_service_1 = require("../services/cookie-service");
|
|
5
5
|
const oauth_helpers_1 = require("../services/oauth-helpers");
|
|
6
6
|
const audit_log_1 = require("../services/audit-log");
|
|
7
|
+
const account_provisioning_1 = require("../services/account-provisioning");
|
|
8
|
+
const rate_limit_1 = require("../middleware/rate-limit");
|
|
9
|
+
function pickIntent(value) {
|
|
10
|
+
return value === 'signup' ? 'signup' : 'signin';
|
|
11
|
+
}
|
|
12
|
+
// Issue #691: intent=signup makes /start reachable as an account-creation path
|
|
13
|
+
// (see completeSignIn below), the same sensitive business flow /api/request-access
|
|
14
|
+
// already rate-limits. Apply the same limiter here, scoped to signup attempts only —
|
|
15
|
+
// the pre-existing sign-in path (no intent) is left at its original, unlimited behavior.
|
|
16
|
+
const signupRateLimiter = (0, rate_limit_1.sensitiveRateLimiter)();
|
|
7
17
|
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
8
18
|
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
9
19
|
const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize';
|
|
@@ -75,13 +85,17 @@ function registerOAuthRoutes(app, deps) {
|
|
|
75
85
|
NODE_ENV: process.env.NODE_ENV || '(not set)',
|
|
76
86
|
});
|
|
77
87
|
// Helper used by both providers to finalise authentication once we have a verified email.
|
|
78
|
-
async function completeSignIn(req, res,
|
|
88
|
+
async function completeSignIn(req, res, params) {
|
|
89
|
+
const { verifiedEmail, provider, surface, redirectTo, intent } = params;
|
|
79
90
|
const lower = verifiedEmail.toLowerCase();
|
|
80
91
|
const apiKeyData = await dbService.getApiKeyByUserId(lower, false);
|
|
81
|
-
if (!apiKeyData) {
|
|
92
|
+
if (!apiKeyData && intent !== 'signup') {
|
|
82
93
|
// Sign-in is NOT a sign-up. If the verified email doesn't already match a
|
|
83
94
|
// FRAIM account, route the user to the sign-up flow. Account creation is
|
|
84
95
|
// owned by /api/request-access, not by login. (Round-2 user feedback.)
|
|
96
|
+
// Issue #691: this only applies to the sign-in surface (intent !== 'signup').
|
|
97
|
+
// The dedicated signup surface uses intent='signup' to provision an account
|
|
98
|
+
// below instead of bouncing to no_account.
|
|
85
99
|
await (0, audit_log_1.auditLog)('OAUTH_CALLBACK_SUCCESS', {
|
|
86
100
|
userId: lower,
|
|
87
101
|
provider,
|
|
@@ -92,6 +106,9 @@ function registerOAuthRoutes(app, deps) {
|
|
|
92
106
|
(0, cookie_service_1.clearOAuthPendingCookie)(res);
|
|
93
107
|
return res.redirect(`/auth/error.html?reason=no_account`);
|
|
94
108
|
}
|
|
109
|
+
if (!apiKeyData && intent === 'signup') {
|
|
110
|
+
await (0, account_provisioning_1.ensureTrialApiKey)(dbService, lower);
|
|
111
|
+
}
|
|
95
112
|
const sessionId = (0, oauth_helpers_1.generateSessionId)();
|
|
96
113
|
await dbService.createAuthSession({
|
|
97
114
|
sessionId,
|
|
@@ -106,13 +123,18 @@ function registerOAuthRoutes(app, deps) {
|
|
|
106
123
|
(0, cookie_service_1.clearOAuthPendingCookie)(res);
|
|
107
124
|
res.redirect(redirectTo);
|
|
108
125
|
}
|
|
109
|
-
app.get('/api/auth/oauth/:provider/start',
|
|
126
|
+
app.get('/api/auth/oauth/:provider/start', (req, res, next) => {
|
|
127
|
+
if (pickIntent(req.query.intent) === 'signup')
|
|
128
|
+
return signupRateLimiter(req, res, next);
|
|
129
|
+
next();
|
|
130
|
+
}, async (req, res) => {
|
|
110
131
|
const providerRaw = req.params.provider;
|
|
111
132
|
const provider = typeof providerRaw === 'string' ? providerRaw : '';
|
|
112
133
|
if (!(0, oauth_helpers_1.isSupportedProvider)(provider)) {
|
|
113
134
|
return res.status(404).json({ error: 'unsupported_provider' });
|
|
114
135
|
}
|
|
115
136
|
const surface = (0, oauth_helpers_1.pickSurfaceOrDefault)(req.query.surface);
|
|
137
|
+
const intent = pickIntent(req.query.intent);
|
|
116
138
|
const redirectTo = (0, oauth_helpers_1.safeRedirectPath)(req.query.redirect_to, (0, oauth_helpers_1.defaultRedirectForSurface)(surface));
|
|
117
139
|
try {
|
|
118
140
|
const codeVerifier = (0, oauth_helpers_1.generatePkceVerifier)();
|
|
@@ -122,6 +144,7 @@ function registerOAuthRoutes(app, deps) {
|
|
|
122
144
|
pendingId,
|
|
123
145
|
provider,
|
|
124
146
|
surface,
|
|
147
|
+
intent,
|
|
125
148
|
codeVerifier,
|
|
126
149
|
stateNonce,
|
|
127
150
|
redirectTo,
|
|
@@ -228,7 +251,13 @@ function registerOAuthRoutes(app, deps) {
|
|
|
228
251
|
await (0, audit_log_1.auditLog)('OAUTH_UNVERIFIED_EMAIL', { provider: 'google', outcome: 'failure' });
|
|
229
252
|
return res.redirect('/auth/error.html?reason=unverified_email');
|
|
230
253
|
}
|
|
231
|
-
await completeSignIn(req, res,
|
|
254
|
+
await completeSignIn(req, res, {
|
|
255
|
+
verifiedEmail: payload.email,
|
|
256
|
+
provider: 'google',
|
|
257
|
+
surface: pending.surface,
|
|
258
|
+
redirectTo: pending.redirectTo,
|
|
259
|
+
intent: pickIntent(pending.intent),
|
|
260
|
+
});
|
|
232
261
|
}
|
|
233
262
|
catch (err) {
|
|
234
263
|
console.error('[FRAIM AUTH] /api/auth/oauth/google/callback error:', err instanceof Error ? err.message : String(err));
|
|
@@ -315,7 +344,13 @@ function registerOAuthRoutes(app, deps) {
|
|
|
315
344
|
await (0, audit_log_1.auditLog)('OAUTH_UNVERIFIED_EMAIL', { provider: 'github', outcome: 'failure' });
|
|
316
345
|
return res.redirect('/auth/error.html?reason=unverified_email');
|
|
317
346
|
}
|
|
318
|
-
await completeSignIn(req, res,
|
|
347
|
+
await completeSignIn(req, res, {
|
|
348
|
+
verifiedEmail,
|
|
349
|
+
provider: 'github',
|
|
350
|
+
surface: pending.surface,
|
|
351
|
+
redirectTo: pending.redirectTo,
|
|
352
|
+
intent: pickIntent(pending.intent),
|
|
353
|
+
});
|
|
319
354
|
}
|
|
320
355
|
catch (err) {
|
|
321
356
|
console.error('[FRAIM AUTH] /api/auth/oauth/github/callback error:', err instanceof Error ? err.message : String(err));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureTrialApiKey = ensureTrialApiKey;
|
|
4
|
+
const feature_flags_1 = require("../config/feature-flags");
|
|
5
|
+
const TRIAL_DURATION_MS = 14 * 24 * 60 * 60 * 1000;
|
|
6
|
+
/**
|
|
7
|
+
* Provision a 14-day trial FraimApiKey for `emailLower` if one does not already
|
|
8
|
+
* exist; otherwise return the existing key. Shared by the email+code signup
|
|
9
|
+
* flow (`AdminService.verifyRequestAccessCode`) and the OAuth signup-intent
|
|
10
|
+
* flow (`oauth-routes.ts`) so account creation has exactly one implementation.
|
|
11
|
+
* Issue #691.
|
|
12
|
+
*/
|
|
13
|
+
async function ensureTrialApiKey(dbService, emailLower) {
|
|
14
|
+
const existingKey = await dbService.getApiKeyByUserId(emailLower, false);
|
|
15
|
+
if (existingKey)
|
|
16
|
+
return existingKey.key;
|
|
17
|
+
const orgId = 'default';
|
|
18
|
+
const apiKey = dbService.generateApiKey(emailLower, orgId);
|
|
19
|
+
const trialExpiresAt = new Date(Date.now() + TRIAL_DURATION_MS);
|
|
20
|
+
await dbService.createApiKey({
|
|
21
|
+
key: apiKey,
|
|
22
|
+
userId: emailLower,
|
|
23
|
+
orgId,
|
|
24
|
+
tier: 'trial',
|
|
25
|
+
status: 'active',
|
|
26
|
+
expiresAt: trialExpiresAt,
|
|
27
|
+
stripeCustomerId: null,
|
|
28
|
+
stripeSubscriptionId: null,
|
|
29
|
+
currentPeriodEnd: null,
|
|
30
|
+
cancelAt: null,
|
|
31
|
+
suspendedAt: null,
|
|
32
|
+
suspensionReason: null,
|
|
33
|
+
lastUsedAt: null,
|
|
34
|
+
apiCallCount: 0,
|
|
35
|
+
personaSystemActive: (0, feature_flags_1.isPersonaEntitlementsEnabled)() || undefined
|
|
36
|
+
});
|
|
37
|
+
return apiKey;
|
|
38
|
+
}
|
|
@@ -7,6 +7,7 @@ const email_service_1 = require("./email-service");
|
|
|
7
7
|
const dashboard_access_1 = require("./dashboard-access");
|
|
8
8
|
const feature_flags_1 = require("../config/feature-flags");
|
|
9
9
|
const email_code_1 = require("./email-code");
|
|
10
|
+
const account_provisioning_1 = require("./account-provisioning");
|
|
10
11
|
class AdminService {
|
|
11
12
|
constructor(dbService) {
|
|
12
13
|
this.dbService = dbService;
|
|
@@ -130,8 +131,11 @@ class AdminService {
|
|
|
130
131
|
*/
|
|
131
132
|
async requestAccess(body, req) {
|
|
132
133
|
const { email, name, company, useCase } = body;
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
// Issue #691: signup is now email + verification code only. Name/company
|
|
135
|
+
// are optional lead-capture fields (createWebsiteSignup below already
|
|
136
|
+
// tolerates missing values); the trial key itself never required them.
|
|
137
|
+
if (!email) {
|
|
138
|
+
throw new Error('Email is required');
|
|
135
139
|
}
|
|
136
140
|
if (!(0, request_utils_1.validateEmail)(email))
|
|
137
141
|
throw new Error('Invalid email address');
|
|
@@ -140,8 +144,8 @@ class AdminService {
|
|
|
140
144
|
const { ip: ipAddress, userAgent } = (0, request_utils_1.getRequestMeta)(req);
|
|
141
145
|
await this.dbService.createWebsiteSignup({
|
|
142
146
|
email: emailLower,
|
|
143
|
-
name: name
|
|
144
|
-
company: company
|
|
147
|
+
name: name?.trim() || '',
|
|
148
|
+
company: company?.trim() || '',
|
|
145
149
|
useCase: useCase?.trim(),
|
|
146
150
|
source: 'request-access',
|
|
147
151
|
timestamp: new Date(),
|
|
@@ -184,38 +188,8 @@ class AdminService {
|
|
|
184
188
|
if (!verification) {
|
|
185
189
|
return { success: false, error: 'Invalid or expired verification code' };
|
|
186
190
|
}
|
|
187
|
-
const
|
|
188
|
-
|
|
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
|
-
}
|
|
191
|
+
const apiKey = await (0, account_provisioning_1.ensureTrialApiKey)(this.dbService, emailLower);
|
|
192
|
+
console.log('[FRAIM] verify_email_code key_resolved', { userId: emailLower });
|
|
219
193
|
const access = await (0, dashboard_access_1.createDashboardAccessLink)(this.dbService, emailLower, apiKey);
|
|
220
194
|
console.log('[FRAIM] verify_email_code verification_completed', { userId: emailLower });
|
|
221
195
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.185",
|
|
4
4
|
"description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|