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,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeSubscriptionActive = computeSubscriptionActive;
|
|
4
|
+
exports.resolvePersonaAccessStatuses = resolvePersonaAccessStatuses;
|
|
5
|
+
exports.buildUngatedPersonaWorkspaceState = buildUngatedPersonaWorkspaceState;
|
|
6
|
+
exports.buildPersonaHireUrl = buildPersonaHireUrl;
|
|
7
|
+
exports.syncPersonaEntitlementPurchase = syncPersonaEntitlementPurchase;
|
|
8
|
+
exports.syncPersonaEntitlementFromCheckoutSession = syncPersonaEntitlementFromCheckoutSession;
|
|
9
|
+
exports.getWorkspacePersonaState = getWorkspacePersonaState;
|
|
10
|
+
exports.evaluatePersonaAccess = evaluatePersonaAccess;
|
|
11
|
+
const persona_hiring_1 = require("../config/persona-hiring");
|
|
12
|
+
const persona_capability_bundles_1 = require("../config/persona-capability-bundles");
|
|
13
|
+
const workspace_identity_1 = require("./workspace-identity");
|
|
14
|
+
const DEFAULT_JOB_CREDITS = 1;
|
|
15
|
+
// The one place the "is the persona system gating this workspace?" question is
|
|
16
|
+
// answered. A workspace is subscribed if it explicitly opted in (personaSystemActive)
|
|
17
|
+
// or has ever held an active entitlement. Shared by getWorkspacePersonaState and
|
|
18
|
+
// evaluatePersonaAccess so display and enforcement can never diverge.
|
|
19
|
+
function computeSubscriptionActive(apiKeyData, entitlements) {
|
|
20
|
+
return apiKeyData?.personaSystemActive === true || entitlements.some((e) => e.status === 'active');
|
|
21
|
+
}
|
|
22
|
+
// Single source of truth for per-persona access, mirroring evaluatePersonaAccess's
|
|
23
|
+
// gating rules exactly:
|
|
24
|
+
// - `ungated` (feature flag OFF, or legacy/never-subscribed workspace) => every
|
|
25
|
+
// persona is accessible ('hired').
|
|
26
|
+
// - otherwise => 'hired' iff the workspace holds an active entitlement for it.
|
|
27
|
+
// Pure and DB-free so it is trivially testable and callable from either code path.
|
|
28
|
+
function resolvePersonaAccessStatuses(ungated, entitlements) {
|
|
29
|
+
const activeByKey = new Map();
|
|
30
|
+
for (const entitlement of entitlements) {
|
|
31
|
+
if (entitlement.status !== 'active')
|
|
32
|
+
continue;
|
|
33
|
+
const list = activeByKey.get(entitlement.personaKey) || [];
|
|
34
|
+
list.push(entitlement);
|
|
35
|
+
activeByKey.set(entitlement.personaKey, list);
|
|
36
|
+
}
|
|
37
|
+
return (0, persona_capability_bundles_1.listPersonaCapabilityBundles)().map((bundle) => {
|
|
38
|
+
const active = activeByKey.get(bundle.personaKey) || [];
|
|
39
|
+
const seatCount = active.reduce((sum, e) => sum + (e.jobCreditsRemaining ?? 1), 0);
|
|
40
|
+
return {
|
|
41
|
+
personaKey: bundle.personaKey,
|
|
42
|
+
personaName: bundle.catalogMetadata.displayName,
|
|
43
|
+
personaRole: bundle.catalogMetadata.role,
|
|
44
|
+
hireUrl: buildPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
|
|
45
|
+
status: (ungated || active.length > 0) ? 'hired' : 'locked',
|
|
46
|
+
seatCount,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// The persona-entitlements feature is OFF: there is no gating, so every persona is
|
|
51
|
+
// accessible. Returned by /api/personas/me instead of a 404 so downstream consumers
|
|
52
|
+
// (the Hub especially) inherit "flag off => all employees available" rather than
|
|
53
|
+
// inventing their own fallback. Matches maybeBuildPersonaLockResponse, which also
|
|
54
|
+
// short-circuits to "allowed" when the flag is off. No DB lookup is required.
|
|
55
|
+
function buildUngatedPersonaWorkspaceState(userId, apiKey) {
|
|
56
|
+
return {
|
|
57
|
+
featureFlagEnabled: false,
|
|
58
|
+
featureFlags: { personaEntitlementsEnabled: false },
|
|
59
|
+
subscriptionActive: false,
|
|
60
|
+
workspaceId: normalizeEmail(userId),
|
|
61
|
+
userId: normalizeEmail(userId),
|
|
62
|
+
apiKey: apiKey || null,
|
|
63
|
+
entitlements: [],
|
|
64
|
+
catalog: (0, persona_capability_bundles_1.listPersonaCapabilityBundles)().map((bundle) => ({
|
|
65
|
+
personaKey: bundle.personaKey,
|
|
66
|
+
personaName: bundle.catalogMetadata.displayName,
|
|
67
|
+
personaRole: bundle.catalogMetadata.role,
|
|
68
|
+
hireUrl: buildPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
|
|
69
|
+
protectedJobs: bundle.protectedJobs,
|
|
70
|
+
})),
|
|
71
|
+
lockedPersonas: [],
|
|
72
|
+
personas: resolvePersonaAccessStatuses(true, []),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function normalizeEmail(email) {
|
|
76
|
+
return email.trim().toLowerCase();
|
|
77
|
+
}
|
|
78
|
+
function mergeMetadata(existingMetadata, nextMetadata) {
|
|
79
|
+
if (!existingMetadata && !nextMetadata) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
...(existingMetadata || {}),
|
|
84
|
+
...(nextMetadata || {})
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function buildPersonaHireUrl(personaKey, hireMode = 'job', returnTo) {
|
|
88
|
+
const params = new URLSearchParams({
|
|
89
|
+
persona: personaKey,
|
|
90
|
+
mode: hireMode
|
|
91
|
+
});
|
|
92
|
+
if (returnTo) {
|
|
93
|
+
params.set('returnTo', returnTo);
|
|
94
|
+
}
|
|
95
|
+
return `/pricing?${params.toString()}`;
|
|
96
|
+
}
|
|
97
|
+
async function resolveWorkspaceContext(dbService, input) {
|
|
98
|
+
const normalizedUserId = normalizeEmail(input.userId);
|
|
99
|
+
let apiKey = input.workspaceId ? await dbService.getApiKeyByWorkspaceId(input.workspaceId, false) : null;
|
|
100
|
+
if (!apiKey && input.stripeCustomerId) {
|
|
101
|
+
apiKey = await dbService.getApiKeyByStripeCustomerId(input.stripeCustomerId);
|
|
102
|
+
}
|
|
103
|
+
if (!apiKey) {
|
|
104
|
+
apiKey = await dbService.getApiKeyByUserId(normalizedUserId, false);
|
|
105
|
+
}
|
|
106
|
+
const workspaceId = (0, workspace_identity_1.resolveWorkspaceId)({
|
|
107
|
+
workspaceId: apiKey?.workspaceId || input.workspaceId || undefined,
|
|
108
|
+
stripeCustomerId: input.stripeCustomerId || apiKey?.stripeCustomerId || undefined,
|
|
109
|
+
email: normalizedUserId
|
|
110
|
+
}) || `user:${normalizedUserId}`;
|
|
111
|
+
if (apiKey && apiKey.workspaceId !== workspaceId) {
|
|
112
|
+
await dbService.updateApiKey(apiKey.key, { workspaceId });
|
|
113
|
+
apiKey.workspaceId = workspaceId;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
workspaceId,
|
|
117
|
+
orgId: apiKey?.orgId || input.orgId || 'default',
|
|
118
|
+
apiKey
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function syncPersonaEntitlementPurchase(dbService, input) {
|
|
122
|
+
const persona = persona_hiring_1.PERSONA_HIRE_CATALOG[input.personaKey];
|
|
123
|
+
const normalizedUserId = normalizeEmail(input.userId);
|
|
124
|
+
const workspace = await resolveWorkspaceContext(dbService, {
|
|
125
|
+
userId: normalizedUserId,
|
|
126
|
+
orgId: input.orgId,
|
|
127
|
+
stripeCustomerId: input.stripeCustomerId
|
|
128
|
+
});
|
|
129
|
+
const existing = await dbService.getPersonaEntitlement(workspace.workspaceId, input.personaKey, input.hireMode);
|
|
130
|
+
const currentPurchaseCount = existing?.purchasedCount || 0;
|
|
131
|
+
const isNewPurchase = !existing || (input.stripeCheckoutSessionId &&
|
|
132
|
+
existing.stripeCheckoutSessionId !== input.stripeCheckoutSessionId) || (input.stripeSubscriptionId &&
|
|
133
|
+
existing.stripeSubscriptionId !== input.stripeSubscriptionId);
|
|
134
|
+
const jobCreditsIncluded = input.hireMode === 'job'
|
|
135
|
+
? (existing?.jobCreditsIncluded || 0) + (isNewPurchase ? DEFAULT_JOB_CREDITS : 0)
|
|
136
|
+
: 0;
|
|
137
|
+
const jobCreditsRemaining = input.hireMode === 'job'
|
|
138
|
+
? (existing?.jobCreditsRemaining || 0) + (isNewPurchase ? DEFAULT_JOB_CREDITS : 0)
|
|
139
|
+
: null;
|
|
140
|
+
const entitlement = await dbService.upsertPersonaEntitlement({
|
|
141
|
+
workspaceId: workspace.workspaceId,
|
|
142
|
+
userId: normalizedUserId,
|
|
143
|
+
orgId: workspace.orgId,
|
|
144
|
+
personaKey: input.personaKey,
|
|
145
|
+
personaName: persona.displayName,
|
|
146
|
+
personaRole: persona.role,
|
|
147
|
+
hireMode: input.hireMode,
|
|
148
|
+
status: input.status || 'active',
|
|
149
|
+
stripeCustomerId: input.stripeCustomerId || existing?.stripeCustomerId || null,
|
|
150
|
+
stripeSubscriptionId: input.stripeSubscriptionId || existing?.stripeSubscriptionId || null,
|
|
151
|
+
stripeCheckoutSessionId: input.stripeCheckoutSessionId || existing?.stripeCheckoutSessionId || null,
|
|
152
|
+
purchaseSource: input.purchaseSource,
|
|
153
|
+
purchasedCount: currentPurchaseCount + (isNewPurchase ? 1 : 0),
|
|
154
|
+
jobCreditsIncluded,
|
|
155
|
+
jobCreditsRemaining,
|
|
156
|
+
activatedAt: existing?.activatedAt || new Date(),
|
|
157
|
+
expiresAt: input.expiresAt ?? existing?.expiresAt ?? null,
|
|
158
|
+
canceledAt: input.status === 'expired' ? new Date() : null,
|
|
159
|
+
metadata: mergeMetadata(existing?.metadata, input.metadata)
|
|
160
|
+
});
|
|
161
|
+
// Transition a legacy workspace into the persona system on first purchase.
|
|
162
|
+
if (workspace.apiKey?.key && !workspace.apiKey.personaSystemActive) {
|
|
163
|
+
await dbService.updateApiKey(workspace.apiKey.key, { personaSystemActive: true }).catch(() => { });
|
|
164
|
+
}
|
|
165
|
+
return entitlement;
|
|
166
|
+
}
|
|
167
|
+
async function syncPersonaEntitlementFromCheckoutSession(dbService, session, purchaseSource) {
|
|
168
|
+
const personaKey = session.metadata?.personaKey;
|
|
169
|
+
const rawHireMode = session.metadata?.hireMode;
|
|
170
|
+
const email = session.customer_details?.email || session.customer_email;
|
|
171
|
+
if (!personaKey || !(0, persona_hiring_1.isPersonaHireKey)(personaKey) || !email || !rawHireMode || (rawHireMode !== 'job' && rawHireMode !== 'fulltime')) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const customerId = !session.customer
|
|
175
|
+
? null
|
|
176
|
+
: (typeof session.customer === 'string' ? session.customer : session.customer.id);
|
|
177
|
+
const subscriptionId = !session.subscription
|
|
178
|
+
? null
|
|
179
|
+
: (typeof session.subscription === 'string' ? session.subscription : session.subscription.id);
|
|
180
|
+
return await syncPersonaEntitlementPurchase(dbService, {
|
|
181
|
+
userId: email,
|
|
182
|
+
stripeCustomerId: customerId,
|
|
183
|
+
stripeSubscriptionId: subscriptionId,
|
|
184
|
+
stripeCheckoutSessionId: session.id,
|
|
185
|
+
personaKey,
|
|
186
|
+
hireMode: rawHireMode,
|
|
187
|
+
purchaseSource,
|
|
188
|
+
metadata: session.metadata || {}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function getWorkspacePersonaState(dbService, userId, apiKey) {
|
|
192
|
+
const normalizedUserId = normalizeEmail(userId);
|
|
193
|
+
let apiKeyData = null;
|
|
194
|
+
if (apiKey) {
|
|
195
|
+
apiKeyData = await dbService.getApiKeyByKey(apiKey);
|
|
196
|
+
}
|
|
197
|
+
if (!apiKeyData) {
|
|
198
|
+
apiKeyData = await dbService.getApiKeyByUserId(normalizedUserId, false);
|
|
199
|
+
}
|
|
200
|
+
const workspace = await resolveWorkspaceContext(dbService, {
|
|
201
|
+
userId: normalizedUserId,
|
|
202
|
+
orgId: apiKeyData?.orgId,
|
|
203
|
+
stripeCustomerId: apiKeyData?.stripeCustomerId,
|
|
204
|
+
workspaceId: apiKeyData?.workspaceId
|
|
205
|
+
});
|
|
206
|
+
let entitlements = await dbService.getPersonaEntitlementsByWorkspaceId(workspace.workspaceId, false);
|
|
207
|
+
// Fallback: when the local dev workspace has no API key the workspaceId may not
|
|
208
|
+
// match the cust: prefix written by the Stripe checkout sync. Look up by userId.
|
|
209
|
+
if (entitlements.length === 0) {
|
|
210
|
+
entitlements = await dbService.getPersonaEntitlementsByUserId(normalizedUserId, false);
|
|
211
|
+
}
|
|
212
|
+
const catalog = (0, persona_capability_bundles_1.listPersonaCapabilityBundles)().map((bundle) => ({
|
|
213
|
+
personaKey: bundle.personaKey,
|
|
214
|
+
personaName: bundle.catalogMetadata.displayName,
|
|
215
|
+
personaRole: bundle.catalogMetadata.role,
|
|
216
|
+
hireUrl: buildPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
|
|
217
|
+
protectedJobs: bundle.protectedJobs
|
|
218
|
+
}));
|
|
219
|
+
const knownPersonaKeys = new Set(entitlements.map((entitlement) => entitlement.personaKey));
|
|
220
|
+
const subscriptionActive = computeSubscriptionActive(apiKeyData, entitlements);
|
|
221
|
+
return {
|
|
222
|
+
featureFlagEnabled: true,
|
|
223
|
+
featureFlags: {
|
|
224
|
+
personaEntitlementsEnabled: true
|
|
225
|
+
},
|
|
226
|
+
subscriptionActive,
|
|
227
|
+
workspaceId: workspace.workspaceId,
|
|
228
|
+
userId: normalizedUserId,
|
|
229
|
+
apiKey: apiKeyData?.key || apiKey || null,
|
|
230
|
+
entitlements: entitlements.map((entitlement) => ({
|
|
231
|
+
personaKey: entitlement.personaKey,
|
|
232
|
+
personaName: entitlement.personaName,
|
|
233
|
+
personaRole: entitlement.personaRole,
|
|
234
|
+
hireMode: entitlement.hireMode,
|
|
235
|
+
status: entitlement.status,
|
|
236
|
+
jobCreditsRemaining: entitlement.jobCreditsRemaining,
|
|
237
|
+
expiresAt: entitlement.expiresAt ? entitlement.expiresAt.toISOString() : null,
|
|
238
|
+
hireUrl: buildPersonaHireUrl(entitlement.personaKey, entitlement.hireMode),
|
|
239
|
+
jobs: (0, persona_hiring_1.isPersonaHireKey)(entitlement.personaKey)
|
|
240
|
+
? (0, persona_capability_bundles_1.getPersonaCapabilityBundle)(entitlement.personaKey).protectedJobs
|
|
241
|
+
: []
|
|
242
|
+
})),
|
|
243
|
+
catalog,
|
|
244
|
+
lockedPersonas: catalog
|
|
245
|
+
.filter((persona) => !knownPersonaKeys.has(persona.personaKey))
|
|
246
|
+
.map((persona) => ({
|
|
247
|
+
...persona,
|
|
248
|
+
status: 'locked'
|
|
249
|
+
})),
|
|
250
|
+
// Authoritative per-persona verdict: a non-subscribed (legacy) workspace is
|
|
251
|
+
// ungated, so all personas are accessible; a subscribed one gates to owned.
|
|
252
|
+
personas: resolvePersonaAccessStatuses(!subscriptionActive, entitlements),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function evaluatePersonaAccess(dbService, userId, jobName, apiKey, returnTo) {
|
|
256
|
+
const normalizedUserId = normalizeEmail(userId);
|
|
257
|
+
let apiKeyData = null;
|
|
258
|
+
if (apiKey) {
|
|
259
|
+
apiKeyData = await dbService.getApiKeyByKey(apiKey);
|
|
260
|
+
}
|
|
261
|
+
if (!apiKeyData) {
|
|
262
|
+
apiKeyData = await dbService.getApiKeyByUserId(normalizedUserId, false);
|
|
263
|
+
}
|
|
264
|
+
const workspace = await resolveWorkspaceContext(dbService, {
|
|
265
|
+
userId: normalizedUserId,
|
|
266
|
+
orgId: apiKeyData?.orgId,
|
|
267
|
+
stripeCustomerId: apiKeyData?.stripeCustomerId,
|
|
268
|
+
workspaceId: apiKeyData?.workspaceId
|
|
269
|
+
});
|
|
270
|
+
const workspaceEntitlements = await dbService.getPersonaEntitlementsByWorkspaceId(workspace.workspaceId, false);
|
|
271
|
+
// Legacy bypass: if the workspace has never subscribed to the persona system,
|
|
272
|
+
// all jobs are freely accessible — no lock enforcement. Same rule the Hub
|
|
273
|
+
// display inherits via resolvePersonaAccessStatuses(!subscriptionActive, ...).
|
|
274
|
+
const subscriptionActive = computeSubscriptionActive(apiKeyData, workspaceEntitlements);
|
|
275
|
+
if (!subscriptionActive) {
|
|
276
|
+
return {
|
|
277
|
+
allowed: true,
|
|
278
|
+
personaKey: null,
|
|
279
|
+
workspaceId: workspace.workspaceId,
|
|
280
|
+
ownedPersonaKeys: []
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const ownedPersonaKeys = Array.from(new Set(workspaceEntitlements
|
|
284
|
+
.filter((entitlement) => entitlement.status === 'active')
|
|
285
|
+
.map((entitlement) => entitlement.personaKey)
|
|
286
|
+
.filter((personaKey) => (0, persona_hiring_1.isPersonaHireKey)(personaKey))));
|
|
287
|
+
const protectedPersonaKey = (0, persona_capability_bundles_1.getProtectedPersonaForJob)(jobName);
|
|
288
|
+
if (!protectedPersonaKey) {
|
|
289
|
+
return {
|
|
290
|
+
allowed: true,
|
|
291
|
+
personaKey: null,
|
|
292
|
+
workspaceId: workspace.workspaceId,
|
|
293
|
+
ownedPersonaKeys
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const fulltimeEntitlement = await dbService.getPersonaEntitlement(workspace.workspaceId, protectedPersonaKey, 'fulltime');
|
|
297
|
+
if (fulltimeEntitlement && fulltimeEntitlement.status === 'active') {
|
|
298
|
+
return {
|
|
299
|
+
allowed: true,
|
|
300
|
+
personaKey: protectedPersonaKey,
|
|
301
|
+
workspaceId: workspace.workspaceId,
|
|
302
|
+
ownedPersonaKeys,
|
|
303
|
+
entitlement: fulltimeEntitlement
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const jobEntitlement = await dbService.getPersonaEntitlement(workspace.workspaceId, protectedPersonaKey, 'job');
|
|
307
|
+
if (jobEntitlement && jobEntitlement.status === 'active' && (jobEntitlement.jobCreditsRemaining || 0) > 0) {
|
|
308
|
+
return {
|
|
309
|
+
allowed: true,
|
|
310
|
+
personaKey: protectedPersonaKey,
|
|
311
|
+
workspaceId: workspace.workspaceId,
|
|
312
|
+
ownedPersonaKeys,
|
|
313
|
+
entitlement: jobEntitlement
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const bundle = (0, persona_capability_bundles_1.getPersonaCapabilityBundle)(protectedPersonaKey);
|
|
317
|
+
const persona = persona_hiring_1.PERSONA_HIRE_CATALOG[protectedPersonaKey];
|
|
318
|
+
const priorEntitlement = fulltimeEntitlement || jobEntitlement || null;
|
|
319
|
+
const ownedOtherPersonas = ownedPersonaKeys
|
|
320
|
+
.filter((personaKey) => personaKey !== protectedPersonaKey)
|
|
321
|
+
.map((personaKey) => persona_hiring_1.PERSONA_HIRE_CATALOG[personaKey].displayName);
|
|
322
|
+
let message = bundle?.lockCopy || `This job requires ${persona.displayName}. Hire them to unlock it.`;
|
|
323
|
+
let hireUrl = buildPersonaHireUrl(protectedPersonaKey, bundle?.defaultHireMode || 'job', returnTo);
|
|
324
|
+
let ctaLabel = `Hire ${persona.displayName}`;
|
|
325
|
+
if (priorEntitlement?.status === 'suspended') {
|
|
326
|
+
message = `${persona.displayName} is currently suspended for this workspace because billing needs attention. Update billing to restore ${persona.displayName} for this request.`;
|
|
327
|
+
hireUrl = process.env.STRIPE_BILLING_PORTAL_URL || '/billing';
|
|
328
|
+
ctaLabel = `Restore ${persona.displayName}`;
|
|
329
|
+
}
|
|
330
|
+
else if (priorEntitlement?.status === 'expired') {
|
|
331
|
+
message = `${persona.displayName} was previously active in this workspace but is no longer active. Reactivate ${persona.displayName} to resume this request.`;
|
|
332
|
+
ctaLabel = `Reactivate ${persona.displayName}`;
|
|
333
|
+
}
|
|
334
|
+
else if (ownedOtherPersonas.length > 0) {
|
|
335
|
+
message = `${persona.displayName} owns this job, and your workspace has not hired ${persona.displayName} yet. You currently have ${ownedOtherPersonas.join(' and ')} active. ${ctaLabel} to unlock this request.`;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
allowed: false,
|
|
339
|
+
personaKey: protectedPersonaKey,
|
|
340
|
+
workspaceId: workspace.workspaceId,
|
|
341
|
+
ownedPersonaKeys,
|
|
342
|
+
entitlement: priorEntitlement,
|
|
343
|
+
lock: {
|
|
344
|
+
personaKey: protectedPersonaKey,
|
|
345
|
+
personaName: persona.displayName,
|
|
346
|
+
personaRole: persona.role,
|
|
347
|
+
message,
|
|
348
|
+
hireUrl,
|
|
349
|
+
ctaLabel
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeWorkspaceEmail = normalizeWorkspaceEmail;
|
|
4
|
+
exports.resolveWorkspaceId = resolveWorkspaceId;
|
|
5
|
+
function normalizeWorkspaceEmail(email) {
|
|
6
|
+
if (!email)
|
|
7
|
+
return null;
|
|
8
|
+
const trimmed = email.trim().toLowerCase();
|
|
9
|
+
return trimmed || null;
|
|
10
|
+
}
|
|
11
|
+
function resolveWorkspaceId(input) {
|
|
12
|
+
if (input.workspaceId)
|
|
13
|
+
return input.workspaceId;
|
|
14
|
+
if (input.stripeCustomerId)
|
|
15
|
+
return `cust:${input.stripeCustomerId}`;
|
|
16
|
+
const normalizedEmail = normalizeWorkspaceEmail(input.email);
|
|
17
|
+
if (normalizedEmail) {
|
|
18
|
+
return `user:${normalizedEmail}`;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculatePaymentAmount = calculatePaymentAmount;
|
|
4
|
+
exports.calculateDiscount = calculateDiscount;
|
|
5
|
+
exports.validateFounderEmail = validateFounderEmail;
|
|
6
|
+
exports.isConsumerEmailDomain = isConsumerEmailDomain;
|
|
7
|
+
const pricing_1 = require("../config/pricing");
|
|
8
|
+
function calculatePaymentAmount(plan, numberOfUsers, billingCycle, founderDiscount) {
|
|
9
|
+
let baseAmount;
|
|
10
|
+
if (plan === 'managed') {
|
|
11
|
+
// Managed plan: $200/user/month (license) + $10K per group of 10 (training/support)
|
|
12
|
+
const licenseCost = pricing_1.PRICING[plan][billingCycle] * numberOfUsers;
|
|
13
|
+
const numberOfGroups = Math.ceil(numberOfUsers / pricing_1.MANAGED_PRICING.licensesPerGroup);
|
|
14
|
+
const trainingCost = numberOfGroups * pricing_1.MANAGED_PRICING[billingCycle];
|
|
15
|
+
baseAmount = licenseCost + trainingCost;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
// Self-served plan: per-user pricing
|
|
19
|
+
const userCost = pricing_1.PRICING[plan][billingCycle] * numberOfUsers;
|
|
20
|
+
const fixedFee = pricing_1.FIXED_FEES[plan];
|
|
21
|
+
baseAmount = userCost + fixedFee;
|
|
22
|
+
}
|
|
23
|
+
if (founderDiscount) {
|
|
24
|
+
const discountAmount = Math.round(baseAmount * pricing_1.FOUNDER_DISCOUNT_RATE);
|
|
25
|
+
const amount = baseAmount - discountAmount;
|
|
26
|
+
return { amount, discountAmount };
|
|
27
|
+
}
|
|
28
|
+
return { amount: baseAmount, discountAmount: 0 };
|
|
29
|
+
}
|
|
30
|
+
function calculateDiscount(amount) {
|
|
31
|
+
return Math.round(amount * pricing_1.FOUNDER_DISCOUNT_RATE);
|
|
32
|
+
}
|
|
33
|
+
function validateFounderEmail(email) {
|
|
34
|
+
if (!email || !email.includes('@')) {
|
|
35
|
+
return { valid: false, reason: 'Invalid email format' };
|
|
36
|
+
}
|
|
37
|
+
const parts = email.split('@');
|
|
38
|
+
if (parts.length !== 2 || !parts[1] || parts[1].trim() === '') {
|
|
39
|
+
return { valid: false, reason: 'Invalid email format' };
|
|
40
|
+
}
|
|
41
|
+
const domain = parts[1].toLowerCase();
|
|
42
|
+
if (isConsumerEmailDomain(domain)) {
|
|
43
|
+
return {
|
|
44
|
+
valid: false,
|
|
45
|
+
reason: 'Founder/Academia discount requires a company or university email address (not gmail, yahoo, etc.)'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { valid: true };
|
|
49
|
+
}
|
|
50
|
+
function isConsumerEmailDomain(domain) {
|
|
51
|
+
return pricing_1.CONSUMER_EMAIL_DOMAINS.includes(domain.toLowerCase());
|
|
52
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
|
|
3
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
4
|
+
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
|
|
5
|
+
xsi:type="TaskPaneApp">
|
|
6
|
+
<Id>d1090951-50cf-4cf2-9d12-b0f8541d265c</Id>
|
|
7
|
+
<Version>1.0.0.0</Version>
|
|
8
|
+
<ProviderName>FRAIM</ProviderName>
|
|
9
|
+
<DefaultLocale>en-US</DefaultLocale>
|
|
10
|
+
<DisplayName DefaultValue="FRAIM"/>
|
|
11
|
+
<Description DefaultValue="AI Hub for your document - legal, product, engineering, and more in one task pane."/>
|
|
12
|
+
<IconUrl DefaultValue="http://localhost:43091/word-taskpane/icon-64.png"/>
|
|
13
|
+
<HighResolutionIconUrl DefaultValue="http://localhost:43091/word-taskpane/icon-64.png"/>
|
|
14
|
+
<AppDomains>
|
|
15
|
+
<AppDomain>https://appsforoffice.microsoft.com</AppDomain>
|
|
16
|
+
</AppDomains>
|
|
17
|
+
|
|
18
|
+
<Hosts>
|
|
19
|
+
<Host Name="Document"/>
|
|
20
|
+
</Hosts>
|
|
21
|
+
|
|
22
|
+
<Requirements>
|
|
23
|
+
<Sets>
|
|
24
|
+
<Set Name="WordApi" MinVersion="1.1"/>
|
|
25
|
+
</Sets>
|
|
26
|
+
</Requirements>
|
|
27
|
+
|
|
28
|
+
<DefaultSettings>
|
|
29
|
+
<SourceLocation DefaultValue="http://localhost:43091/word-taskpane/"/>
|
|
30
|
+
</DefaultSettings>
|
|
31
|
+
|
|
32
|
+
<Permissions>ReadWriteDocument</Permissions>
|
|
33
|
+
</OfficeApp>
|