@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 +2 -2
- package/package.json +2 -2
- package/scripts/install.js +57 -9
- package/scripts/lib/license-gate.mjs +4 -4
- package/scripts/tera-heartbeat.mjs +4 -4
package/MANIFEST.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tera-system/pro",
|
|
3
|
-
"version": "0.1.
|
|
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-
|
|
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.
|
|
4
|
-
"description": "Tera System Pro
|
|
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": {
|
package/scripts/install.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)`);
|