fraim-framework 2.0.195 → 2.0.197

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.
@@ -1,5 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeSubscriptionActive = computeSubscriptionActive;
4
+ exports.resolvePersonaAccessStatuses = resolvePersonaAccessStatuses;
5
+ exports.buildUngatedPersonaWorkspaceState = buildUngatedPersonaWorkspaceState;
3
6
  exports.buildPersonaHireUrl = buildPersonaHireUrl;
4
7
  exports.syncPersonaEntitlementPurchase = syncPersonaEntitlementPurchase;
5
8
  exports.syncPersonaEntitlementFromCheckoutSession = syncPersonaEntitlementFromCheckoutSession;
@@ -9,6 +12,66 @@ const persona_hiring_1 = require("../config/persona-hiring");
9
12
  const persona_capability_bundles_1 = require("../config/persona-capability-bundles");
10
13
  const workspace_identity_1 = require("./workspace-identity");
11
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
+ }
12
75
  function normalizeEmail(email) {
13
76
  return email.trim().toLowerCase();
14
77
  }
@@ -154,9 +217,7 @@ async function getWorkspacePersonaState(dbService, userId, apiKey) {
154
217
  protectedJobs: bundle.protectedJobs
155
218
  }));
156
219
  const knownPersonaKeys = new Set(entitlements.map((entitlement) => entitlement.personaKey));
157
- // Subscribed = explicitly opted in via flag, OR has ever had an active entitlement.
158
- const subscriptionActive = apiKeyData?.personaSystemActive === true ||
159
- entitlements.some((e) => e.status === 'active');
220
+ const subscriptionActive = computeSubscriptionActive(apiKeyData, entitlements);
160
221
  return {
161
222
  featureFlagEnabled: true,
162
223
  featureFlags: {
@@ -185,7 +246,10 @@ async function getWorkspacePersonaState(dbService, userId, apiKey) {
185
246
  .map((persona) => ({
186
247
  ...persona,
187
248
  status: 'locked'
188
- }))
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),
189
253
  };
190
254
  }
191
255
  async function evaluatePersonaAccess(dbService, userId, jobName, apiKey, returnTo) {
@@ -205,9 +269,9 @@ async function evaluatePersonaAccess(dbService, userId, jobName, apiKey, returnT
205
269
  });
206
270
  const workspaceEntitlements = await dbService.getPersonaEntitlementsByWorkspaceId(workspace.workspaceId, false);
207
271
  // Legacy bypass: if the workspace has never subscribed to the persona system,
208
- // all jobs are freely accessible — no lock enforcement.
209
- const subscriptionActive = apiKeyData?.personaSystemActive === true ||
210
- workspaceEntitlements.some((e) => e.status === 'active');
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);
211
275
  if (!subscriptionActive) {
212
276
  return {
213
277
  allowed: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.195",
3
+ "version": "2.0.197",
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": {
@@ -12,6 +12,9 @@
12
12
  // #700: set the theme pre-paint (no flash). Explicit choice in
13
13
  // localStorage wins; otherwise follow the OS preference.
14
14
  try{var t=localStorage.getItem('fraim-theme');if(t!=='light'&&t!=='dark'){t=(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light';}h.setAttribute('data-theme',t);}catch(e){h.setAttribute('data-theme','light');}
15
+ // #744: apply the cached org brand pre-paint (no flash of FRAIM identity
16
+ // before company branding). script.js refines the per-theme accent on load.
17
+ try{var b=JSON.parse(localStorage.getItem('fraim-org-brand')||'null');if(b){if(typeof b.color==='string'&&/^#[0-9a-f]{6}$/i.test(b.color)){h.style.setProperty('--accent',b.color);h.style.setProperty('--accent-strong',b.color);}if(typeof b.name==='string'&&b.name){document.title=b.name+' Hub';}}}catch(e){}
15
18
  })();</script>
16
19
  <link rel="stylesheet" href="./styles.css?v=conv-panels-20260611b">
17
20
  <link rel="stylesheet" href="./review.css">
@@ -20,10 +23,15 @@
20
23
 
21
24
  <!-- Issue #512: three-area top nav + account menu (surface=hub only). -->
22
25
  <nav class="hub-tabs" id="hub-tabs" hidden>
26
+ <!-- #744: company brand lockup (logo + name), populated from bootstrap.orgBrand. -->
27
+ <span class="hub-brand" id="hub-brand" hidden></span>
28
+ <span class="hub-brand-divider" id="hub-brand-divider" hidden></span>
23
29
  <button class="hub-tab" type="button" data-area="company">Company</button>
24
30
  <button class="hub-tab" type="button" data-area="manager">Manager</button>
25
31
  <button class="hub-tab on" type="button" data-area="projects">Projects</button>
26
32
  <div class="nav-right">
33
+ <!-- #744: FRAIM co-mark (co-branding, not white-label) shown when a brand is set. -->
34
+ <span class="hub-cobrand" id="hub-cobrand" hidden></span>
27
35
  <button class="avatar-btn" id="avatar-btn" type="button" title="Account &amp; settings">SM</button>
28
36
  <div id="account-menu" class="account-menu">
29
37
  <div class="am-header">
@@ -68,6 +76,12 @@
68
76
  <div class="area-h1">Company</div>
69
77
  <p class="area-lede">Your organization's identity — what you do, how you operate, and the guardrails every employee follows on every job.</p>
70
78
  <div id="company-push-banner"></div>
79
+ <!-- #744: Brand editor — company identity across the Hub. -->
80
+ <details class="ctx-acc" id="company-brand-acc">
81
+ <summary><span class="ca-chev">▸</span> <span>🎨 Brand</span>
82
+ <span class="ca-note">— your company's identity across the Hub, applied for every teammate</span></summary>
83
+ <div class="ctx-acc-body"><div class="card area-profile" id="company-brand-editor"></div></div>
84
+ </details>
71
85
  <details class="ctx-acc" id="company-ctx-acc">
72
86
  <summary><span class="ca-chev">▸</span> <span>🏢 Context &amp; rules</span></summary>
73
87
  <div class="ctx-acc-body"><div class="card area-profile" id="company-profile"></div></div>
@@ -199,6 +213,7 @@
199
213
  <!-- Onboarding state banner lives ABOVE the accordions so it shows even
200
214
  when the Brief is collapsed (so the conversation keeps its height). -->
201
215
  <div id="proj-onboarding-banner"></div>
216
+ <div id="hub-agent-setup-panel" class="hub-agent-setup-panel" data-testid="hub-cli-setup-panel" hidden></div>
202
217
  <details class="ctx-acc" id="proj-brief-acc">
203
218
  <summary><span class="ca-chev">▸</span> <span>📋 Brief</span>
204
219
  <span class="ca-note">— this project's context and rules, captured by Project Onboarding</span></summary>
@@ -839,6 +854,7 @@
839
854
  <span class="cp-agent-label">Run with:</span>
840
855
  <span id="cp-agent-picker" class="cp-agent-pills"></span>
841
856
  </div>
857
+ <div id="cp-agent-install-panel" class="cp-agent-install-panel" data-testid="cp-cli-setup-panel" hidden></div>
842
858
  </div>
843
859
  </div>
844
860
 
@@ -868,6 +884,6 @@
868
884
  </div>
869
885
  </div>
870
886
 
871
- <script src="./script.js?v=conv-persist-20260705"></script>
887
+ <script src="./script.js?v=persona-mgr-del-20260707"></script>
872
888
  </body>
873
889
  </html>