shieldcortex 3.4.28 → 3.4.29

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.
Files changed (41) hide show
  1. package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
  2. package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
  3. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
  4. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
  5. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  6. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  7. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  8. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  9. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
  11. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +1 -1
  12. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  13. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  14. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  15. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  16. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  17. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  18. package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
  19. package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +1 -1
  20. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  21. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +1 -1
  22. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
  23. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +1 -1
  24. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  25. package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
  26. package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
  27. package/dist/api/routes/admin.js +14 -2
  28. package/dist/api/routes/system.js +28 -0
  29. package/dist/database/init.js +4 -4
  30. package/dist/index.js +54 -1
  31. package/dist/license/cli.js +50 -22
  32. package/dist/license/index.d.ts +1 -1
  33. package/dist/license/index.js +1 -1
  34. package/dist/license/store.d.ts +4 -1
  35. package/dist/license/store.js +38 -15
  36. package/dist/license/trial.d.ts +48 -0
  37. package/dist/license/trial.js +128 -0
  38. package/package.json +1 -1
  39. /package/dashboard/.next/standalone/dashboard/.next/static/{oqbBQnPvKRkfU3j_7cE7c → c51lWSauvNC0BT832gfq4}/_buildManifest.js +0 -0
  40. /package/dashboard/.next/standalone/dashboard/.next/static/{oqbBQnPvKRkfU3j_7cE7c → c51lWSauvNC0BT832gfq4}/_clientMiddlewareManifest.json +0 -0
  41. /package/dashboard/.next/standalone/dashboard/.next/static/{oqbBQnPvKRkfU3j_7cE7c → c51lWSauvNC0BT832gfq4}/_ssgManifest.js +0 -0
