@tera-system/pro 0.1.3 → 0.1.4

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.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Build manifest for the Tera System Pro commercial plugin component.",
5
5
  "compatibleEnvironments": [
6
6
  "opencode"
@@ -13,7 +13,7 @@
13
13
  "tools": 7,
14
14
  "projectControlTemplates": 1
15
15
  },
16
- "builtAt": "2026-08-26",
16
+ "builtAt": "2026-08-27",
17
17
  "sha256": {
18
18
  "agents": "ef634a3bd0a1fb3739b59b74b1e58c150a6beed73c48ce48ac12416b15b6a6a6",
19
19
  "commands": "f8c91dab42e41d1f8362edfd816964ceabdbd95e8ebdd02e83ea36f7f805a259",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tera-system/pro",
3
- "version": "0.1.3",
4
- "description": "Tera System Pro أ¢â‚¬â€‌ commercial edition with enforced license gate, server sync, and heartbeat verification",
3
+ "version": "0.1.4",
4
+ "description": "Tera System Pro ط£آ¢أ¢â€ڑآ¬أ¢â‚¬â€Œ commercial edition with enforced license gate, server sync, and heartbeat verification",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "type": "module",
7
7
  "engines": {
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { createHash } from "node:crypto";
23
23
  import fs from "node:fs";
24
+ import os from "node:os";
24
25
  import path from "node:path";
25
26
  import { fileURLToPath } from "node:url";
26
27
  import { assessLicenseState, readLicenseState, writeLicenseState } from "./lib/license.mjs";
@@ -32,6 +33,12 @@ const PKG_ROOT = path.resolve(__dirname, "..");
32
33
  const MANIFEST_NAME = "tera-core.manifest.json";
33
34
  const CONFIG_EXAMPLE = "opencode.tera.example.json";
34
35
 
36
+ /** Generate a stable device id from hostname + target path. */
37
+ function createDeviceId(target) {
38
+ const seed = `${os.hostname()}::${target}`;
39
+ return createHash("sha256").update(seed).digest("hex").slice(0, 16);
40
+ }
41
+
35
42
  /* ---------------------------------- helpers ---------------------------------- */
36
43
 
37
44
  const sha256 = (buf) => createHash("sha256").update(buf).digest("hex");
@@ -243,10 +250,12 @@ const LICENSE_TIMEOUT_MS = 5000; // 5 seconds max for API call
243
250
 
244
251
  /**
245
252
  * Verify license via remote API (online).
253
+ * Requires BOTH licenseKey and accountEmail (the API validates both).
246
254
  * Returns: { ok: boolean, state: string, reason: string, expiresAt?: string, source: "api" }
247
255
  */
248
- async function verifyViaAPI(licenseKey) {
256
+ async function verifyViaAPI(licenseKey, accountEmail) {
249
257
  if (!licenseKey) return { ok: false, state: "Unlicensed", reason: "no-key", source: "api" };
258
+ if (!accountEmail) return { ok: false, state: "Unknown", reason: "no-account-email", source: "api-fallback" };
250
259
 
251
260
  try {
252
261
  const controller = new AbortController();
@@ -255,7 +264,7 @@ async function verifyViaAPI(licenseKey) {
255
264
  const res = await fetch(`${LICENSE_API_URL}/verify`, {
256
265
  method: "POST",
257
266
  headers: { "Content-Type": "application/json" },
258
- body: JSON.stringify({ licenseKey }),
267
+ body: JSON.stringify({ licenseKey, accountEmail }),
259
268
  signal: controller.signal,
260
269
  });
261
270
  clearTimeout(timeout);
@@ -270,7 +279,8 @@ async function verifyViaAPI(licenseKey) {
270
279
  const data = await res.json();
271
280
  return {
272
281
  ok: data.valid === true,
273
- state: data.state || "Invalid",
282
+ // The API does not return a `state` field — derive it from `valid` + `reason`.
283
+ state: data.valid === true ? "Active" : data.state || "Invalid",
274
284
  reason: data.reason || "api-response",
275
285
  expiresAt: data.expiresAt || null,
276
286
  source: "api",
@@ -281,6 +291,29 @@ async function verifyViaAPI(licenseKey) {
281
291
  }
282
292
  }
283
293
 
294
+ /**
295
+ * Try to ACTIVATE the license on this device (best-effort, never breaks install).
296
+ * Requires licenseKey + accountEmail + deviceId.
297
+ */
298
+ async function activateViaAPI(licenseKey, accountEmail, deviceId) {
299
+ if (!licenseKey || !accountEmail || !deviceId) return null;
300
+ try {
301
+ const controller = new AbortController();
302
+ const timeout = setTimeout(() => controller.abort(), LICENSE_TIMEOUT_MS);
303
+ const res = await fetch(`${LICENSE_API_URL}/activate`, {
304
+ method: "POST",
305
+ headers: { "Content-Type": "application/json" },
306
+ body: JSON.stringify({ licenseKey, accountEmail, deviceId }),
307
+ signal: controller.signal,
308
+ });
309
+ clearTimeout(timeout);
310
+ if (!res.ok) return null;
311
+ return await res.json();
312
+ } catch {
313
+ return null;
314
+ }
315
+ }
316
+
284
317
  /**
285
318
  * Verify license offline (Ed25519 signature check).
286
319
  * Returns: { ok: boolean, state: string, reason: string, expiresAt?: string, source: "offline" }
@@ -298,16 +331,16 @@ function verifyOffline(licenseKey) {
298
331
  }
299
332
 
300
333
  /**
301
- * Combined verification: API-first, offline fallback.
334
+ * Combined verification: API-first (with email), offline fallback.
302
335
  * Always returns a result — never throws.
303
336
  */
304
- async function verifyLicense(licenseKey) {
337
+ async function verifyLicense(licenseKey, accountEmail) {
305
338
  // Step 1: Try API (unless explicitly disabled)
306
339
  if (process.env.TERA_OFFLINE_ONLY === "1") {
307
340
  return verifyOffline(licenseKey);
308
341
  }
309
342
 
310
- const apiResult = await verifyViaAPI(licenseKey);
343
+ const apiResult = await verifyViaAPI(licenseKey, accountEmail);
311
344
 
312
345
  // If API succeeded and gave a definitive answer, use it
313
346
  if (apiResult.source === "api" && apiResult.ok !== undefined) {
@@ -379,17 +412,32 @@ async function tryServerSync() {
379
412
  console.log(` [integrity] skipped (${err.message}) — install continues`);
380
413
  }
381
414
 
382
- // Step 3: license verification (API-first, offline fallback)
415
+ // Step 3: license verification (API-first with email, offline fallback)
383
416
  const stateFile = path.join(target, ".tera", "license.state.json");
384
- let licenseKey = (readLicenseState(stateFile) || {}).licenseKey;
417
+ const prevState = readLicenseState(stateFile) || {};
418
+ let licenseKey = prevState.licenseKey;
385
419
  if (!licenseKey) licenseKey = process.env.TERA_LICENSE_KEY || undefined;
420
+ let accountEmail = prevState.accountEmail;
421
+ if (!accountEmail) accountEmail = process.env.TERA_LICENSE_EMAIL || undefined;
422
+ const deviceId = prevState.deviceId || createDeviceId(target);
423
+
424
+ // 3a. best-effort activation (only when we have key + email and not activated yet)
425
+ if (licenseKey && accountEmail && !prevState.activated) {
426
+ const act = await activateViaAPI(licenseKey, accountEmail, deviceId);
427
+ if (act && act.ok === true) {
428
+ console.log(` [license ] ✔ activated on this device (${act.tier} / ${act.project})`);
429
+ }
430
+ }
386
431
 
387
- const result = await verifyLicense(licenseKey);
432
+ const result = await verifyLicense(licenseKey, accountEmail);
388
433
 
389
434
  writeLicenseState(stateFile, {
390
435
  state: result.state,
391
436
  reason: result.reason,
392
437
  licenseKey: licenseKey || null,
438
+ accountEmail: accountEmail || null,
439
+ deviceId,
440
+ activated: true,
393
441
  expiresAt: result.expiresAt || null,
394
442
  graceUntil: result.graceUntil || null,
395
443
  source: result.source,
@@ -22,15 +22,15 @@ const RECHECK_INTERVAL_MS = RECHECK_INTERVAL_DAYS * 86400000;
22
22
  * Try to verify license via API (online check).
23
23
  * Returns null if API unreachable.
24
24
  */
25
- async function verifyViaAPI(licenseKey, apiUrl) {
26
- if (!licenseKey || !apiUrl) return null;
25
+ async function verifyViaAPI(licenseKey, accountEmail, apiUrl) {
26
+ if (!licenseKey || !accountEmail || !apiUrl) return null;
27
27
  try {
28
28
  const controller = new AbortController();
29
29
  const timeout = setTimeout(() => controller.abort(), 5000);
30
30
  const res = await fetch(`${apiUrl}/verify`, {
31
31
  method: "POST",
32
32
  headers: { "Content-Type": "application/json" },
33
- body: JSON.stringify({ licenseKey }),
33
+ body: JSON.stringify({ licenseKey, accountEmail }),
34
34
  signal: controller.signal,
35
35
  });
36
36
  clearTimeout(timeout);
@@ -200,7 +200,7 @@ export async function enforceLicenseGate(target, options = {}) {
200
200
  const recheck = checkRecheckDue(lic);
201
201
  if (recheck.due && !offlineOnly) {
202
202
  // Try API re-verification
203
- const apiResult = await verifyViaAPI(lic.licenseKey, apiUrl);
203
+ const apiResult = await verifyViaAPI(lic.licenseKey, lic.accountEmail, apiUrl);
204
204
  if (apiResult && apiResult.valid === false) {
205
205
  return {
206
206
  allow: false,
@@ -32,15 +32,15 @@ function resolveTarget() {
32
32
  return path.resolve(process.env.TERA_TARGET_DIR || process.env.INIT_CWD || process.cwd());
33
33
  }
34
34
 
35
- async function serverVerify(licenseKey) {
36
- if (!licenseKey) return null;
35
+ async function serverVerify(licenseKey, accountEmail) {
36
+ if (!licenseKey || !accountEmail) return null;
37
37
  try {
38
38
  const controller = new AbortController();
39
39
  const timeout = setTimeout(() => controller.abort(), 8000);
40
40
  const res = await fetch(`${API_URL}/verify`, {
41
41
  method: "POST",
42
42
  headers: { "Content-Type": "application/json" },
43
- body: JSON.stringify({ licenseKey }),
43
+ body: JSON.stringify({ licenseKey, accountEmail }),
44
44
  signal: controller.signal,
45
45
  });
46
46
  clearTimeout(timeout);
@@ -81,7 +81,7 @@ const heartbeatFile = path.join(target, ".tera", "heartbeat.state.json");
81
81
  }
82
82
 
83
83
  console.log(`heartbeat due (last ${hb.lastHeartbeat || "never"}) — contacting server...`);
84
- const server = await serverVerify(license.licenseKey);
84
+ const server = await serverVerify(license.licenseKey, license.accountEmail);
85
85
 
86
86
  if (!server) {
87
87
  console.log(`⚠ server unreachable — keeping current license state (will retry in ${HEARTBEAT_DAYS}d)`);