sdtk-kit 1.6.0 → 1.7.1
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/README.md +1 -1
- package/assets/keys/sdtk-entitlement-public.pem +9 -0
- package/bin/sdtk.js +21 -0
- package/package.json +2 -1
- package/src/commands/activate.js +62 -0
- package/src/lib/license-activation.js +495 -0
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ are the mechanism that puts `sdtk`, `sdtk-spec`, `sdtk-code`, `sdtk-ops`,
|
|
|
119
119
|
- **Major version bumps** in any sub-toolkit require a coordinated `sdtk-kit` major-bump and re-publish.
|
|
120
120
|
- If you need exact version control per toolkit, use standalone packages instead.
|
|
121
121
|
|
|
122
|
-
Current dependency ranges (as of sdtk-kit v1.
|
|
122
|
+
Current dependency ranges (as of sdtk-kit v1.7.1):
|
|
123
123
|
|
|
124
124
|
| Package | Version |
|
|
125
125
|
|------------------|---------|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-----BEGIN PUBLIC KEY-----
|
|
2
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuxpjOnU2tGqY4jqQIWWQ
|
|
3
|
+
mMTStgAAri8UguT+okkLEOjPav+bnHU2brobXadkVvsiX5P0cSjvjKSCdxjD97uM
|
|
4
|
+
MCRhCzhSJ27cHkWdXzlREpI2xUaQK78Mmv712zuCzoQbNp4cKcDTxXFsaAdJd5S8
|
|
5
|
+
0S88lPdiWFv6DI+tN1t0U7Lv3v3iIEtHdzRkYNxX958tHHuZWIrvJ6oQushZLngy
|
|
6
|
+
/WpD9L3/ZraAtf+eBcK4bVc5nvcdvoQx+OEygr0tfRJlFqRp/cHkz/MyHSu7yXWa
|
|
7
|
+
NtUD/zYb9jnN/DmG5O3kTD8iIQYzBErAzQ9bfMGm0J4QgTctxZLdDCy9zpBeNXva
|
|
8
|
+
QwIDAQAB
|
|
9
|
+
-----END PUBLIC KEY-----
|
package/bin/sdtk.js
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
// `sdtk` — unified entry point for the SDTK suite (BK-268).
|
|
5
5
|
//
|
|
6
6
|
// sdtk init --runtime <claude|codex> one-command setup for all five toolkits
|
|
7
|
+
// sdtk activate --license <KEY> activate SDTK Pro (unlocks all premium features)
|
|
8
|
+
// sdtk update [--check-only] update all five toolkit CLIs
|
|
7
9
|
// sdtk --help top-level help
|
|
8
10
|
// sdtk --version sdtk-kit version + resolved per-kit versions
|
|
9
11
|
// sdtk no args → help
|
|
10
12
|
//
|
|
11
13
|
// `init` delegates to each toolkit's own shipped init (see src/lib/unified-init.js).
|
|
14
|
+
// `activate` calls the backend activation API and installs the entitlement + packs.
|
|
12
15
|
|
|
13
16
|
const SUB_KITS = [
|
|
14
17
|
"sdtk-spec-kit",
|
|
@@ -23,11 +26,17 @@ function helpText() {
|
|
|
23
26
|
"sdtk — unified SDTK suite CLI",
|
|
24
27
|
"",
|
|
25
28
|
"Usage:",
|
|
29
|
+
" sdtk activate --license <KEY> Activate SDTK Pro (all premium features)",
|
|
26
30
|
" sdtk init --runtime <claude|codex> [options] Initialise all five toolkits",
|
|
27
31
|
" sdtk update [--check-only] Update all five toolkit CLIs",
|
|
28
32
|
" sdtk --help Show this help",
|
|
29
33
|
" sdtk --version Show versions",
|
|
30
34
|
"",
|
|
35
|
+
"activate options:",
|
|
36
|
+
" --license <KEY> Required. License key in SDTK-XXXX-YYYY format.",
|
|
37
|
+
" --json Output result as JSON.",
|
|
38
|
+
" --activation-url <url> Override activation API endpoint (advanced).",
|
|
39
|
+
"",
|
|
31
40
|
"init options:",
|
|
32
41
|
" --runtime <claude|codex> Required. Runtime for the runtime-aware kits",
|
|
33
42
|
" (spec/ops/code and design place skills under",
|
|
@@ -101,6 +110,18 @@ function main(argv) {
|
|
|
101
110
|
return cmdUpdate(argv.slice(1));
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
if (command === "activate") {
|
|
114
|
+
// eslint-disable-next-line global-require
|
|
115
|
+
const { cmdActivate } = require("../src/commands/activate");
|
|
116
|
+
cmdActivate(argv.slice(1)).then((code) => {
|
|
117
|
+
process.exitCode = code;
|
|
118
|
+
}).catch((err) => {
|
|
119
|
+
console.error(`sdtk activate: unexpected error: ${err.message}`);
|
|
120
|
+
process.exitCode = 2;
|
|
121
|
+
});
|
|
122
|
+
return 0; // exitCode will be overwritten by the Promise above
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
console.error(`sdtk: unknown command '${command}'.`);
|
|
105
126
|
console.error("Run `sdtk --help` for usage.");
|
|
106
127
|
return 2;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdtk-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Install all five SDTK toolkits in one command. Meta-package for sdtk-spec-kit, sdtk-code-kit, sdtk-ops-kit, sdtk-design-kit, and sdtk-wiki-kit.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"bin/",
|
|
16
16
|
"src/",
|
|
17
17
|
"scripts/",
|
|
18
|
+
"assets/",
|
|
18
19
|
"README.md",
|
|
19
20
|
"LICENSE"
|
|
20
21
|
],
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { activateWithLicense } = require("../lib/license-activation");
|
|
4
|
+
|
|
5
|
+
function parseActivateArgs(args) {
|
|
6
|
+
let licenseKey = null;
|
|
7
|
+
let json = false;
|
|
8
|
+
let activationUrl;
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
const arg = args[i];
|
|
12
|
+
if (arg === "--license") {
|
|
13
|
+
if (i + 1 >= args.length) throw new Error("--license requires a value");
|
|
14
|
+
licenseKey = args[++i];
|
|
15
|
+
} else if (arg === "--json") {
|
|
16
|
+
json = true;
|
|
17
|
+
} else if (arg === "--activation-url") {
|
|
18
|
+
if (i + 1 >= args.length) throw new Error("--activation-url requires a value");
|
|
19
|
+
activationUrl = args[++i];
|
|
20
|
+
} else {
|
|
21
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { licenseKey, json, activationUrl };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function cmdActivate(args) {
|
|
29
|
+
let options;
|
|
30
|
+
try {
|
|
31
|
+
options = parseActivateArgs(args);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(`Error: ${err.message}`);
|
|
34
|
+
console.error("Usage: sdtk activate --license <KEY> [--json]");
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!options.licenseKey) {
|
|
39
|
+
console.error("Error: --license is required");
|
|
40
|
+
console.error("Usage: sdtk activate --license <KEY> [--json]");
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await activateWithLicense({
|
|
45
|
+
licenseKey: options.licenseKey,
|
|
46
|
+
activationUrl: options.activationUrl,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (options.json) {
|
|
50
|
+
console.log(JSON.stringify({ success: result.success, decision: result.decision, message: result.message }, null, 2));
|
|
51
|
+
} else {
|
|
52
|
+
if (result.success) {
|
|
53
|
+
console.log(result.message);
|
|
54
|
+
} else {
|
|
55
|
+
console.error(result.message);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result.exitCode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { cmdActivate };
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Umbrella `sdtk activate` implementation.
|
|
5
|
+
*
|
|
6
|
+
* Ported from sdtk-spec-kit/src/lib/license-activation.js with two key changes:
|
|
7
|
+
* 1. Installs ALL packs from the activation response (not just product === "spec").
|
|
8
|
+
* 2. Pack directories use the pack's own `product` field dynamically so each
|
|
9
|
+
* kit's loader can find its pack at the expected path
|
|
10
|
+
* (~/.sdtk/packs/{product}/{id}/{version}/).
|
|
11
|
+
*
|
|
12
|
+
* State paths are shared with the other kits:
|
|
13
|
+
* ~/.sdtk/entitlements.json — signed manifest (verified before write)
|
|
14
|
+
* ~/.sdtk/activation.json — lightweight activation metadata
|
|
15
|
+
* ~/.sdtk/packs/ — pack cache root
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const os = require("os");
|
|
20
|
+
const crypto = require("crypto");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Suite-state paths (mirrors sdtk-spec-kit/src/lib/suite-state.js)
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
function getSuiteRoot() {
|
|
28
|
+
return path.join(os.homedir(), ".sdtk");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getEntitlementsFile() {
|
|
32
|
+
return path.join(getSuiteRoot(), "entitlements.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getActivationFile() {
|
|
36
|
+
return path.join(getSuiteRoot(), "activation.json");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getPacksRoot() {
|
|
40
|
+
return path.join(getSuiteRoot(), "packs");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ensureDir(dirPath) {
|
|
44
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hardenFilePermissions(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
if (process.platform === "win32") {
|
|
50
|
+
const { execFileSync } = require("child_process");
|
|
51
|
+
const username = os.userInfo().username;
|
|
52
|
+
execFileSync("icacls", [filePath, "/inheritance:r", "/grant:r", `${username}:(F)`], {
|
|
53
|
+
stdio: "ignore",
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
fs.chmodSync(filePath, 0o600);
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
} catch (_e) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Public key resolution
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
function resolvePublicKey() {
|
|
69
|
+
if (process.env.SDTK_ENTITLEMENT_PUBLIC_KEY) {
|
|
70
|
+
return process.env.SDTK_ENTITLEMENT_PUBLIC_KEY;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const bundledKeyPath = path.join(
|
|
74
|
+
__dirname, "..", "..", "assets", "keys", "sdtk-entitlement-public.pem"
|
|
75
|
+
);
|
|
76
|
+
if (fs.existsSync(bundledKeyPath)) {
|
|
77
|
+
return fs.readFileSync(bundledKeyPath, "utf8");
|
|
78
|
+
}
|
|
79
|
+
} catch (_e) {
|
|
80
|
+
// fall through
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Manifest canonicalization + verification
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
function canonicalize(value) {
|
|
90
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
91
|
+
if (value !== null && typeof value === "object") {
|
|
92
|
+
const sorted = {};
|
|
93
|
+
for (const key of Object.keys(value).sort()) {
|
|
94
|
+
sorted[key] = canonicalize(value[key]);
|
|
95
|
+
}
|
|
96
|
+
return sorted;
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildCanonicalPayload(manifest) {
|
|
102
|
+
const { signature: _sig, ...rest } = manifest;
|
|
103
|
+
return Buffer.from(JSON.stringify(canonicalize(rest)), "utf8");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const REQUIRED_MANIFEST_FIELDS = [
|
|
107
|
+
"schema_version", "customer_id", "plan", "products", "capabilities",
|
|
108
|
+
"issued_at", "expires_at", "offline_grace_until",
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
function evaluateManifestObject(manifest, now) {
|
|
112
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
113
|
+
return { state: "malformed", manifest: null };
|
|
114
|
+
}
|
|
115
|
+
for (const field of REQUIRED_MANIFEST_FIELDS) {
|
|
116
|
+
if (!(field in manifest)) return { state: "malformed", manifest };
|
|
117
|
+
}
|
|
118
|
+
if (typeof manifest.schema_version !== "number") return { state: "malformed", manifest };
|
|
119
|
+
if (typeof manifest.customer_id !== "string" || !manifest.customer_id) return { state: "malformed", manifest };
|
|
120
|
+
if (typeof manifest.plan !== "string" || !manifest.plan) return { state: "malformed", manifest };
|
|
121
|
+
if (!manifest.products || typeof manifest.products !== "object" || Array.isArray(manifest.products)) return { state: "malformed", manifest };
|
|
122
|
+
if (!Array.isArray(manifest.capabilities)) return { state: "malformed", manifest };
|
|
123
|
+
if (typeof manifest.issued_at !== "string" || typeof manifest.expires_at !== "string" || typeof manifest.offline_grace_until !== "string") return { state: "malformed", manifest };
|
|
124
|
+
if (!("signature" in manifest) || !manifest.signature || typeof manifest.signature !== "string") return { state: "unsigned", manifest };
|
|
125
|
+
|
|
126
|
+
const publicKeyPem = resolvePublicKey();
|
|
127
|
+
if (!publicKeyPem) return { state: "untrusted-key", manifest };
|
|
128
|
+
|
|
129
|
+
let verified = false;
|
|
130
|
+
try {
|
|
131
|
+
const verifier = crypto.createVerify("RSA-SHA256");
|
|
132
|
+
verifier.update(buildCanonicalPayload(manifest));
|
|
133
|
+
verified = verifier.verify(publicKeyPem, Buffer.from(manifest.signature, "base64"));
|
|
134
|
+
} catch (_e) {
|
|
135
|
+
return { state: "invalid-signature", manifest };
|
|
136
|
+
}
|
|
137
|
+
if (!verified) return { state: "invalid-signature", manifest };
|
|
138
|
+
|
|
139
|
+
const expiresAt = new Date(manifest.expires_at).getTime();
|
|
140
|
+
const graceUntil = new Date(manifest.offline_grace_until).getTime();
|
|
141
|
+
if (isNaN(expiresAt) || isNaN(graceUntil)) return { state: "malformed", manifest };
|
|
142
|
+
|
|
143
|
+
const nowMs = now !== undefined ? now : Date.now();
|
|
144
|
+
if (nowMs < expiresAt) return { state: "active", manifest };
|
|
145
|
+
if (nowMs < graceUntil) return { state: "grace", manifest };
|
|
146
|
+
return { state: "expired", manifest };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Pack installation (umbrella version: dynamic product, no product filter)
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
const SAFE_PACK_PART_RE = /^[A-Za-z0-9._-]+$/;
|
|
154
|
+
|
|
155
|
+
function isSafePackPathPart(value) {
|
|
156
|
+
return typeof value === "string" && SAFE_PACK_PART_RE.test(value) && value !== "." && value !== "..";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const REQUIRED_PACK_FIELDS = ["id", "product", "version", "capabilities", "source", "sha256"];
|
|
160
|
+
|
|
161
|
+
function validatePackMetaForInstall(meta) {
|
|
162
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
163
|
+
return { valid: false, reason: "pack metadata is not an object" };
|
|
164
|
+
}
|
|
165
|
+
for (const field of REQUIRED_PACK_FIELDS) {
|
|
166
|
+
if (!(field in meta)) return { valid: false, reason: `missing required field: ${field}` };
|
|
167
|
+
}
|
|
168
|
+
if (!isSafePackPathPart(meta.id)) return { valid: false, reason: "id must be a safe identifier" };
|
|
169
|
+
if (!isSafePackPathPart(meta.product)) return { valid: false, reason: "product must be a safe identifier" };
|
|
170
|
+
if (!isSafePackPathPart(meta.version)) return { valid: false, reason: "version must be a safe identifier" };
|
|
171
|
+
if (!Array.isArray(meta.capabilities)) return { valid: false, reason: "capabilities must be an array" };
|
|
172
|
+
if (!meta.source || typeof meta.source !== "object" || Array.isArray(meta.source) || meta.source.type !== "github-content") {
|
|
173
|
+
return { valid: false, reason: 'source.type must be "github-content"' };
|
|
174
|
+
}
|
|
175
|
+
if (typeof meta.source.path !== "string" || !meta.source.path) {
|
|
176
|
+
return { valid: false, reason: "source.path must be a non-empty string" };
|
|
177
|
+
}
|
|
178
|
+
if (typeof meta.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(meta.sha256)) {
|
|
179
|
+
return { valid: false, reason: "sha256 must be a 64-character lowercase hex string" };
|
|
180
|
+
}
|
|
181
|
+
return { valid: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function computeSha256(bytes) {
|
|
185
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getPackDir(product, packId, packVersion) {
|
|
189
|
+
return path.join(getPacksRoot(), product, packId, packVersion);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function writePackToCache(packMeta, packBytes) {
|
|
193
|
+
const validation = validatePackMetaForInstall(packMeta);
|
|
194
|
+
if (!validation.valid) {
|
|
195
|
+
return { success: false, reason: `Invalid pack metadata: ${validation.reason}` };
|
|
196
|
+
}
|
|
197
|
+
if (!Buffer.isBuffer(packBytes)) {
|
|
198
|
+
return { success: false, reason: "packBytes must be a Buffer" };
|
|
199
|
+
}
|
|
200
|
+
if (computeSha256(packBytes) !== packMeta.sha256) {
|
|
201
|
+
return { success: false, reason: `SHA256 mismatch for pack "${packMeta.id}@${packMeta.version}"` };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const packDir = getPackDir(packMeta.product, packMeta.id, packMeta.version);
|
|
205
|
+
try {
|
|
206
|
+
ensureDir(packDir);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
return { success: false, reason: `Failed to create pack dir: ${err.message}` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const tmpFile = path.join(packDir, `.pack.js.${process.pid}.${Date.now()}.tmp`);
|
|
212
|
+
try {
|
|
213
|
+
fs.writeFileSync(tmpFile, packBytes);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
return { success: false, reason: `Failed to write pack temp file: ${err.message}` };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const finalPackFile = path.join(packDir, "pack.js");
|
|
219
|
+
try {
|
|
220
|
+
if (fs.existsSync(finalPackFile)) fs.unlinkSync(finalPackFile);
|
|
221
|
+
fs.renameSync(tmpFile, finalPackFile);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
try { fs.unlinkSync(tmpFile); } catch (_) {}
|
|
224
|
+
return { success: false, reason: `Failed to move pack to cache: ${err.message}` };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
fs.writeFileSync(
|
|
229
|
+
path.join(packDir, "pack.json"),
|
|
230
|
+
JSON.stringify({
|
|
231
|
+
id: packMeta.id,
|
|
232
|
+
product: packMeta.product,
|
|
233
|
+
version: packMeta.version,
|
|
234
|
+
capabilities: packMeta.capabilities,
|
|
235
|
+
sha256: packMeta.sha256,
|
|
236
|
+
source: packMeta.source,
|
|
237
|
+
installed_at: new Date().toISOString(),
|
|
238
|
+
entrypoint: "pack.js",
|
|
239
|
+
}, null, 2),
|
|
240
|
+
"utf8"
|
|
241
|
+
);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return { success: false, reason: `Failed to write pack.json: ${err.message}` };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { success: true };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function installAllPacks(premiumPacks) {
|
|
250
|
+
const errors = [];
|
|
251
|
+
if (!Array.isArray(premiumPacks)) return { success: true, errors: [] };
|
|
252
|
+
|
|
253
|
+
for (const packEntry of premiumPacks) {
|
|
254
|
+
if (!packEntry || !packEntry.bytes || typeof packEntry.bytes !== "string") continue;
|
|
255
|
+
|
|
256
|
+
const validation = validatePackMetaForInstall(packEntry);
|
|
257
|
+
if (!validation.valid) {
|
|
258
|
+
errors.push(`Pack "${packEntry && packEntry.id || "unknown"}": ${validation.reason}`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let packBytes;
|
|
263
|
+
try {
|
|
264
|
+
packBytes = Buffer.from(packEntry.bytes, "base64");
|
|
265
|
+
} catch (err) {
|
|
266
|
+
errors.push(`Pack "${packEntry.id}": Failed to decode bytes: ${err.message}`);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const writeResult = writePackToCache(packEntry, packBytes);
|
|
271
|
+
if (!writeResult.success) {
|
|
272
|
+
errors.push(`Pack "${packEntry.id}": ${writeResult.reason}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { success: errors.length === 0, errors };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Activation state persistence
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
function persistEntitlementAndActivation(manifest, activation) {
|
|
284
|
+
const entitlementsFile = getEntitlementsFile();
|
|
285
|
+
try {
|
|
286
|
+
ensureDir(path.dirname(entitlementsFile));
|
|
287
|
+
fs.writeFileSync(entitlementsFile, JSON.stringify(manifest, null, 2), "utf8");
|
|
288
|
+
hardenFilePermissions(entitlementsFile);
|
|
289
|
+
|
|
290
|
+
const activationFile = getActivationFile();
|
|
291
|
+
ensureDir(path.dirname(activationFile));
|
|
292
|
+
fs.writeFileSync(
|
|
293
|
+
activationFile,
|
|
294
|
+
JSON.stringify({
|
|
295
|
+
provider: "license",
|
|
296
|
+
customer_id: manifest.customer_id,
|
|
297
|
+
plan: manifest.plan,
|
|
298
|
+
machine_id: activation.machine_id,
|
|
299
|
+
decision: activation.decision || "activated",
|
|
300
|
+
activated_at: new Date().toISOString(),
|
|
301
|
+
}, null, 2),
|
|
302
|
+
"utf8"
|
|
303
|
+
);
|
|
304
|
+
hardenFilePermissions(activationFile);
|
|
305
|
+
return { success: true };
|
|
306
|
+
} catch (err) {
|
|
307
|
+
return { success: false, error: err.message };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function rollbackPartialState() {
|
|
312
|
+
try {
|
|
313
|
+
const activationFile = getActivationFile();
|
|
314
|
+
if (fs.existsSync(activationFile)) fs.unlinkSync(activationFile);
|
|
315
|
+
} catch (_e) {}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Machine ID
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
function buildMachineId() {
|
|
323
|
+
const combined = `${os.hostname()}-${process.platform}-${os.cpus().length}`;
|
|
324
|
+
return crypto.createHash("sha256").update(combined).digest("hex").substring(0, 16);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// HTTP activation request
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
async function postActivationRequest({ activationUrl, licenseKey, machineId }) {
|
|
332
|
+
const pkg = require("../../package.json");
|
|
333
|
+
const body = {
|
|
334
|
+
license_key: licenseKey,
|
|
335
|
+
machine_id: machineId,
|
|
336
|
+
cli_version: pkg.version,
|
|
337
|
+
os: process.platform,
|
|
338
|
+
hostname: os.hostname(),
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetch(activationUrl, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
headers: { "Content-Type": "application/json" },
|
|
345
|
+
body: JSON.stringify(body),
|
|
346
|
+
});
|
|
347
|
+
const rawText = await response.text();
|
|
348
|
+
let data = null;
|
|
349
|
+
try { if (rawText) data = JSON.parse(rawText); } catch (_) {}
|
|
350
|
+
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
const deniedReason =
|
|
353
|
+
data && data.decision === "denied" && data.manifest && typeof data.manifest.error === "string"
|
|
354
|
+
? data.manifest.error : null;
|
|
355
|
+
return {
|
|
356
|
+
success: false,
|
|
357
|
+
error: deniedReason || (data && typeof data.error === "string" ? data.error : null) ||
|
|
358
|
+
`Activation API returned status ${response.status}`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (!data || typeof data !== "object") {
|
|
362
|
+
return { success: false, error: "Activation API returned a non-JSON response" };
|
|
363
|
+
}
|
|
364
|
+
return { success: true, response: data };
|
|
365
|
+
} catch (err) {
|
|
366
|
+
return { success: false, error: `Network error: ${err.message}` };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Response validation
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
function validateActivationResponse(response) {
|
|
375
|
+
if (!response || typeof response !== "object") {
|
|
376
|
+
return { valid: false, reason: "Response is not an object" };
|
|
377
|
+
}
|
|
378
|
+
if (!response.decision || !["activated", "already_activated", "denied"].includes(response.decision)) {
|
|
379
|
+
return { valid: false, reason: "Invalid decision in response" };
|
|
380
|
+
}
|
|
381
|
+
if (!response.manifest || typeof response.manifest !== "object") {
|
|
382
|
+
return { valid: false, reason: "Response is missing manifest" };
|
|
383
|
+
}
|
|
384
|
+
if (response.decision === "denied") {
|
|
385
|
+
return { valid: false, reason: response.manifest.error || "Activation was denied by server" };
|
|
386
|
+
}
|
|
387
|
+
if (!response.activation || typeof response.activation !== "object") {
|
|
388
|
+
return { valid: false, reason: "Response is missing activation metadata" };
|
|
389
|
+
}
|
|
390
|
+
if (typeof response.activation.machine_id !== "string") {
|
|
391
|
+
return { valid: false, reason: "Response activation.machine_id is invalid" };
|
|
392
|
+
}
|
|
393
|
+
return { valid: true };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Main activation flow
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
async function activateWithLicense({ licenseKey, activationUrl } = {}) {
|
|
401
|
+
if (!activationUrl) {
|
|
402
|
+
activationUrl = process.env.SDTK_ACTIVATION_URL || "https://sdtk.dev/api/activate";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const result = { success: false, message: "", exitCode: 1 };
|
|
406
|
+
|
|
407
|
+
if (!licenseKey || typeof licenseKey !== "string" || !licenseKey.trim()) {
|
|
408
|
+
result.message = "License key is required. Usage: sdtk activate --license <KEY>";
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const machineId = buildMachineId();
|
|
413
|
+
|
|
414
|
+
const apiResult = await postActivationRequest({
|
|
415
|
+
activationUrl,
|
|
416
|
+
licenseKey: licenseKey.trim(),
|
|
417
|
+
machineId,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (!apiResult.success) {
|
|
421
|
+
result.message = `Activation failed: ${apiResult.error}`;
|
|
422
|
+
rollbackPartialState();
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const validation = validateActivationResponse(apiResult.response);
|
|
427
|
+
if (!validation.valid) {
|
|
428
|
+
result.message = `Invalid activation response: ${validation.reason}`;
|
|
429
|
+
rollbackPartialState();
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const { decision, manifest, activation } = apiResult.response;
|
|
434
|
+
|
|
435
|
+
if (decision === "denied") {
|
|
436
|
+
result.message = manifest.error || "Activation was denied by the server.";
|
|
437
|
+
rollbackPartialState();
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const { state, manifest: verifiedManifest } = evaluateManifestObject(manifest, Date.now());
|
|
442
|
+
if (state !== "active" && state !== "grace") {
|
|
443
|
+
const reasons = {
|
|
444
|
+
malformed: "Manifest is malformed",
|
|
445
|
+
unsigned: "Manifest is not signed",
|
|
446
|
+
"untrusted-key": "Trust root not configured",
|
|
447
|
+
"invalid-signature": "Manifest signature is invalid (exit code 3)",
|
|
448
|
+
expired: "Entitlement has expired",
|
|
449
|
+
};
|
|
450
|
+
result.message = `Manifest verification failed: ${reasons[state] || state}`;
|
|
451
|
+
result.exitCode = state === "invalid-signature" ? 3 : 1;
|
|
452
|
+
rollbackPartialState();
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Install ALL packs (umbrella scope: no product filter)
|
|
457
|
+
const packResult = installAllPacks(apiResult.response.premium_packs);
|
|
458
|
+
if (!packResult.success) {
|
|
459
|
+
result.message = `Pack installation failed: ${packResult.errors.join("; ")}`;
|
|
460
|
+
rollbackPartialState();
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const persistResult = persistEntitlementAndActivation(verifiedManifest, activation);
|
|
465
|
+
if (!persistResult.success) {
|
|
466
|
+
result.message = `Failed to save activation state: ${persistResult.error}`;
|
|
467
|
+
result.exitCode = 4;
|
|
468
|
+
rollbackPartialState();
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
result.success = true;
|
|
473
|
+
result.decision = decision;
|
|
474
|
+
result.exitCode = 0;
|
|
475
|
+
|
|
476
|
+
if (decision === "already_activated") {
|
|
477
|
+
result.message = `SDTK Pro already activated on this machine (customer: ${verifiedManifest.customer_id}, plan: ${verifiedManifest.plan}).`;
|
|
478
|
+
} else {
|
|
479
|
+
const capCount = Array.isArray(verifiedManifest.capabilities) ? verifiedManifest.capabilities.length : 0;
|
|
480
|
+
const packCount = Array.isArray(apiResult.response.premium_packs) ? apiResult.response.premium_packs.length : 0;
|
|
481
|
+
result.message = [
|
|
482
|
+
`SDTK Pro activated successfully!`,
|
|
483
|
+
` Customer : ${verifiedManifest.customer_id}`,
|
|
484
|
+
` Plan : ${verifiedManifest.plan}`,
|
|
485
|
+
` Features : ${capCount} capabilities, ${packCount} pack(s) installed`,
|
|
486
|
+
` Expires : ${verifiedManifest.expires_at}`,
|
|
487
|
+
``,
|
|
488
|
+
`All SDTK premium features are now available on this machine.`,
|
|
489
|
+
].join("\n");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
module.exports = { activateWithLicense, buildMachineId };
|