@tera-system/pro 0.2.11 → 0.2.12

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/MANIFEST.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tera-system/pro",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Build manifest for the Tera System Pro commercial plugin component (THIN mode — content served from teranoo.com signed bundle).",
5
5
  "compatibleEnvironments": [
6
6
  "opencode"
package/RELEASES.md CHANGED
@@ -6,6 +6,20 @@
6
6
 
7
7
 
8
8
 
9
+
10
+ ## 0.2.12 (2026-08-30)
11
+
12
+ ### Bug fix: preflight false tamper detection (SCP-2026-08-30-002)
13
+
14
+ **What changed (scripts only - bundle unchanged):**
15
+ - integrity.mjs: removed .tera/license.state.json from CRITICAL_FILES - it is a self-modifying STATE file (checkedAt changes on every install/heartbeat/re-check). License security comes from the Ed25519-signed key, not from hashing the state file.
16
+ - install.js: license state is now written BEFORE the integrity snapshot, so the manifest always reflects the final on-disk state.
17
+ - tera-license.mjs: added the 'activate <key> [email]' command (TERA_LICENSE_GUARD.md instructs blocked users to run it - it did not exist).
18
+
19
+ **Why:**
20
+ - preflight.mjs always failed with "Tampered files: .tera/license.state.json" because the install wrote the state file after the snapshot (0.36s gap) - verified and reproduced.
21
+ - No bundle change: the fix is entirely in npm-shipped scripts. Server bundle stays v0.2.11.
22
+
9
23
  ## 0.2.11 (2026-08-30)
10
24
 
11
25
  ### Security hardening: license-gate message fix + zero core traces
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tera-system/pro",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Tera System Pro — commercial edition (thin): system files are fetched from teranoo.com as a signed bundle; enforced license gate + heartbeat",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "type": "module",
@@ -445,16 +445,9 @@ async function tryServerSync() {
445
445
  console.log(" npm run tera-sync (or re-run install when online)");
446
446
  }
447
447
 
448
- // Step 2: integrity manifest (after sync, covers server files too)
449
- try {
450
- const integrityManifest = createIntegrityManifest(target);
451
- saveIntegrityManifest(target, integrityManifest);
452
- console.log(` [integrity] manifest created (${integrityManifest.fileCount} files tracked)`);
453
- } catch (err) {
454
- console.log(` [integrity] skipped (${err.message}) — install continues`);
455
- }
456
-
457
- // Step 3: license verification (API-first with email, offline fallback)
448
+ // Step 2: license verification (API-first with email, offline fallback)
449
+ // NOTE: license state is written BEFORE the integrity snapshot (SCP-2026-08-30-002)
450
+ // so the manifest always reflects the final on-disk state.
458
451
  const stateFile = path.join(target, ".tera", "license.state.json");
459
452
  const prevState = readLicenseState(stateFile) || {};
460
453
  let licenseKey = prevState.licenseKey;
@@ -463,7 +456,7 @@ async function tryServerSync() {
463
456
  if (!accountEmail) accountEmail = process.env.TERA_LICENSE_EMAIL || undefined;
464
457
  const deviceId = prevState.deviceId || createDeviceId(target);
465
458
 
466
- // 3a. best-effort activation (only when we have key + email and not activated yet)
459
+ // 2a. best-effort activation (only when we have key + email and not activated yet)
467
460
  if (licenseKey && accountEmail && !prevState.activated) {
468
461
  const act = await activateViaAPI(licenseKey, accountEmail, deviceId);
469
462
  if (act && act.ok === true) {
@@ -488,6 +481,15 @@ async function tryServerSync() {
488
481
 
489
482
  const sourceTag = result.source === "api" ? "☁️" : result.source === "offline" ? "🔒" : "🔄";
490
483
  console.log(` [license ] ${sourceTag} state=${result.state} (${result.reason})`);
484
+
485
+ // Step 3: integrity manifest (AFTER all writes — snapshot reflects final state)
486
+ try {
487
+ const integrityManifest = createIntegrityManifest(target);
488
+ saveIntegrityManifest(target, integrityManifest);
489
+ console.log(` [integrity] manifest created (${integrityManifest.fileCount} files tracked)`);
490
+ } catch (err) {
491
+ console.log(` [integrity] skipped (${err.message}) — install continues`);
492
+ }
491
493
  } catch (err) {
492
494
  console.log(` [license ] skipped (${err.message}) — install continues`);
493
495
  }
@@ -12,7 +12,9 @@ const CRITICAL_FILES = [
12
12
  "scripts/lib/integrity.mjs",
13
13
  "scripts/install.js",
14
14
  "scripts/preflight.mjs",
15
- ".tera/license.state.json",
15
+ // NOTE: .tera/license.state.json is intentionally NOT hashed (SCP-2026-08-30-002).
16
+ // It is a self-modifying STATE file (checkedAt written on every install/heartbeat/re-check).
17
+ // License security comes from the Ed25519-signed key, not from hashing the state file.
16
18
  ];
17
19
 
18
20
  /**
@@ -13,7 +13,10 @@ function usage() {
13
13
  tera-license gen --tier <basic|premium> --project <name> --expires <YYYY-MM-DD> [--seats N]
14
14
  (private key: env TERA_LICENSE_PRIVATE or the dev key under license/dev/)
15
15
  tera-license check [--key <licenseKey> | --state-file <path>]
16
- (or env TERA_LICENSE_KEY)`);
16
+ (or env TERA_LICENSE_KEY)
17
+ tera-license activate <licenseKey> [accountEmail]
18
+ (writes the key into .tera/license.state.json and verifies it)
19
+ (target: env TERA_TARGET_DIR / INIT_CWD / cwd)`);
17
20
  }
18
21
 
19
22
  const args = process.argv.slice(2);
@@ -58,6 +61,31 @@ if (cmd === "gen") {
58
61
  process.exit(0);
59
62
  }
60
63
  console.log(JSON.stringify(assessLicenseState(key), null, 2));
64
+ } else if (cmd === "activate") {
65
+ // Write a license key into .tera/license.state.json and verify it (SCP-2026-08-30-002).
66
+ const key = args[1] || flag("--key");
67
+ if (!key) { usage(); process.exit(1); }
68
+ const email = args[2] || flag("--email") || process.env.TERA_LICENSE_EMAIL || null;
69
+ const target = path.resolve(process.env.TERA_TARGET_DIR || process.env.INIT_CWD || process.cwd());
70
+ const stateFile = path.join(target, ".tera", "license.state.json");
71
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true });
72
+ const prev = readLicenseState(stateFile) || {};
73
+ const assessment = assessLicenseState(key);
74
+ const state = {
75
+ ...prev,
76
+ state: assessment.state,
77
+ reason: assessment.reason,
78
+ licenseKey: key,
79
+ accountEmail: email || prev.accountEmail || null,
80
+ deviceId: prev.deviceId || null,
81
+ activated: true,
82
+ expiresAt: assessment.expiresAt || null,
83
+ graceUntil: assessment.graceUntil || null,
84
+ checkedAt: new Date().toISOString(),
85
+ };
86
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
87
+ console.log(`Activated: state=${assessment.state} (${assessment.reason}) — ${stateFile}`);
88
+ process.exit(assessment.state === "Active" || assessment.state === "Grace" ? 0 : 1);
61
89
  } else {
62
90
  usage();
63
91
  }