@@ -3,10 +3,38 @@ import { getCloudConfig, getCloudSyncControls, getDeviceId, getDeviceName, getDe
3
3
  import { getQueueStats } from '../../cloud/sync-queue.js';
4
4
  import { getDatabase } from '../../database/init.js';
5
5
  import { getRequiredTier, isFeatureEnabled } from '../../license/gate.js';
6
+ import { getLicense } from '../../license/store.js';
7
+ import { getTrialStatus } from '../../license/trial.js';
8
+ import { existsSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { homedir } from 'os';
6
11
  import { getControlStatus, isKillSwitchActive, pause, resume } from '../control.js';
7
12
  import { checkForUpdates, getCurrentVersion, getRunningVersion, performUpdate, scheduleRestart, } from '../version.js';
8
13
  export function registerSystemRoutes(app, deps) {
9
14
  const { broadcast, clients, requireIronDomeAction } = deps;
15
+ app.get('/api/system/status', (_req, res) => {
16
+ try {
17
+ const licenseFile = join(homedir(), '.shieldcortex', 'license.json');
18
+ const licenseFileExists = existsSync(licenseFile);
19
+ const license = getLicense();
20
+ const trial = getTrialStatus(licenseFileExists);
21
+ const trialInfo = trial
22
+ ? {
23
+ active: trial.active,
24
+ daysRemaining: trial.daysRemaining,
25
+ expiresAt: trial.expiresAt.slice(0, 10), // YYYY-MM-DD
26
+ }
27
+ : null;
28
+ res.json({
29
+ tier: license.valid ? license.tier : (trial?.active ? 'pro' : 'free'),
30
+ licenseValid: license.valid,
31
+ trial: trialInfo,
32
+ });
33
+ }
34
+ catch (error) {
35
+ res.status(500).json({ error: error.message });
36
+ }
37
+ });
10
38
  app.get('/api/control/status', (_req, res) => {
11
39
  try {
12
40
  res.json(getControlStatus());
@@ -266,12 +266,12 @@ function attemptDumpRecovery(dbPath) {
266
266
  return null;
267
267
  // Back up the corrupt file
268
268
  const backupPath = backupCorruptDatabase(dbPath);
269
- console.log(`[database] Backed up corrupt database to: ${backupPath}`);
269
+ console.error(`[database] Backed up corrupt database to: ${backupPath}`);
270
270
  // Create fresh database and import the dump
271
271
  const freshDb = new Database(dbPath);
272
272
  try {
273
273
  freshDb.exec(dumpOutput);
274
- console.log('[database] Successfully recovered data via dump/reimport.');
274
+ console.error('[database] Successfully recovered data via dump/reimport.');
275
275
  return freshDb;
276
276
  }
277
277
  catch {
@@ -330,7 +330,7 @@ function attemptFtsRecovery(database) {
330
330
  database.exec(`INSERT INTO memories_fts(memories_fts) VALUES('rebuild')`);
331
331
  const postRepairIntegrity = runIntegrityCheck(database);
332
332
  if (postRepairIntegrity === 'ok') {
333
- console.log('[database] Successfully rebuilt memories_fts after integrity failure.');
333
+ console.error('[database] Successfully rebuilt memories_fts after integrity failure.');
334
334
  return true;
335
335
  }
336
336
  console.warn(`[database] FTS rebuild did not fully repair integrity: ${postRepairIntegrity}`);
@@ -374,7 +374,7 @@ export function initDatabase(dbPath) {
374
374
  // Store path for size monitoring
375
375
  currentDbPath = expandedPath;
376
376
  acquireStartupLock(expandedPath);
377
- console.log(`[database] Startup runtime=${resolveRuntimeInfo().kind} db=${expandedPath} wal=${existsSync(expandedPath + '-wal')} shm=${existsSync(expandedPath + '-shm')}`);
377
+ console.error(`[database] Startup runtime=${resolveRuntimeInfo().kind} db=${expandedPath} wal=${existsSync(expandedPath + '-wal')} shm=${existsSync(expandedPath + '-shm')}`);
378
378
  const healthyBackups = listHealthyBackups(expandedPath);
379
379
  // Wrap the initial open in try/catch to handle corrupt files gracefully
380
380
  let database;
package/dist/index.js CHANGED
@@ -363,6 +363,59 @@ async function isLocalDashboardRunning() {
363
363
  async function main() {
364
364
  // Warn if npx is serving a stale cached version
365
365
  checkVersionStaleness();
366
+ const parsedArgs = parseArgs();
367
+ // ── Trial welcome / expiry warning ──────────────────────
368
+ // Only show for interactive CLI commands (not MCP server mode — stdout must stay clean for JSON-RPC)
369
+ if (process.argv[2] && parsedArgs.mode !== 'mcp') {
370
+ try {
371
+ const { existsSync } = await import('fs');
372
+ const { join } = await import('path');
373
+ const { homedir } = await import('os');
374
+ const { getTrialStatus } = await import('./license/trial.js');
375
+ const { acknowledgeTrialWelcome } = await import('./license/trial.js');
376
+ const { getLicense } = await import('./license/store.js');
377
+ const licenseFile = join(homedir(), '.shieldcortex', 'license.json');
378
+ const licenseFileExists = existsSync(licenseFile);
379
+ const activeLicense = getLicense();
380
+ // Only show trial messages when no paid license is active
381
+ if (!activeLicense.valid) {
382
+ const trial = getTrialStatus(licenseFileExists);
383
+ if (trial?.justCreated) {
384
+ // First ever run — show welcome message
385
+ const expiryDate = new Date(trial.expiresAt).toLocaleDateString();
386
+ const bold = '\x1b[1m';
387
+ const reset = '\x1b[0m';
388
+ const green = '\x1b[32m';
389
+ const cyan = '\x1b[36m';
390
+ const yellow = '\x1b[33m';
391
+ console.log(`\n${bold}🎁 Welcome to ShieldCortex!${reset}\n`);
392
+ console.log(`You have a ${yellow}14-day Pro trial${reset} — all Pro features are unlocked.\n`);
393
+ console.log(` ${green}✓${reset} Custom injection patterns (up to 50)`);
394
+ console.log(` ${green}✓${reset} Custom Iron Dome policies`);
395
+ console.log(` ${green}✓${reset} Custom firewall rules`);
396
+ console.log(` ${green}✓${reset} Audit export (JSON/CSV)`);
397
+ console.log(` ${green}✓${reset} Skill scanner deep mode`);
398
+ console.log(`\nYour trial expires on ${bold}${expiryDate}${reset}. Upgrade anytime at:`);
399
+ console.log(` ${cyan}https://shieldcortex.ai/pricing${reset}`);
400
+ console.log(`\nRun: shieldcortex license status\n`);
401
+ acknowledgeTrialWelcome();
402
+ }
403
+ else if (trial?.active && trial.daysRemaining <= 3) {
404
+ // Trial expiring soon — show warning
405
+ const yellow = '\x1b[33m';
406
+ const cyan = '\x1b[36m';
407
+ const reset = '\x1b[0m';
408
+ const days = trial.daysRemaining;
409
+ console.error(`${yellow}⚠️ Pro trial expires in ${days} day${days !== 1 ? 's' : ''}. Upgrade to keep Pro features:${reset}`);
410
+ console.error(` shieldcortex license activate <key>`);
411
+ console.error(` ${cyan}https://shieldcortex.ai/pricing${reset}\n`);
412
+ }
413
+ }
414
+ }
415
+ catch {
416
+ // Best effort — never let trial messaging crash the CLI
417
+ }
418
+ }
366
419
  // Handle --help / -h / help / unknown args
367
420
  if (process.argv[2] === '--help' || process.argv[2] === '-h' || process.argv[2] === 'help') {
368
421
  const bold = '\x1b[1m';
@@ -689,7 +742,7 @@ ${bold}DOCS${reset}
689
742
  console.error(`Run 'shieldcortex --help' for a list of commands.`);
690
743
  process.exit(1);
691
744
  }
692
- const { dbPath, mode } = parseArgs();
745
+ const { dbPath, mode } = parsedArgs;
693
746
  let dashboardProcess = null;
694
747
  if (mode === 'api') {
695
748
  // API mode only - for dashboard visualization
@@ -5,9 +5,15 @@
5
5
  * shieldcortex license status
6
6
  * shieldcortex license deactivate
7
7
  */
8
- import { activateLicense, deactivateLicense, getLicense, getLicenseFile } from './store.js';
8
+ import { activateLicense, deactivateLicense, getLicense, getLicenseFile, getTrialStatus } from './store.js';
9
9
  import { listFeatures } from './gate.js';
10
10
  import { validateOnceNow } from './validate.js';
11
+ import { existsSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { homedir } from 'os';
14
+ function getLicenseFilePath() {
15
+ return join(process.env.SHIELDCORTEX_CONFIG_DIR || join(homedir(), '.shieldcortex'), 'license.json');
16
+ }
11
17
  const bold = '\x1b[1m';
12
18
  const reset = '\x1b[0m';
13
19
  const green = '\x1b[32m';
@@ -72,31 +78,53 @@ async function handleActivate(key) {
72
78
  function handleStatus() {
73
79
  const info = getLicense();
74
80
  const file = getLicenseFile();
81
+ const licenseFileExists = existsSync(getLicenseFilePath());
82
+ const trial = getTrialStatus(licenseFileExists);
75
83
  console.log(`\n${bold}ShieldCortex Licence${reset}`);
76
84
  console.log('═'.repeat(40));
77
- console.log(` Tier: ${tierBadge(info.tier)}`);
78
- if (info.tier === 'free') {
79
- console.log(`\n No licence activated.`);
80
- console.log(` Upgrade at ${cyan}https://shieldcortex.ai/pricing${reset}`);
81
- console.log(` Activate: shieldcortex license activate <key>\n`);
82
- return;
83
- }
84
- console.log(` Email: ${info.email}`);
85
- if (info.expiresAt) {
86
- const daysStr = info.daysUntilExpiry !== null && info.daysUntilExpiry > 0
87
- ? `(${info.daysUntilExpiry} days remaining)`
88
- : info.daysUntilExpiry !== null && info.daysUntilExpiry <= 0
89
- ? `${red}(expired)${reset}`
90
- : '';
91
- console.log(` Expires: ${info.expiresAt.toLocaleDateString()} ${daysStr}`);
85
+ if (info.valid) {
86
+ // Paid license active — show full license info, no trial messaging
87
+ console.log(` Tier: ${tierBadge(info.tier)}`);
88
+ console.log(` Email: ${info.email}`);
89
+ if (info.expiresAt) {
90
+ const daysStr = info.daysUntilExpiry !== null && info.daysUntilExpiry > 0
91
+ ? `(${info.daysUntilExpiry} days remaining)`
92
+ : info.daysUntilExpiry !== null && info.daysUntilExpiry <= 0
93
+ ? `${red}(expired)${reset}`
94
+ : '';
95
+ console.log(` Expires: ${info.expiresAt.toLocaleDateString()} ${daysStr}`);
96
+ }
97
+ if (file?.lastValidatedAt) {
98
+ console.log(` Validated: ${new Date(file.lastValidatedAt).toLocaleString()}`);
99
+ }
100
+ if (file?.validationStatus) {
101
+ const statusColor = file.validationStatus === 'valid' ? green :
102
+ file.validationStatus === 'revoked' ? red : yellow;
103
+ console.log(` Status: ${statusColor}${file.validationStatus}${reset}`);
104
+ }
92
105
  }
93
- if (file?.lastValidatedAt) {
94
- console.log(` Validated: ${new Date(file.lastValidatedAt).toLocaleString()}`);
106
+ else if (trial?.active) {
107
+ // Trial active — no paid license
108
+ const expiryDate = new Date(trial.expiresAt).toLocaleDateString();
109
+ console.log(` Tier: ${tierBadge('pro')} ${dim}(trial)${reset}`);
110
+ console.log(` 🎁 Pro Trial: ${yellow}${trial.daysRemaining} day${trial.daysRemaining !== 1 ? 's' : ''} remaining${reset} (expires ${expiryDate})`);
111
+ console.log(`\n Upgrade to keep Pro features after the trial:`);
112
+ console.log(` ${cyan}https://shieldcortex.ai/pricing${reset}`);
113
+ console.log(` shieldcortex license activate <key>`);
95
114
  }
96
- if (file?.validationStatus) {
97
- const statusColor = file.validationStatus === 'valid' ? green :
98
- file.validationStatus === 'revoked' ? red : yellow;
99
- console.log(` Status: ${statusColor}${file.validationStatus}${reset}`);
115
+ else {
116
+ // No license and no active trial
117
+ console.log(` Tier: ${tierBadge('free')}`);
118
+ if (trial && !trial.active) {
119
+ // Trial existed but expired
120
+ console.log(`\n ${yellow}Pro trial expired.${reset} Upgrade at ${cyan}https://shieldcortex.ai/pricing${reset}`);
121
+ }
122
+ else {
123
+ console.log(`\n No licence activated.`);
124
+ console.log(` Upgrade at ${cyan}https://shieldcortex.ai/pricing${reset}`);
125
+ }
126
+ console.log(` Activate: shieldcortex license activate <key>\n`);
127
+ return;
100
128
  }
101
129
  console.log(`\n${bold}Features:${reset}`);
102
130
  const features = listFeatures();
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * License module — re-exports for public API and internal use.
3
3
  */
4
- export { getLicense, getLicenseTier, getLicenseFile, clearLicenseCache } from './store.js';
4
+ export { getLicense, getLicenseTier, getLicenseFile, clearLicenseCache, getTrialStatus, isTrialActive, getTrialDaysRemaining } from './store.js';
5
5
  export { verifyLicenseKey, parseLicensePayload } from './verify.js';
6
6
  export { isFeatureEnabled, requireFeature, getRequiredTier, listFeatures, FeatureGatedError, } from './gate.js';
7
7
  export { scheduleOnlineValidation } from './validate.js';
@@ -2,7 +2,7 @@
2
2
  * License module — re-exports for public API and internal use.
3
3
  */
4
4
  // ── Public API (exported from lib.ts) ────────────────────
5
- export { getLicense, getLicenseTier, getLicenseFile, clearLicenseCache } from './store.js';
5
+ export { getLicense, getLicenseTier, getLicenseFile, clearLicenseCache, getTrialStatus, isTrialActive, getTrialDaysRemaining } from './store.js';
6
6
  export { verifyLicenseKey, parseLicensePayload } from './verify.js';
7
7
  export { isFeatureEnabled, requireFeature, getRequiredTier, listFeatures, FeatureGatedError, } from './gate.js';
8
8
  export { scheduleOnlineValidation } from './validate.js';
@@ -10,6 +10,8 @@
10
10
  * License is cached in memory after first read.
11
11
  */
12
12
  import type { LicenseTier, LicenseInfo, LicenseFile } from './keys.js';
13
+ import { getTrialStatus, isTrialActive, getTrialDaysRemaining } from './trial.js';
14
+ export { isTrialActive, getTrialDaysRemaining, getTrialStatus };
13
15
  /** Clear the in-memory cache (useful for testing or after activation). */
14
16
  export declare function clearLicenseCache(): void;
15
17
  /**
@@ -19,7 +21,8 @@ export declare function clearLicenseCache(): void;
19
21
  export declare function getLicense(): LicenseInfo;
20
22
  /**
21
23
  * Quick accessor for the current licence tier.
22
- * Returns 'free' if no valid licence exists.
24
+ * Returns 'pro' during an active trial (if no paid license is active).
25
+ * Returns 'free' otherwise.
23
26
  */
24
27
  export declare function getLicenseTier(): LicenseTier;
25
28
  /**
@@ -13,13 +13,20 @@ import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from '
13
13
  import { join } from 'path';
14
14
  import { homedir } from 'os';
15
15
  import { verifyLicenseKey } from './verify.js';
16
- const CONFIG_DIR = join(homedir(), '.shieldcortex');
17
- const LICENSE_FILE = join(CONFIG_DIR, 'license.json');
16
+ import { getTrialStatus, isTrialActive, getTrialDaysRemaining, clearTrialCache, } from './trial.js';
17
+ export { isTrialActive, getTrialDaysRemaining, getTrialStatus };
18
+ function getConfigDir() {
19
+ return process.env.SHIELDCORTEX_CONFIG_DIR || join(homedir(), '.shieldcortex');
20
+ }
21
+ function getLicenseFilePath() {
22
+ return join(getConfigDir(), 'license.json');
23
+ }
18
24
  // ── Cache ────────────────────────────────────────────────
19
25
  let cachedLicense = null;
20
26
  /** Clear the in-memory cache (useful for testing or after activation). */
21
27
  export function clearLicenseCache() {
22
28
  cachedLicense = null;
29
+ clearTrialCache();
23
30
  }
24
31
  // ── Read ─────────────────────────────────────────────────
25
32
  /**
@@ -39,11 +46,12 @@ export function getLicense() {
39
46
  subscriptionId: null,
40
47
  };
41
48
  try {
42
- if (!existsSync(LICENSE_FILE)) {
49
+ const licenseFile = getLicenseFilePath();
50
+ if (!existsSync(licenseFile)) {
43
51
  cachedLicense = FREE;
44
52
  return FREE;
45
53
  }
46
- const raw = JSON.parse(readFileSync(LICENSE_FILE, 'utf-8'));
54
+ const raw = JSON.parse(readFileSync(licenseFile, 'utf-8'));
47
55
  if (!raw.key) {
48
56
  cachedLicense = FREE;
49
57
  return FREE;
@@ -68,10 +76,19 @@ export function getLicense() {
68
76
  }
69
77
  /**
70
78
  * Quick accessor for the current licence tier.
71
- * Returns 'free' if no valid licence exists.
79
+ * Returns 'pro' during an active trial (if no paid license is active).
80
+ * Returns 'free' otherwise.
72
81
  */
73
82
  export function getLicenseTier() {
74
- return getLicense().tier;
83
+ const license = getLicense();
84
+ // Paid license takes priority — trial is irrelevant
85
+ if (license.valid)
86
+ return license.tier;
87
+ // No paid license: check if trial is active
88
+ const licenseFileExists = existsSync(getLicenseFilePath());
89
+ if (isTrialActive(licenseFileExists))
90
+ return 'pro';
91
+ return license.tier; // 'free'
75
92
  }
76
93
  /**
77
94
  * Read the raw license file data (for status display).
@@ -79,9 +96,10 @@ export function getLicenseTier() {
79
96
  */
80
97
  export function getLicenseFile() {
81
98
  try {
82
- if (!existsSync(LICENSE_FILE))
99
+ const licenseFile = getLicenseFilePath();
100
+ if (!existsSync(licenseFile))
83
101
  return null;
84
- return JSON.parse(readFileSync(LICENSE_FILE, 'utf-8'));
102
+ return JSON.parse(readFileSync(licenseFile, 'utf-8'));
85
103
  }
86
104
  catch {
87
105
  return null;
@@ -104,8 +122,10 @@ export function activateLicense(key) {
104
122
  lastValidatedAt: null,
105
123
  validationStatus: 'unvalidated',
106
124
  };
107
- mkdirSync(CONFIG_DIR, { recursive: true });
108
- writeFileSync(LICENSE_FILE, JSON.stringify(file, null, 2) + '\n', { mode: 0o600 });
125
+ const configDir = getConfigDir();
126
+ const licenseFile = getLicenseFilePath();
127
+ mkdirSync(configDir, { recursive: true });
128
+ writeFileSync(licenseFile, JSON.stringify(file, null, 2) + '\n', { mode: 0o600 });
109
129
  // Invalidate cache
110
130
  cachedLicense = info;
111
131
  return info;
@@ -115,14 +135,16 @@ export function activateLicense(key) {
115
135
  */
116
136
  export function deactivateLicense() {
117
137
  try {
118
- if (existsSync(LICENSE_FILE)) {
119
- unlinkSync(LICENSE_FILE);
138
+ const licenseFile = getLicenseFilePath();
139
+ if (existsSync(licenseFile)) {
140
+ unlinkSync(licenseFile);
120
141
  }
121
142
  }
122
143
  catch {
123
144
  // Best effort
124
145
  }
125
146
  cachedLicense = null;
147
+ clearTrialCache();
126
148
  }
127
149
  /**
128
150
  * Update the validation status and timestamp in the license file.
@@ -130,12 +152,13 @@ export function deactivateLicense() {
130
152
  */
131
153
  export function updateValidationStatus(status) {
132
154
  try {
133
- if (!existsSync(LICENSE_FILE))
155
+ const licenseFile = getLicenseFilePath();
156
+ if (!existsSync(licenseFile))
134
157
  return;
135
- const raw = JSON.parse(readFileSync(LICENSE_FILE, 'utf-8'));
158
+ const raw = JSON.parse(readFileSync(licenseFile, 'utf-8'));
136
159
  raw.validationStatus = status;
137
160
  raw.lastValidatedAt = new Date().toISOString();
138
- writeFileSync(LICENSE_FILE, JSON.stringify(raw, null, 2) + '\n', { mode: 0o600 });
161
+ writeFileSync(licenseFile, JSON.stringify(raw, null, 2) + '\n', { mode: 0o600 });
139
162
  // If revoked or expired, invalidate cache so next getLicense() re-verifies
140
163
  if (status === 'revoked' || status === 'expired') {
141
164
  cachedLicense = null;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 14-day Pro trial — auto-granted on first install, no signup required.
3
+ *
4
+ * Trial state is stored in ~/.shieldcortex/trial.json.
5
+ * It is NEVER reset on reinstall (file persists across npm updates).
6
+ * An active paid license always takes priority over the trial.
7
+ */
8
+ export interface TrialFile {
9
+ startedAt: string;
10
+ durationDays: number;
11
+ acknowledged: boolean;
12
+ }
13
+ export interface TrialStatus {
14
+ active: boolean;
15
+ daysRemaining: number;
16
+ startedAt: string;
17
+ expiresAt: string;
18
+ justCreated: boolean;
19
+ }
20
+ /** Clear the in-memory trial cache (useful for testing). */
21
+ export declare function clearTrialCache(): void;
22
+ /**
23
+ * Read (and lazily create) the trial state.
24
+ *
25
+ * - If `trial.json` already exists: read and return its status.
26
+ * - If `trial.json` does NOT exist AND `license.json` does NOT exist: create it now (first install).
27
+ * - If `trial.json` does NOT exist AND `license.json` DOES exist: don't create — user already has a paid license.
28
+ * - Returns null if no trial applies.
29
+ *
30
+ * Pass `licenseFileExists` to avoid a second disk read inside store.ts.
31
+ */
32
+ export declare function getTrialStatus(licenseFileExists: boolean): TrialStatus | null;
33
+ /**
34
+ * Whether a trial is currently active (Pro features unlocked by trial).
35
+ * Only returns true when no paid license is in effect — callers should
36
+ * check the license first and only fall back to this.
37
+ */
38
+ export declare function isTrialActive(licenseFileExists: boolean): boolean;
39
+ /**
40
+ * Days remaining in the trial (0 if expired or no trial).
41
+ */
42
+ export declare function getTrialDaysRemaining(licenseFileExists: boolean): number;
43
+ /**
44
+ * Mark the welcome message as acknowledged (suppress on subsequent runs).
45
+ */
46
+ export declare function acknowledgeTrialWelcome(): void;
47
+ /** Exposed for tests — returns the full trial file path */
48
+ export declare function getTrialFilePath(): string;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * 14-day Pro trial — auto-granted on first install, no signup required.
3
+ *
4
+ * Trial state is stored in ~/.shieldcortex/trial.json.
5
+ * It is NEVER reset on reinstall (file persists across npm updates).
6
+ * An active paid license always takes priority over the trial.
7
+ */
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { homedir } from 'os';
11
+ const TRIAL_DURATION_DAYS = 14;
12
+ // ── Cache ────────────────────────────────────────────────
13
+ let cachedTrial = undefined; // undefined = not loaded yet
14
+ function getConfigDir() {
15
+ return process.env.SHIELDCORTEX_CONFIG_DIR || join(homedir(), '.shieldcortex');
16
+ }
17
+ function getTrialFile() {
18
+ return join(getConfigDir(), 'trial.json');
19
+ }
20
+ /** Clear the in-memory trial cache (useful for testing). */
21
+ export function clearTrialCache() {
22
+ cachedTrial = undefined;
23
+ }
24
+ // ── Helpers ──────────────────────────────────────────────
25
+ function computeStatus(data, justCreated = false) {
26
+ const startMs = new Date(data.startedAt).getTime();
27
+ const expiryMs = startMs + data.durationDays * 24 * 60 * 60 * 1000;
28
+ const nowMs = Date.now();
29
+ const msRemaining = expiryMs - nowMs;
30
+ const daysRemaining = Math.max(0, Math.ceil(msRemaining / (24 * 60 * 60 * 1000)));
31
+ return {
32
+ active: nowMs < expiryMs,
33
+ daysRemaining,
34
+ startedAt: data.startedAt,
35
+ expiresAt: new Date(expiryMs).toISOString(),
36
+ justCreated,
37
+ };
38
+ }
39
+ // ── Public API ────────────────────────────────────────────
40
+ /**
41
+ * Read (and lazily create) the trial state.
42
+ *
43
+ * - If `trial.json` already exists: read and return its status.
44
+ * - If `trial.json` does NOT exist AND `license.json` does NOT exist: create it now (first install).
45
+ * - If `trial.json` does NOT exist AND `license.json` DOES exist: don't create — user already has a paid license.
46
+ * - Returns null if no trial applies.
47
+ *
48
+ * Pass `licenseFileExists` to avoid a second disk read inside store.ts.
49
+ */
50
+ export function getTrialStatus(licenseFileExists) {
51
+ // Skip trial if env var set (useful for tests and CI)
52
+ if (process.env.SHIELDCORTEX_SKIP_TRIAL === '1')
53
+ return null;
54
+ if (cachedTrial !== undefined)
55
+ return cachedTrial;
56
+ try {
57
+ const trialFile = getTrialFile();
58
+ if (existsSync(trialFile)) {
59
+ const data = JSON.parse(readFileSync(trialFile, 'utf-8'));
60
+ // Return full status whether active or expired — callers use status.active to distinguish.
61
+ // Only return null when no trial file exists at all.
62
+ const status = computeStatus(data, false);
63
+ cachedTrial = status;
64
+ return cachedTrial;
65
+ }
66
+ // No trial file — only create on first install (no license either)
67
+ if (licenseFileExists) {
68
+ cachedTrial = null;
69
+ return null;
70
+ }
71
+ // First run — create the trial file
72
+ mkdirSync(getConfigDir(), { recursive: true });
73
+ const now = new Date().toISOString();
74
+ const trialData = {
75
+ startedAt: now,
76
+ durationDays: TRIAL_DURATION_DAYS,
77
+ acknowledged: false,
78
+ };
79
+ writeFileSync(trialFile, JSON.stringify(trialData, null, 2) + '\n', { mode: 0o600 });
80
+ const status = computeStatus(trialData, true);
81
+ cachedTrial = status; // always active on creation
82
+ return cachedTrial;
83
+ }
84
+ catch {
85
+ cachedTrial = null;
86
+ return null;
87
+ }
88
+ }
89
+ /**
90
+ * Whether a trial is currently active (Pro features unlocked by trial).
91
+ * Only returns true when no paid license is in effect — callers should
92
+ * check the license first and only fall back to this.
93
+ */
94
+ export function isTrialActive(licenseFileExists) {
95
+ return getTrialStatus(licenseFileExists)?.active === true;
96
+ }
97
+ /**
98
+ * Days remaining in the trial (0 if expired or no trial).
99
+ */
100
+ export function getTrialDaysRemaining(licenseFileExists) {
101
+ return getTrialStatus(licenseFileExists)?.daysRemaining ?? 0;
102
+ }
103
+ /**
104
+ * Mark the welcome message as acknowledged (suppress on subsequent runs).
105
+ */
106
+ export function acknowledgeTrialWelcome() {
107
+ try {
108
+ const trialFile = getTrialFile();
109
+ if (!existsSync(trialFile))
110
+ return;
111
+ const data = JSON.parse(readFileSync(trialFile, 'utf-8'));
112
+ if (!data.acknowledged) {
113
+ data.acknowledged = true;
114
+ writeFileSync(trialFile, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
115
+ }
116
+ // Update cache
117
+ if (cachedTrial) {
118
+ cachedTrial = { ...cachedTrial, justCreated: false };
119
+ }
120
+ }
121
+ catch {
122
+ // Best effort
123
+ }
124
+ }
125
+ /** Exposed for tests — returns the full trial file path */
126
+ export function getTrialFilePath() {
127
+ return getTrialFile();
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shieldcortex",
3
- "version": "3.4.28",
3
+ "version": "3.4.29",
4
4
  "description": "Trustworthy memory and security for AI agents. Recall debugging, review queue, OpenClaw session capture, and memory poisoning defence for Claude Code, Codex, OpenClaw, LangChain, and MCP agents.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",