@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 +2 -2
- package/package.json +2 -2
- package/scripts/install.js +61 -10
- 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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)`);
|