@tera-system/pro 0.1.2 → 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.2",
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.2",
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,19 +264,23 @@ 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);
262
271
 
263
272
  if (!res.ok) {
264
- return { ok: false, state: "Invalid", reason: `api-http-${res.status}`, source: "api" };
273
+ // Server responded with an error status (e.g. 404 while the endpoint is
274
+ // not implemented yet, or 5xx). This is NOT a definitive license verdict —
275
+ // treat it as "API unavailable" so the offline Ed25519 fallback runs.
276
+ return { ok: false, state: "Unknown", reason: `api-http-${res.status}`, source: "api-fallback" };
265
277
  }
266
278
 
267
279
  const data = await res.json();
268
280
  return {
269
281
  ok: data.valid === true,
270
- 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",
271
284
  reason: data.reason || "api-response",
272
285
  expiresAt: data.expiresAt || null,
273
286
  source: "api",
@@ -278,6 +291,29 @@ async function verifyViaAPI(licenseKey) {
278
291
  }
279
292
  }
280
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
+
281
317
  /**
282
318
  * Verify license offline (Ed25519 signature check).
283
319
  * Returns: { ok: boolean, state: string, reason: string, expiresAt?: string, source: "offline" }
@@ -295,16 +331,16 @@ function verifyOffline(licenseKey) {
295
331
  }
296
332
 
297
333
  /**
298
- * Combined verification: API-first, offline fallback.
334
+ * Combined verification: API-first (with email), offline fallback.
299
335
  * Always returns a result — never throws.
300
336
  */
301
- async function verifyLicense(licenseKey) {
337
+ async function verifyLicense(licenseKey, accountEmail) {
302
338
  // Step 1: Try API (unless explicitly disabled)
303
339
  if (process.env.TERA_OFFLINE_ONLY === "1") {
304
340
  return verifyOffline(licenseKey);
305
341
  }
306
342
 
307
- const apiResult = await verifyViaAPI(licenseKey);
343
+ const apiResult = await verifyViaAPI(licenseKey, accountEmail);
308
344
 
309
345
  // If API succeeded and gave a definitive answer, use it
310
346
  if (apiResult.source === "api" && apiResult.ok !== undefined) {
@@ -376,17 +412,32 @@ async function tryServerSync() {
376
412
  console.log(` [integrity] skipped (${err.message}) — install continues`);
377
413
  }
378
414
 
379
- // Step 3: license verification (API-first, offline fallback)
415
+ // Step 3: license verification (API-first with email, offline fallback)
380
416
  const stateFile = path.join(target, ".tera", "license.state.json");
381
- let licenseKey = (readLicenseState(stateFile) || {}).licenseKey;
417
+ const prevState = readLicenseState(stateFile) || {};
418
+ let licenseKey = prevState.licenseKey;
382
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
+ }
383
431
 
384
- const result = await verifyLicense(licenseKey);
432
+ const result = await verifyLicense(licenseKey, accountEmail);
385
433
 
386
434
  writeLicenseState(stateFile, {
387
435
  state: result.state,
388
436
  reason: result.reason,
389
437
  licenseKey: licenseKey || null,
438
+ accountEmail: accountEmail || null,
439
+ deviceId,
440
+ activated: true,
390
441
  expiresAt: result.expiresAt || null,
391
442
  graceUntil: result.graceUntil || null,
392
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)`);