fraim 2.0.353 → 2.0.354

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getFeatureFlags = getFeatureFlags;
4
4
  exports.isPersonaEntitlementsEnabled = isPersonaEntitlementsEnabled;
5
+ exports.isShowTokenCostEnabled = isShowTokenCostEnabled;
5
6
  exports.getPublicFeatureFlags = getPublicFeatureFlags;
6
7
  function parseBooleanFlag(value) {
7
8
  if (!value)
@@ -18,6 +19,9 @@ function getFeatureFlags() {
18
19
  function isPersonaEntitlementsEnabled() {
19
20
  return getFeatureFlags().personaEntitlements.enabled;
20
21
  }
22
+ function isShowTokenCostEnabled() {
23
+ return parseBooleanFlag(process.env.FRAIM_FF_SHOW_TOKEN_COST);
24
+ }
21
25
  function getPublicFeatureFlags() {
22
26
  return {
23
27
  personaEntitlementsEnabled: isPersonaEntitlementsEnabled()
@@ -188,6 +188,10 @@ class FraimDbService {
188
188
  await this.sessionsCollection.createIndex({ sessionId: 1 }, { unique: true });
189
189
  await this.sessionsCollection.createIndex({ userId: 1 });
190
190
  await this.sessionsCollection.createIndex({ lastActive: -1 });
191
+ // Issue #2093 — getActiveSessionByApiKey no longer bounds its match by recency, so a
192
+ // long-lived user's full session history is a candidate. This compound index keeps that
193
+ // lookup an indexed scan with an ordered result instead of a fetch-then-sort in memory.
194
+ await this.sessionsCollection.createIndex({ userId: 1, lastActive: -1 });
191
195
  await this.signupsCollection.createIndex({ email: 1 }, { unique: true });
192
196
  await this.signupsCollection.createIndex({ timestamp: -1 });
193
197
  await this.salesCollection.createIndex({ email: 1 });
@@ -344,17 +348,21 @@ class FraimDbService {
344
348
  // Preserve the stored credential and its history; auth checks the current entitlement.
345
349
  return paid || credential;
346
350
  }
351
+ /**
352
+ * Most recently active session for the key's owner, at any age.
353
+ *
354
+ * Issue #2093 — a FRAIM work session has no lifetime. Elapsed time must never make an
355
+ * existing, owned session unresolvable, so this lookup filters on ownership only and
356
+ * leaves recency to the sort. Access is still bounded by `verifyApiKey`: an unknown,
357
+ * revoked or suspended credential resolves nothing.
358
+ */
347
359
  async getActiveSessionByApiKey(key) {
348
360
  if (!this.keysCollection || !this.sessionsCollection)
349
361
  throw new Error('DB not connected');
350
- // 1. Get user for this key
351
362
  const apiKeyData = await this.verifyApiKey(key);
352
363
  if (!apiKeyData)
353
364
  return null;
354
- // 2. Get latest session for this user (within last 24h)
355
- const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
356
- const session = await this.sessionsCollection.findOne({ userId: apiKeyData.userId, lastActive: { $gt: dayAgo } }, { sort: { lastActive: -1 } });
357
- return session;
365
+ return this.sessionsCollection.findOne({ userId: apiKeyData.userId }, { sort: { lastActive: -1 } });
358
366
  }
359
367
  async getSessionByApiKeyAndSessionId(key, sessionId) {
360
368
  if (!this.keysCollection || !this.sessionsCollection)
@@ -364,11 +372,17 @@ class FraimDbService {
364
372
  return null;
365
373
  return this.getSessionByUserIdAndSessionId(apiKeyData.userId, sessionId);
366
374
  }
375
+ /**
376
+ * Exact session lookup scoped to its owner, at any age.
377
+ *
378
+ * Issue #2093 — resuming a session after days, months or years must return the original
379
+ * record so the run keeps its job, phase and review state. `userId` remains the only
380
+ * eligibility condition, so another account still cannot reach this session.
381
+ */
367
382
  async getSessionByUserIdAndSessionId(userId, sessionId) {
368
383
  if (!this.sessionsCollection)
369
384
  throw new Error('DB not connected');
370
- const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
371
- return this.sessionsCollection.findOne({ sessionId, userId, lastActive: { $gt: dayAgo } });
385
+ return this.sessionsCollection.findOne({ sessionId, userId });
372
386
  }
373
387
  async createAuthSession(input) {
374
388
  if (!this.authSessionsCollection)
@@ -137,7 +137,9 @@ class McpService {
137
137
  else if (method === 'fraim/session/validate') {
138
138
  const sessionId = typeof params?.sessionId === 'string' ? params.sessionId.trim() : '';
139
139
  if (!sessionId || !context.userId || !(await this.dbService.getSessionByUserIdAndSessionId(context.userId, sessionId))) {
140
- const error = new Error('FRAIM session is missing, expired, or invalid. Call fraim_connect again.');
140
+ // Issue #2093 — sessions do not expire, so the rejection names the two
141
+ // causes that remain: the session is unknown, or it belongs elsewhere.
142
+ const error = new Error('FRAIM session is unknown or not accessible with this account. Call fraim_connect again.');
141
143
  error.code = -32001;
142
144
  throw error;
143
145
  }
@@ -306,19 +306,21 @@ class UsageAnalyticsService {
306
306
  await this.logUsage(mentoringEvent);
307
307
  }
308
308
  /**
309
- * Validate that a jobId exists for the user (check recent usage events)
309
+ * Validate that this user started this jobId, regardless of how long ago.
310
310
  */
311
311
  async validateJobId(jobId, userId) {
312
312
  try {
313
- // Check if there's a recent job start event for this jobId and user
314
- const recentEvents = await this.dbService.getDb();
315
- const collection = recentEvents.collection('fraim_usage_events');
313
+ // Issue #2093 — a run resumed days or months later is still the same job, so
314
+ // this match is on job identity and ownership only. Adding an age condition
315
+ // here would silently drop analytics for exactly the long-gap resumes that
316
+ // the session fix is meant to support.
317
+ const events = await this.dbService.getDb();
318
+ const collection = events.collection('fraim_usage_events');
316
319
  const jobStartEvent = await collection.findOne({
317
320
  jobId,
318
321
  userId,
319
322
  type: 'job',
320
- 'args.action': 'start',
321
- createdAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } // Within last 24 hours
323
+ 'args.action': 'start'
322
324
  });
323
325
  return jobStartEvent !== null;
324
326
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.353",
3
+ "version": "2.0.354",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {