fraim-hub 2.0.204
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +467 -0
- package/bin/fraim-hub.js +17 -0
- package/dist/src/ai-hub/brand-store.js +17 -0
- package/dist/src/ai-hub/catalog.js +412 -0
- package/dist/src/ai-hub/cert-store.js +70 -0
- package/dist/src/ai-hub/cli.js +212 -0
- package/dist/src/ai-hub/configured-agents.js +283 -0
- package/dist/src/ai-hub/conversation-store.js +428 -0
- package/dist/src/ai-hub/desktop-main.js +449 -0
- package/dist/src/ai-hub/hosts.js +1669 -0
- package/dist/src/ai-hub/hub-latest-version.js +52 -0
- package/dist/src/ai-hub/hub-launch-decision.js +57 -0
- package/dist/src/ai-hub/hub-runtime-file.js +43 -0
- package/dist/src/ai-hub/managed-browser.js +269 -0
- package/dist/src/ai-hub/manager-turns.js +64 -0
- package/dist/src/ai-hub/office-sideload.js +156 -0
- package/dist/src/ai-hub/openclaw-bridge.js +250 -0
- package/dist/src/ai-hub/preferences.js +201 -0
- package/dist/src/ai-hub/remote-hub-gateway.js +88 -0
- package/dist/src/ai-hub/server.js +4013 -0
- package/dist/src/ai-hub/types.js +2 -0
- package/dist/src/ai-hub/url-safety.js +19 -0
- 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/sales/contact.js +44 -0
- package/dist/src/cli/api/get-provider-client.js +41 -0
- package/dist/src/cli/api/provider-client.js +107 -0
- package/dist/src/cli/commands/add-ide.js +462 -0
- package/dist/src/cli/fraim-hub.js +50 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/command-resolution.js +81 -0
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +136 -0
- package/dist/src/cli/mcp/ide-formats.js +317 -0
- package/dist/src/cli/mcp/mcp-server-builder.js +48 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +173 -0
- package/dist/src/cli/providers/local-provider-registry.js +165 -0
- package/dist/src/cli/providers/provider-registry.js +230 -0
- package/dist/src/cli/setup/claude-code-telemetry.js +59 -0
- package/dist/src/cli/setup/codex-local-config.js +37 -0
- package/dist/src/cli/setup/ide-detector.js +383 -0
- package/dist/src/cli/setup/ide-global-integration.js +100 -0
- package/dist/src/cli/setup/ide-invocation-surfaces.js +160 -0
- package/dist/src/cli/setup/mcp-config-generator.js +225 -0
- package/dist/src/cli/setup/provider-prompts.js +339 -0
- package/dist/src/cli/utils/managed-agent-paths.js +118 -0
- package/dist/src/cli/utils/script-sync-utils.js +221 -0
- package/dist/src/cli/utils/user-config.js +100 -0
- package/dist/src/cli/utils/version-utils.js +35 -0
- package/dist/src/config/ai-manager-hiring.js +121 -0
- package/dist/src/config/feature-flags.js +25 -0
- package/dist/src/config/persona-capability-bundles.js +277 -0
- package/dist/src/config/persona-hiring.js +270 -0
- package/dist/src/config/portfolio-slug-overrides.js +17 -0
- package/dist/src/config/pricing.js +37 -0
- package/dist/src/config/stripe.js +43 -0
- package/dist/src/core/brand-store.js +232 -0
- package/dist/src/core/quality-evidence.js +331 -0
- package/dist/src/core/utils/git-utils.js +194 -0
- package/dist/src/core/utils/ports.js +32 -0
- package/dist/src/core/utils/project-fraim-paths.js +220 -0
- package/dist/src/db/payment-repository.js +61 -0
- package/dist/src/first-run/types.js +93 -0
- package/dist/src/fraim/db-service.js +2409 -0
- package/dist/src/local-mcp-server/agent-token-prices.js +167 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +975 -0
- package/dist/src/middleware/rate-limit.js +110 -0
- package/dist/src/models/payment.js +2 -0
- package/dist/src/routes/payment-routes.js +186 -0
- package/dist/src/routes/persona-catalog-routes.js +84 -0
- package/dist/src/services/dashboard-access.js +27 -0
- package/dist/src/services/email-service.js +951 -0
- package/dist/src/services/persona-entitlement-service.js +352 -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/index.js +85 -0
- package/package.json +106 -0
- package/public/ai-hub/index.html +910 -0
- package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
- package/public/ai-hub/powerpoint-taskpane/index.html +236 -0
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +30 -0
- package/public/ai-hub/review.css +444 -0
- package/public/ai-hub/script.js +11737 -0
- package/public/ai-hub/styles.css +5102 -0
- package/public/first-run/error-frame.js +100 -0
- package/public/first-run/index.html +35 -0
- package/public/first-run/script.js +739 -0
- package/public/first-run/styles.css +929 -0
- package/public/portfolio/ashley.html +523 -0
- package/public/portfolio/auditya.html +83 -0
- package/public/portfolio/banke.html +83 -0
- package/public/portfolio/beza.html +659 -0
- package/public/portfolio/careena.html +632 -0
- package/public/portfolio/casey.html +568 -0
- package/public/portfolio/celia.html +490 -0
- package/public/portfolio/deidre.html +642 -0
- package/public/portfolio/gautam.html +597 -0
- package/public/portfolio/hari.html +469 -0
- package/public/portfolio/huxley.html +1354 -0
- package/public/portfolio/index.html +741 -0
- package/public/portfolio/maestro.html +518 -0
- package/public/portfolio/mandy.html +590 -0
- package/public/portfolio/mona.html +597 -0
- package/public/portfolio/pam.html +887 -0
- package/public/portfolio/procella.html +107 -0
- package/public/portfolio/qasm.html +569 -0
- package/public/portfolio/ricardo.html +489 -0
- package/public/portfolio/sade.html +560 -0
- package/public/portfolio/sam.html +654 -0
- package/public/portfolio/sechar.html +580 -0
- package/public/portfolio/sreya.html +599 -0
- package/public/portfolio/swen.html +601 -0
|
@@ -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,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerPaymentRoutes = registerPaymentRoutes;
|
|
4
|
+
exports.registerAdminPaymentRoutes = registerAdminPaymentRoutes;
|
|
5
|
+
const create_session_1 = require("../api/payment/create-session");
|
|
6
|
+
const session_details_1 = require("../api/payment/session-details");
|
|
7
|
+
const dashboard_link_1 = require("../api/payment/dashboard-link");
|
|
8
|
+
const webhook_1 = require("../api/payment/webhook");
|
|
9
|
+
const contact_1 = require("../api/sales/contact");
|
|
10
|
+
const payments_1 = require("../api/admin/payments");
|
|
11
|
+
const sales_leads_1 = require("../api/admin/sales-leads");
|
|
12
|
+
const get_config_1 = require("../api/pricing/get-config");
|
|
13
|
+
const rate_limit_1 = require("../middleware/rate-limit");
|
|
14
|
+
/**
|
|
15
|
+
* Register all payment-related routes
|
|
16
|
+
* These routes must be registered BEFORE auth middleware
|
|
17
|
+
*/
|
|
18
|
+
function registerPaymentRoutes(app, getPaymentRepo, dbService) {
|
|
19
|
+
// Per-route rate limiters. Webhooks are intentionally NOT rate-limited at
|
|
20
|
+
// the application layer because Stripe controls delivery and retry semantics.
|
|
21
|
+
const publicLimiter = (0, rate_limit_1.publicApiRateLimiter)();
|
|
22
|
+
const sensitiveLimiter = (0, rate_limit_1.sensitiveRateLimiter)();
|
|
23
|
+
// Pricing config endpoint (public, no auth required)
|
|
24
|
+
app.get('/api/pricing/config', publicLimiter, async (req, res) => {
|
|
25
|
+
await (0, get_config_1.getPricingConfig)(req, res);
|
|
26
|
+
});
|
|
27
|
+
// Payment API endpoints (public, no auth required)
|
|
28
|
+
app.post('/api/payment/create-session', sensitiveLimiter, async (req, res) => {
|
|
29
|
+
const paymentRepo = getPaymentRepo();
|
|
30
|
+
if (!paymentRepo) {
|
|
31
|
+
return res.status(500).json({ error: 'Payment system not initialized' });
|
|
32
|
+
}
|
|
33
|
+
await (0, create_session_1.createCheckoutSession)(req, res, paymentRepo, dbService);
|
|
34
|
+
});
|
|
35
|
+
// Payment success page — fetch Stripe session details by session_id
|
|
36
|
+
app.get('/api/payment/session-details', publicLimiter, async (req, res) => {
|
|
37
|
+
await (0, session_details_1.getSessionDetails)(req, res);
|
|
38
|
+
});
|
|
39
|
+
// Checkout success page: provide immediate dashboard URL (no email wait)
|
|
40
|
+
app.get('/api/payment/dashboard-link', publicLimiter, async (req, res) => {
|
|
41
|
+
await (0, dashboard_link_1.getDashboardLinkFromCheckoutSession)(req, res, dbService);
|
|
42
|
+
});
|
|
43
|
+
// Stripe webhook endpoint (raw body required for signature verification)
|
|
44
|
+
// Intentionally NOT rate-limited; Stripe handles its own delivery semantics.
|
|
45
|
+
app.post('/api/payment/webhook', async (req, res) => {
|
|
46
|
+
const paymentRepo = getPaymentRepo();
|
|
47
|
+
if (!paymentRepo) {
|
|
48
|
+
return res.status(500).json({ error: 'Payment system not initialized' });
|
|
49
|
+
}
|
|
50
|
+
await (0, webhook_1.handleStripeWebhook)(req, res, paymentRepo, dbService);
|
|
51
|
+
});
|
|
52
|
+
// Sales inquiry endpoint (public, no auth required)
|
|
53
|
+
app.post('/api/sales/contact', sensitiveLimiter, async (req, res) => {
|
|
54
|
+
await (0, contact_1.createSalesLead)(req, res, dbService);
|
|
55
|
+
});
|
|
56
|
+
// Payment bypass endpoint for non-production testing only.
|
|
57
|
+
// Two layers of defence:
|
|
58
|
+
// 1. Route returns 404 entirely when NODE_ENV === 'production'.
|
|
59
|
+
// 2. PAYMENT_BYPASS_CODE must be explicitly set in the environment; there is no
|
|
60
|
+
// hardcoded fallback. If it is unset, the route returns 503.
|
|
61
|
+
app.post('/api/payment/bypass', sensitiveLimiter, async (req, res) => {
|
|
62
|
+
if (process.env.NODE_ENV === 'production') {
|
|
63
|
+
return res.status(404).json({ error: 'Not found' });
|
|
64
|
+
}
|
|
65
|
+
const BYPASS_CODE = process.env.PAYMENT_BYPASS_CODE;
|
|
66
|
+
if (!BYPASS_CODE) {
|
|
67
|
+
console.error('[FRAIM] /api/payment/bypass invoked but PAYMENT_BYPASS_CODE is not configured');
|
|
68
|
+
return res.status(503).json({ error: 'Payment bypass is not configured on this server' });
|
|
69
|
+
}
|
|
70
|
+
const { code, apiKey, daysUntilExpiry } = req.body;
|
|
71
|
+
if (code !== BYPASS_CODE) {
|
|
72
|
+
return res.status(403).json({ error: 'Invalid bypass code' });
|
|
73
|
+
}
|
|
74
|
+
if (!apiKey) {
|
|
75
|
+
return res.status(400).json({ error: 'apiKey is required' });
|
|
76
|
+
}
|
|
77
|
+
const existingKey = await dbService.getApiKeyByKey(apiKey);
|
|
78
|
+
if (!existingKey) {
|
|
79
|
+
return res.status(404).json({ error: 'API key not found' });
|
|
80
|
+
}
|
|
81
|
+
// Cap at 365 days. Defense in depth: even with the bypass code, do not let a
|
|
82
|
+
// request grant a multi-year subscription to an account.
|
|
83
|
+
const MAX_BYPASS_DAYS = 365;
|
|
84
|
+
const requestedDays = parseInt(daysUntilExpiry) || 30;
|
|
85
|
+
const days = Math.max(1, Math.min(requestedDays, MAX_BYPASS_DAYS));
|
|
86
|
+
const newExpiry = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
|
|
87
|
+
await dbService.updateApiKey(apiKey, {
|
|
88
|
+
tier: 'paid-subscription',
|
|
89
|
+
status: 'active',
|
|
90
|
+
expiresAt: newExpiry,
|
|
91
|
+
currentPeriodEnd: newExpiry,
|
|
92
|
+
stripeCustomerId: 'bypass_test_customer',
|
|
93
|
+
stripeSubscriptionId: 'bypass_test_subscription'
|
|
94
|
+
});
|
|
95
|
+
console.log(`[FRAIM] payment_bypass activated`, { userId: existingKey.userId, expiresAt: newExpiry.toISOString() });
|
|
96
|
+
return res.json({
|
|
97
|
+
success: true,
|
|
98
|
+
message: `Key upgraded to paid-subscription, expires in ${days} days`,
|
|
99
|
+
expiresAt: newExpiry.toISOString(),
|
|
100
|
+
tier: 'paid-subscription'
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Register admin payment routes
|
|
106
|
+
* These routes require auth middleware to be applied first
|
|
107
|
+
*/
|
|
108
|
+
function registerAdminPaymentRoutes(app, getPaymentRepo, dbService) {
|
|
109
|
+
// Admin endpoints (require auth)
|
|
110
|
+
app.get('/admin/payments', async (req, res) => {
|
|
111
|
+
const paymentRepo = getPaymentRepo();
|
|
112
|
+
if (!paymentRepo) {
|
|
113
|
+
return res.status(500).json({ error: 'Payment system not initialized' });
|
|
114
|
+
}
|
|
115
|
+
await (0, payments_1.listPayments)(req, res, paymentRepo);
|
|
116
|
+
});
|
|
117
|
+
app.get('/admin/sales-leads', async (req, res) => {
|
|
118
|
+
await (0, sales_leads_1.listSalesLeads)(req, res, dbService);
|
|
119
|
+
});
|
|
120
|
+
// ── Partner Discount CRUD ─────────────────────────────────────────────────
|
|
121
|
+
/** List all partner discount entries */
|
|
122
|
+
app.get('/admin/partner-discounts', async (req, res) => {
|
|
123
|
+
try {
|
|
124
|
+
const entries = await dbService.listPartnerDiscounts();
|
|
125
|
+
res.json(entries);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
res.status(500).json({ error: 'Failed to list partner discounts', details: error.message });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
/** Create a new partner discount entry */
|
|
132
|
+
app.post('/admin/partner-discounts', async (req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
const { type, match, couponId, company } = req.body;
|
|
135
|
+
if (!type || !['email', 'domain'].includes(type)) {
|
|
136
|
+
return res.status(400).json({ error: 'type must be "email" or "domain"' });
|
|
137
|
+
}
|
|
138
|
+
if (!match || typeof match !== 'string' || !match.trim()) {
|
|
139
|
+
return res.status(400).json({ error: 'match is required (email address or domain)' });
|
|
140
|
+
}
|
|
141
|
+
if (!couponId || typeof couponId !== 'string' || !couponId.trim()) {
|
|
142
|
+
return res.status(400).json({ error: 'couponId is required (Stripe coupon ID)' });
|
|
143
|
+
}
|
|
144
|
+
const entry = await dbService.createPartnerDiscount({
|
|
145
|
+
type,
|
|
146
|
+
match: match.trim().toLowerCase(),
|
|
147
|
+
couponId: couponId.trim(),
|
|
148
|
+
company: company?.trim() || undefined,
|
|
149
|
+
active: true,
|
|
150
|
+
createdAt: new Date(),
|
|
151
|
+
useCount: 0,
|
|
152
|
+
});
|
|
153
|
+
res.status(201).json(entry);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error.code === 11000) {
|
|
157
|
+
return res.status(409).json({ error: 'A discount entry for that type+match already exists' });
|
|
158
|
+
}
|
|
159
|
+
res.status(500).json({ error: 'Failed to create partner discount', details: error.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
/** Update an existing partner discount entry (active, couponId, company) */
|
|
163
|
+
app.patch('/admin/partner-discounts/:id', async (req, res) => {
|
|
164
|
+
try {
|
|
165
|
+
const id = req.params.id;
|
|
166
|
+
const { active, couponId, company } = req.body;
|
|
167
|
+
const update = {};
|
|
168
|
+
if (typeof active === 'boolean')
|
|
169
|
+
update.active = active;
|
|
170
|
+
if (typeof couponId === 'string' && couponId.trim())
|
|
171
|
+
update.couponId = couponId.trim();
|
|
172
|
+
if (typeof company === 'string')
|
|
173
|
+
update.company = company.trim() || undefined;
|
|
174
|
+
if (Object.keys(update).length === 0) {
|
|
175
|
+
return res.status(400).json({ error: 'No valid fields to update (active, couponId, company)' });
|
|
176
|
+
}
|
|
177
|
+
const updated = await dbService.updatePartnerDiscount(id, update);
|
|
178
|
+
if (!updated)
|
|
179
|
+
return res.status(404).json({ error: 'Partner discount not found' });
|
|
180
|
+
res.json({ success: true });
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
res.status(500).json({ error: 'Failed to update partner discount', details: error.message });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GET /api/personas/catalog
|
|
4
|
+
*
|
|
5
|
+
* Public, read-only endpoint that returns the full persona catalog for
|
|
6
|
+
* fraim-pro/index.html and fraim-pro/pricing.html to hydrate their grids.
|
|
7
|
+
*
|
|
8
|
+
* Auth: none — added to publicPrefixes in src/middleware/auth.ts.
|
|
9
|
+
*
|
|
10
|
+
* Response shape: PersonaCatalogResponse (see types below).
|
|
11
|
+
*
|
|
12
|
+
* Issue #545: centralizes emoji, gradient, blurb, and price display in
|
|
13
|
+
* PERSONA_HIRE_CATALOG so the HTML surfaces are derived views, not sources
|
|
14
|
+
* of truth.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.formatPrice = formatPrice;
|
|
18
|
+
exports.buildPersonaCatalog = buildPersonaCatalog;
|
|
19
|
+
exports.registerPersonaCatalogRoutes = registerPersonaCatalogRoutes;
|
|
20
|
+
const fs_1 = require("fs");
|
|
21
|
+
const path_1 = require("path");
|
|
22
|
+
const persona_hiring_1 = require("../config/persona-hiring");
|
|
23
|
+
const persona_capability_bundles_1 = require("../config/persona-capability-bundles");
|
|
24
|
+
const portfolio_slug_overrides_1 = require("../config/portfolio-slug-overrides");
|
|
25
|
+
/**
|
|
26
|
+
* Format a cents integer to a dollar string, dropping trailing .00.
|
|
27
|
+
*
|
|
28
|
+
* Examples:
|
|
29
|
+
* 990 -> "$9.90"
|
|
30
|
+
* 4990 -> "$49.90"
|
|
31
|
+
* 24900 -> "$249"
|
|
32
|
+
* 100 -> "$1"
|
|
33
|
+
*
|
|
34
|
+
* Exported so persona-capability-bundles.ts can reuse the same function.
|
|
35
|
+
*/
|
|
36
|
+
function formatPrice(cents) {
|
|
37
|
+
return `$${(cents / 100).toFixed(2).replace(/\.00$/, '')}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the catalog array from PERSONA_HIRE_CATALOG + PERSONA_CAPABILITY_BUNDLES.
|
|
41
|
+
*
|
|
42
|
+
* Exported for unit testing without requiring an HTTP context.
|
|
43
|
+
*/
|
|
44
|
+
function buildPersonaCatalog() {
|
|
45
|
+
const ROOT = process.cwd();
|
|
46
|
+
return Object.entries(persona_hiring_1.PERSONA_HIRE_CATALOG).map(([key, persona]) => {
|
|
47
|
+
const hireKey = key;
|
|
48
|
+
const slug = portfolio_slug_overrides_1.PORTFOLIO_SLUG_OVERRIDES[key] ?? key;
|
|
49
|
+
const hasPortfolioPage = (0, fs_1.existsSync)((0, path_1.join)(ROOT, 'public', 'portfolio', slug + '.html'));
|
|
50
|
+
const bundle = persona_capability_bundles_1.PERSONA_CAPABILITY_BUNDLES[hireKey];
|
|
51
|
+
const sampleJobs = bundle?.catalogMetadata?.sampleJobs ?? [];
|
|
52
|
+
return {
|
|
53
|
+
key,
|
|
54
|
+
displayName: persona.displayName,
|
|
55
|
+
role: persona.role,
|
|
56
|
+
emoji: persona.emoji,
|
|
57
|
+
gradient: persona.gradient,
|
|
58
|
+
blurb: persona.blurb,
|
|
59
|
+
avatarUrl: (0, persona_hiring_1.buildPersonaAvatarUrl)(hireKey),
|
|
60
|
+
avatarBackgroundColor: persona_hiring_1.PERSONA_AVATAR_CATALOG[hireKey].bg,
|
|
61
|
+
jobPriceCents: persona.jobPriceCents,
|
|
62
|
+
fulltimePriceCents: persona.fulltimePriceCents,
|
|
63
|
+
portfolioSlug: slug,
|
|
64
|
+
hasPortfolioPage,
|
|
65
|
+
sampleJobs,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Register the persona catalog route.
|
|
71
|
+
* Must be called BEFORE auth middleware (it is public — no key required).
|
|
72
|
+
*/
|
|
73
|
+
function registerPersonaCatalogRoutes(app) {
|
|
74
|
+
app.get('/api/personas/catalog', (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
const personas = buildPersonaCatalog();
|
|
77
|
+
res.json({ personas });
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.error('[persona-catalog] Error building catalog:', err);
|
|
81
|
+
res.status(500).json({ error: 'Failed to build persona catalog' });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -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
|
+
}
|