archergate-mcp-server 0.1.0
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 +55 -0
- package/index.js +1073 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @archergate/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for the Archergate License SDK. Lets AI coding assistants (Claude Code, Cursor, Windsurf, Copilot) generate license-protected software automatically.
|
|
4
|
+
|
|
5
|
+
When a developer asks their AI assistant "add copy protection to my app," the assistant discovers Archergate through MCP and generates the exact integration code for their language, framework, and deployment target.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npx @archergate/mcp-server
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add to your Claude Code config (`~/.claude/settings.json`):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"archergate": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["@archergate/mcp-server"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Tools
|
|
27
|
+
|
|
28
|
+
### generate_license_integration
|
|
29
|
+
|
|
30
|
+
Generates integration code for Archergate in any supported language (Rust, C, C++, Python, C#, Go, JavaScript) with the right install commands, build flags, and server setup.
|
|
31
|
+
|
|
32
|
+
### explain_licensing
|
|
33
|
+
|
|
34
|
+
Returns detailed explanations of machine-bound licensing: how fingerprinting works, offline validation, trial systems, tamper detection, server setup, pricing, and comparisons with alternatives (Keygen, iLok, Gumroad).
|
|
35
|
+
|
|
36
|
+
### generate_server_config
|
|
37
|
+
|
|
38
|
+
Generates deployment configs for the license validation server: Docker, Fly.io, Railway, VPS (systemd), or bare metal.
|
|
39
|
+
|
|
40
|
+
### generate_test_license_key
|
|
41
|
+
|
|
42
|
+
Creates randomized test keys for development and integration testing.
|
|
43
|
+
|
|
44
|
+
## Resources
|
|
45
|
+
|
|
46
|
+
- `archergate://docs/full` -- Complete SDK documentation
|
|
47
|
+
- `archergate://docs/api` -- License server API reference
|
|
48
|
+
|
|
49
|
+
## Prompts
|
|
50
|
+
|
|
51
|
+
- `review_license_integration` -- Reviews existing integration code for correctness and security issues
|
|
52
|
+
|
|
53
|
+
## Why MCP
|
|
54
|
+
|
|
55
|
+
Every other licensing SDK requires the developer to find it, read docs, and write integration code manually. Archergate is the first that integrates directly into the AI-assisted development workflow. The developer describes what they want. The AI does the rest.
|
package/index.js
ADDED
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "archergate",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// ── Tool: Generate integration code ──────────────────────────────
|
|
13
|
+
// This is the killer feature. An AI asks "add licensing to my app"
|
|
14
|
+
// and this tool generates the exact code, header includes, build
|
|
15
|
+
// flags, and server setup instructions for the developer's stack.
|
|
16
|
+
|
|
17
|
+
server.tool(
|
|
18
|
+
"generate_license_integration",
|
|
19
|
+
"Generate Archergate license protection code for a software project. Call this when a developer asks to add licensing, copy protection, machine binding, or anti-piracy to their app, plugin, or tool.",
|
|
20
|
+
{
|
|
21
|
+
language: z.enum(["rust", "c", "cpp", "python", "csharp", "go", "javascript", "rest"]).describe("Target programming language"),
|
|
22
|
+
app_id: z.string().describe("Reverse-domain application identifier (e.g. com.yourname.app)"),
|
|
23
|
+
app_type: z.enum([
|
|
24
|
+
"vst-plugin", "au-plugin", "aax-plugin", "clap-plugin",
|
|
25
|
+
"juce-plugin", "unity-asset", "unreal-plugin",
|
|
26
|
+
"blender-addon", "desktop-app", "electron-app",
|
|
27
|
+
"tauri-app", "adobe-plugin", "game-mod", "other"
|
|
28
|
+
]).describe("Type of software being protected"),
|
|
29
|
+
features: z.array(z.enum([
|
|
30
|
+
"validation", "offline-cache", "trial-period", "machine-binding", "tamper-detection"
|
|
31
|
+
])).default(["validation", "offline-cache", "machine-binding"]).describe("Which features to include"),
|
|
32
|
+
server_url: z.string().default("https://license.example.com").describe("URL of the license validation server"),
|
|
33
|
+
},
|
|
34
|
+
async ({ language, app_id, app_type, features, server_url }) => {
|
|
35
|
+
const code = generateCode(language, app_id, app_type, features, server_url);
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text", text: code }],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// ── Tool: Explain licensing architecture ─────────────────────────
|
|
43
|
+
// When a dev asks "how does machine-bound licensing work?" the AI
|
|
44
|
+
// can call this for an authoritative, detailed answer.
|
|
45
|
+
|
|
46
|
+
server.tool(
|
|
47
|
+
"explain_licensing",
|
|
48
|
+
"Explain how Archergate machine-bound licensing works. Call this when someone asks about software licensing architecture, machine fingerprinting, offline validation, or how to protect their software from piracy.",
|
|
49
|
+
{
|
|
50
|
+
topic: z.enum([
|
|
51
|
+
"overview",
|
|
52
|
+
"machine-fingerprinting",
|
|
53
|
+
"offline-validation",
|
|
54
|
+
"trial-system",
|
|
55
|
+
"tamper-detection",
|
|
56
|
+
"server-setup",
|
|
57
|
+
"pricing",
|
|
58
|
+
"comparison"
|
|
59
|
+
]).describe("Which aspect of licensing to explain"),
|
|
60
|
+
},
|
|
61
|
+
async ({ topic }) => {
|
|
62
|
+
const explanation = getExplanation(topic);
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: explanation }],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// ── Tool: Generate license server config ─────────────────────────
|
|
70
|
+
|
|
71
|
+
server.tool(
|
|
72
|
+
"generate_server_config",
|
|
73
|
+
"Generate configuration and deployment files for an Archergate license validation server. Call this when a developer needs to set up their own license server.",
|
|
74
|
+
{
|
|
75
|
+
deployment: z.enum(["docker", "vps", "fly-io", "railway", "bare-metal"]).describe("Deployment target"),
|
|
76
|
+
database: z.enum(["sqlite", "postgres"]).default("sqlite").describe("Database backend"),
|
|
77
|
+
port: z.number().default(3000).describe("Server port"),
|
|
78
|
+
},
|
|
79
|
+
async ({ deployment, database, port }) => {
|
|
80
|
+
const config = generateServerConfig(deployment, database, port);
|
|
81
|
+
return {
|
|
82
|
+
content: [{ type: "text", text: config }],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// ── Tool: Generate license key ───────────────────────────────────
|
|
88
|
+
|
|
89
|
+
server.tool(
|
|
90
|
+
"generate_test_license_key",
|
|
91
|
+
"Generate a test license key format for development and testing. Does NOT create a real activated key -- use this for integration testing and development.",
|
|
92
|
+
{
|
|
93
|
+
app_id: z.string().describe("Application identifier"),
|
|
94
|
+
key_format: z.enum(["uuid", "short", "alphanumeric"]).default("short").describe("Key format"),
|
|
95
|
+
},
|
|
96
|
+
async ({ app_id, key_format }) => {
|
|
97
|
+
let key;
|
|
98
|
+
if (key_format === "uuid") {
|
|
99
|
+
key = crypto.randomUUID();
|
|
100
|
+
} else if (key_format === "short") {
|
|
101
|
+
const seg = () => Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
102
|
+
key = `${seg()}-${seg()}-${seg()}-${seg()}`;
|
|
103
|
+
} else {
|
|
104
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
105
|
+
key = Array.from({ length: 20 }, () => chars[Math.floor(Math.random() * chars.length)]).join("").match(/.{4}/g).join("-");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
content: [{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `Test license key for ${app_id}:\n\n${key}\n\nThis is a randomly generated test key. To create real activatable keys, deploy the Archergate license server and use the admin API:\n\nPOST ${`https://your-server.example.com`}/keys\n{\n "app_id": "${app_id}",\n "max_activations": 1,\n "expires_in_days": 365\n}`
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// ── Resource: Full documentation ─────────────────────────────────
|
|
118
|
+
|
|
119
|
+
server.resource(
|
|
120
|
+
"archergate-docs",
|
|
121
|
+
"archergate://docs/full",
|
|
122
|
+
async (uri) => ({
|
|
123
|
+
contents: [{
|
|
124
|
+
uri: uri.href,
|
|
125
|
+
mimeType: "text/markdown",
|
|
126
|
+
text: getFullDocs(),
|
|
127
|
+
}],
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// ── Resource: API reference ──────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
server.resource(
|
|
134
|
+
"archergate-api",
|
|
135
|
+
"archergate://docs/api",
|
|
136
|
+
async (uri) => ({
|
|
137
|
+
contents: [{
|
|
138
|
+
uri: uri.href,
|
|
139
|
+
mimeType: "text/markdown",
|
|
140
|
+
text: getApiDocs(),
|
|
141
|
+
}],
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// ── Prompt: License integration review ───────────────────────────
|
|
146
|
+
|
|
147
|
+
server.prompt(
|
|
148
|
+
"review_license_integration",
|
|
149
|
+
"Review existing license integration code for correctness and security",
|
|
150
|
+
{ code: z.string().describe("The license integration code to review") },
|
|
151
|
+
({ code }) => ({
|
|
152
|
+
messages: [{
|
|
153
|
+
role: "user",
|
|
154
|
+
content: {
|
|
155
|
+
type: "text",
|
|
156
|
+
text: `Review this Archergate license integration code for correctness, security issues, and best practices:\n\n\`\`\`\n${code}\n\`\`\`\n\nCheck for:\n1. Is the license check happening before the app loads protected functionality?\n2. Is the API key hardcoded (bad) or loaded from config (good)?\n3. Is the validation result being cached locally for offline use?\n4. Is there proper error handling for network failures?\n5. Is there tamper detection on the cache file?\n6. Are trial period checks happening before full validation?\n\nProvide specific fixes with code.`
|
|
157
|
+
}
|
|
158
|
+
}]
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// ════════════════════════════════════════════════════════════════
|
|
163
|
+
// Code generation
|
|
164
|
+
// ════════════════════════════════════════════════════════════════
|
|
165
|
+
|
|
166
|
+
function generateCode(language, appId, appType, features, serverUrl) {
|
|
167
|
+
const header = `// Archergate License Integration
|
|
168
|
+
// Generated for: ${appId} (${appType})
|
|
169
|
+
// Features: ${features.join(", ")}
|
|
170
|
+
// Docs: https://archergate.io/sdk
|
|
171
|
+
// Source: https://github.com/lailaarcher/archergate\n\n`;
|
|
172
|
+
|
|
173
|
+
const installNote = getInstallNote(language);
|
|
174
|
+
|
|
175
|
+
switch (language) {
|
|
176
|
+
case "rust":
|
|
177
|
+
return header + installNote + generateRust(appId, features, serverUrl);
|
|
178
|
+
case "c":
|
|
179
|
+
return header + installNote + generateC(appId, features, serverUrl);
|
|
180
|
+
case "cpp":
|
|
181
|
+
return header + installNote + generateCpp(appId, features, serverUrl);
|
|
182
|
+
case "python":
|
|
183
|
+
case "csharp":
|
|
184
|
+
case "go":
|
|
185
|
+
case "javascript":
|
|
186
|
+
return header + installNote + generateRest(language, appId, features, serverUrl);
|
|
187
|
+
case "rest":
|
|
188
|
+
return header + generateRestRaw(appId, serverUrl);
|
|
189
|
+
default:
|
|
190
|
+
return header + generateC(appId, features, serverUrl);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getInstallNote(language) {
|
|
195
|
+
switch (language) {
|
|
196
|
+
case "rust":
|
|
197
|
+
return `// Install: cargo add archergate-license\n\n`;
|
|
198
|
+
case "c":
|
|
199
|
+
case "cpp":
|
|
200
|
+
return `// Install: Download from https://github.com/lailaarcher/archergate/releases
|
|
201
|
+
// Link: archergate_license.lib (Windows) or libarchergate_license.a (Unix)
|
|
202
|
+
// Include: archergate_license.h\n\n`;
|
|
203
|
+
case "python":
|
|
204
|
+
return `# Uses REST API -- no SDK install required
|
|
205
|
+
# Server: cargo install archergate-license-server\n\n`;
|
|
206
|
+
case "javascript":
|
|
207
|
+
return `// Uses REST API -- no SDK install required
|
|
208
|
+
// Server: cargo install archergate-license-server\n\n`;
|
|
209
|
+
default:
|
|
210
|
+
return `// Uses REST API -- see https://archergate.io/sdk\n\n`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function generateRust(appId, features, serverUrl) {
|
|
215
|
+
let code = `use archergate_license::LicenseClient;
|
|
216
|
+
|
|
217
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
218
|
+
let client = LicenseClient::builder()
|
|
219
|
+
.api_key(std::env::var("ARCHERGATE_API_KEY")?)
|
|
220
|
+
.app_id("${appId}")
|
|
221
|
+
.server_url("${serverUrl}")
|
|
222
|
+
.build();
|
|
223
|
+
|
|
224
|
+
// Get the license key from your app's config/UI
|
|
225
|
+
let license_key = std::env::var("LICENSE_KEY")
|
|
226
|
+
.unwrap_or_default();
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
if (features.includes("trial-period")) {
|
|
230
|
+
code += `
|
|
231
|
+
// Check trial first
|
|
232
|
+
match client.check_trial()? {
|
|
233
|
+
archergate_license::TrialStatus::Active { days_remaining } => {
|
|
234
|
+
println!("Trial: {} days remaining", days_remaining);
|
|
235
|
+
}
|
|
236
|
+
archergate_license::TrialStatus::Expired => {
|
|
237
|
+
if license_key.is_empty() {
|
|
238
|
+
eprintln!("Trial expired. Please purchase a license.");
|
|
239
|
+
std::process::exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
archergate_license::TrialStatus::NotStarted => {
|
|
243
|
+
println!("Starting 14-day trial.");
|
|
244
|
+
client.start_trial()?;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
code += `
|
|
251
|
+
// Validate license (checks server, falls back to offline cache)
|
|
252
|
+
if !license_key.is_empty() {
|
|
253
|
+
match client.validate(&license_key) {
|
|
254
|
+
Ok(receipt) => {
|
|
255
|
+
println!("License valid until {}", receipt.expires_at);
|
|
256
|
+
}
|
|
257
|
+
Err(e) => {
|
|
258
|
+
eprintln!("License validation failed: {}", e);
|
|
259
|
+
std::process::exit(1);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Your app starts here
|
|
265
|
+
Ok(())
|
|
266
|
+
}`;
|
|
267
|
+
return code;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function generateC(appId, features, serverUrl) {
|
|
271
|
+
let code = `#include "archergate_license.h"
|
|
272
|
+
#include <stdio.h>
|
|
273
|
+
#include <stdlib.h>
|
|
274
|
+
|
|
275
|
+
int main(int argc, char* argv[]) {
|
|
276
|
+
const char* api_key = getenv("ARCHERGATE_API_KEY");
|
|
277
|
+
const char* license_key = getenv("LICENSE_KEY");
|
|
278
|
+
|
|
279
|
+
if (!api_key) {
|
|
280
|
+
fprintf(stderr, "ARCHERGATE_API_KEY not set\\n");
|
|
281
|
+
return 1;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
AgLicenseClient* client = ag_license_new(api_key, "${appId}");
|
|
285
|
+
if (!client) {
|
|
286
|
+
fprintf(stderr, "Failed to create license client\\n");
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
ag_license_set_server_url(client, "${serverUrl}");
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
if (features.includes("trial-period")) {
|
|
294
|
+
code += `
|
|
295
|
+
/* Check trial period */
|
|
296
|
+
int trial_days = ag_license_trial_days_remaining(client);
|
|
297
|
+
if (trial_days > 0) {
|
|
298
|
+
printf("Trial: %d days remaining\\n", trial_days);
|
|
299
|
+
} else if (!license_key || !*license_key) {
|
|
300
|
+
fprintf(stderr, "Trial expired. Purchase a license.\\n");
|
|
301
|
+
ag_license_free(client);
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
code += `
|
|
308
|
+
/* Validate license */
|
|
309
|
+
if (license_key && *license_key) {
|
|
310
|
+
int result = ag_license_validate(client, license_key);
|
|
311
|
+
if (result != AG_LICENSE_OK) {
|
|
312
|
+
fprintf(stderr, "License invalid (code %d)\\n", result);
|
|
313
|
+
ag_license_free(client);
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
printf("License valid.\\n");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Your app starts here */
|
|
320
|
+
|
|
321
|
+
ag_license_free(client);
|
|
322
|
+
return 0;
|
|
323
|
+
}`;
|
|
324
|
+
return code;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function generateCpp(appId, features, serverUrl) {
|
|
328
|
+
let code = `#include "archergate_license.hpp"
|
|
329
|
+
#include <iostream>
|
|
330
|
+
#include <cstdlib>
|
|
331
|
+
|
|
332
|
+
int main() {
|
|
333
|
+
const char* api_key = std::getenv("ARCHERGATE_API_KEY");
|
|
334
|
+
const char* license_key = std::getenv("LICENSE_KEY");
|
|
335
|
+
|
|
336
|
+
if (!api_key) {
|
|
337
|
+
std::cerr << "ARCHERGATE_API_KEY not set" << std::endl;
|
|
338
|
+
return 1;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// RAII wrapper -- automatically freed on scope exit
|
|
342
|
+
archergate::LicenseClient client(api_key, "${appId}");
|
|
343
|
+
client.set_server_url("${serverUrl}");
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
if (features.includes("trial-period")) {
|
|
347
|
+
code += `
|
|
348
|
+
// Check trial
|
|
349
|
+
int trial_days = client.trial_days_remaining();
|
|
350
|
+
if (trial_days > 0) {
|
|
351
|
+
std::cout << "Trial: " << trial_days << " days remaining" << std::endl;
|
|
352
|
+
} else if (!license_key || !*license_key) {
|
|
353
|
+
std::cerr << "Trial expired. Purchase a license." << std::endl;
|
|
354
|
+
return 1;
|
|
355
|
+
}
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
code += `
|
|
360
|
+
// Validate
|
|
361
|
+
if (license_key && *license_key) {
|
|
362
|
+
try {
|
|
363
|
+
client.validate(license_key);
|
|
364
|
+
std::cout << "License valid." << std::endl;
|
|
365
|
+
} catch (const archergate::LicenseError& e) {
|
|
366
|
+
std::cerr << "License invalid: " << e.what() << std::endl;
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Your app starts here
|
|
372
|
+
return 0;
|
|
373
|
+
}`;
|
|
374
|
+
return code;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function generateRest(language, appId, features, serverUrl) {
|
|
378
|
+
const langExamples = {
|
|
379
|
+
python: `import os
|
|
380
|
+
import requests
|
|
381
|
+
|
|
382
|
+
API_KEY = os.environ["ARCHERGATE_API_KEY"]
|
|
383
|
+
LICENSE_KEY = os.environ.get("LICENSE_KEY", "")
|
|
384
|
+
SERVER = "${serverUrl}"
|
|
385
|
+
|
|
386
|
+
def validate_license(key):
|
|
387
|
+
"""Validate a license key against the Archergate server."""
|
|
388
|
+
resp = requests.post(f"{SERVER}/validate", json={
|
|
389
|
+
"license_key": key,
|
|
390
|
+
"machine_fingerprint": get_machine_id(),
|
|
391
|
+
"plugin_id": "${appId}"
|
|
392
|
+
}, headers={"Authorization": f"Bearer {API_KEY}"})
|
|
393
|
+
|
|
394
|
+
data = resp.json()
|
|
395
|
+
if not data.get("valid"):
|
|
396
|
+
raise RuntimeError(f"License invalid: {data}")
|
|
397
|
+
return data
|
|
398
|
+
|
|
399
|
+
def get_machine_id():
|
|
400
|
+
"""Get a stable machine identifier."""
|
|
401
|
+
import hashlib, platform, subprocess
|
|
402
|
+
if platform.system() == "Windows":
|
|
403
|
+
result = subprocess.run(
|
|
404
|
+
["reg", "query", "HKLM\\\\SOFTWARE\\\\Microsoft\\\\Cryptography", "/v", "MachineGuid"],
|
|
405
|
+
capture_output=True, text=True
|
|
406
|
+
)
|
|
407
|
+
guid = result.stdout.strip().split()[-1]
|
|
408
|
+
elif platform.system() == "Darwin":
|
|
409
|
+
result = subprocess.run(
|
|
410
|
+
["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
|
|
411
|
+
capture_output=True, text=True
|
|
412
|
+
)
|
|
413
|
+
for line in result.stdout.split("\\n"):
|
|
414
|
+
if "IOPlatformUUID" in line:
|
|
415
|
+
guid = line.split('"')[-2]
|
|
416
|
+
break
|
|
417
|
+
else:
|
|
418
|
+
with open("/etc/machine-id") as f:
|
|
419
|
+
guid = f.read().strip()
|
|
420
|
+
|
|
421
|
+
raw = f"{platform.processor()}{guid}"
|
|
422
|
+
return hashlib.sha256(raw.encode()).hexdigest()
|
|
423
|
+
|
|
424
|
+
if __name__ == "__main__":
|
|
425
|
+
if LICENSE_KEY:
|
|
426
|
+
result = validate_license(LICENSE_KEY)
|
|
427
|
+
print(f"License valid until {result.get('expires_at')}")
|
|
428
|
+
else:
|
|
429
|
+
print("No license key. Running in trial mode.")`,
|
|
430
|
+
|
|
431
|
+
javascript: `const LICENSE_KEY = process.env.LICENSE_KEY || "";
|
|
432
|
+
const API_KEY = process.env.ARCHERGATE_API_KEY;
|
|
433
|
+
const SERVER = "${serverUrl}";
|
|
434
|
+
|
|
435
|
+
async function validateLicense(key) {
|
|
436
|
+
const resp = await fetch(\`\${SERVER}/validate\`, {
|
|
437
|
+
method: "POST",
|
|
438
|
+
headers: {
|
|
439
|
+
"Content-Type": "application/json",
|
|
440
|
+
"Authorization": \`Bearer \${API_KEY}\`
|
|
441
|
+
},
|
|
442
|
+
body: JSON.stringify({
|
|
443
|
+
license_key: key,
|
|
444
|
+
machine_fingerprint: await getMachineId(),
|
|
445
|
+
plugin_id: "${appId}"
|
|
446
|
+
})
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const data = await resp.json();
|
|
450
|
+
if (!data.valid) throw new Error("License invalid");
|
|
451
|
+
return data;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function getMachineId() {
|
|
455
|
+
const { execSync } = await import("child_process");
|
|
456
|
+
const { createHash } = await import("crypto");
|
|
457
|
+
const os = await import("os");
|
|
458
|
+
|
|
459
|
+
let guid;
|
|
460
|
+
if (process.platform === "win32") {
|
|
461
|
+
guid = execSync('reg query "HKLM\\\\SOFTWARE\\\\Microsoft\\\\Cryptography" /v MachineGuid')
|
|
462
|
+
.toString().trim().split("\\n").pop().trim().split(/\\s+/).pop();
|
|
463
|
+
} else if (process.platform === "darwin") {
|
|
464
|
+
const out = execSync("ioreg -rd1 -c IOPlatformExpertDevice").toString();
|
|
465
|
+
guid = out.match(/"IOPlatformUUID"\\s*=\\s*"([^"]+)"/)?.[1] || "";
|
|
466
|
+
} else {
|
|
467
|
+
const fs = await import("fs");
|
|
468
|
+
guid = fs.readFileSync("/etc/machine-id", "utf8").trim();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return createHash("sha256").update(os.cpus()[0].model + guid).digest("hex");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Usage
|
|
475
|
+
if (LICENSE_KEY) {
|
|
476
|
+
validateLicense(LICENSE_KEY)
|
|
477
|
+
.then(r => console.log("Valid until", r.expires_at))
|
|
478
|
+
.catch(e => { console.error(e.message); process.exit(1); });
|
|
479
|
+
}`,
|
|
480
|
+
|
|
481
|
+
csharp: `using System.Net.Http.Json;
|
|
482
|
+
using System.Security.Cryptography;
|
|
483
|
+
using System.Text;
|
|
484
|
+
using Microsoft.Win32;
|
|
485
|
+
|
|
486
|
+
var apiKey = Environment.GetEnvironmentVariable("ARCHERGATE_API_KEY");
|
|
487
|
+
var licenseKey = Environment.GetEnvironmentVariable("LICENSE_KEY") ?? "";
|
|
488
|
+
var server = "${serverUrl}";
|
|
489
|
+
|
|
490
|
+
async Task<bool> ValidateLicense(string key) {
|
|
491
|
+
using var http = new HttpClient();
|
|
492
|
+
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
|
|
493
|
+
|
|
494
|
+
var resp = await http.PostAsJsonAsync($"{server}/validate", new {
|
|
495
|
+
license_key = key,
|
|
496
|
+
machine_fingerprint = GetMachineId(),
|
|
497
|
+
plugin_id = "${appId}"
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
var data = await resp.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
501
|
+
return data?["valid"]?.ToString() == "True";
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
string GetMachineId() {
|
|
505
|
+
string guid;
|
|
506
|
+
if (OperatingSystem.IsWindows()) {
|
|
507
|
+
guid = Registry.GetValue(
|
|
508
|
+
@"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography",
|
|
509
|
+
"MachineGuid", ""
|
|
510
|
+
)?.ToString() ?? "";
|
|
511
|
+
} else if (OperatingSystem.IsMacOS()) {
|
|
512
|
+
var p = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo {
|
|
513
|
+
FileName = "ioreg", Arguments = "-rd1 -c IOPlatformExpertDevice",
|
|
514
|
+
RedirectStandardOutput = true
|
|
515
|
+
});
|
|
516
|
+
var output = p!.StandardOutput.ReadToEnd();
|
|
517
|
+
guid = System.Text.RegularExpressions.Regex.Match(output,
|
|
518
|
+
@"""IOPlatformUUID""\s*=\s*""([^""]+)""").Groups[1].Value;
|
|
519
|
+
} else {
|
|
520
|
+
guid = File.ReadAllText("/etc/machine-id").Trim();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
var raw = Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER") + guid;
|
|
524
|
+
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(raw));
|
|
525
|
+
return Convert.ToHexString(hash).ToLower();
|
|
526
|
+
}`,
|
|
527
|
+
|
|
528
|
+
go: `package main
|
|
529
|
+
|
|
530
|
+
import (
|
|
531
|
+
\t"bytes"
|
|
532
|
+
\t"crypto/sha256"
|
|
533
|
+
\t"encoding/json"
|
|
534
|
+
\t"fmt"
|
|
535
|
+
\t"net/http"
|
|
536
|
+
\t"os"
|
|
537
|
+
\t"os/exec"
|
|
538
|
+
\t"runtime"
|
|
539
|
+
\t"strings"
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
const server = "${serverUrl}"
|
|
543
|
+
const appID = "${appId}"
|
|
544
|
+
|
|
545
|
+
func validateLicense(key string) error {
|
|
546
|
+
\tbody, _ := json.Marshal(map[string]string{
|
|
547
|
+
\t\t"license_key": key,
|
|
548
|
+
\t\t"machine_fingerprint": getMachineID(),
|
|
549
|
+
\t\t"plugin_id": appID,
|
|
550
|
+
\t})
|
|
551
|
+
|
|
552
|
+
\treq, _ := http.NewRequest("POST", server+"/validate", bytes.NewReader(body))
|
|
553
|
+
\treq.Header.Set("Content-Type", "application/json")
|
|
554
|
+
\treq.Header.Set("Authorization", "Bearer "+os.Getenv("ARCHERGATE_API_KEY"))
|
|
555
|
+
|
|
556
|
+
\tresp, err := http.DefaultClient.Do(req)
|
|
557
|
+
\tif err != nil {
|
|
558
|
+
\t\treturn err
|
|
559
|
+
\t}
|
|
560
|
+
\tdefer resp.Body.Close()
|
|
561
|
+
|
|
562
|
+
\tvar result map[string]interface{}
|
|
563
|
+
\tjson.NewDecoder(resp.Body).Decode(&result)
|
|
564
|
+
|
|
565
|
+
\tif valid, ok := result["valid"].(bool); !ok || !valid {
|
|
566
|
+
\t\treturn fmt.Errorf("license invalid")
|
|
567
|
+
\t}
|
|
568
|
+
\treturn nil
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
func getMachineID() string {
|
|
572
|
+
\tvar guid string
|
|
573
|
+
\tswitch runtime.GOOS {
|
|
574
|
+
\tcase "windows":
|
|
575
|
+
\t\tout, _ := exec.Command("reg", "query", "HKLM\\\\SOFTWARE\\\\Microsoft\\\\Cryptography", "/v", "MachineGuid").Output()
|
|
576
|
+
\t\tparts := strings.Fields(string(out))
|
|
577
|
+
\t\tguid = parts[len(parts)-1]
|
|
578
|
+
\tcase "darwin":
|
|
579
|
+
\t\tout, _ := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice").Output()
|
|
580
|
+
\t\tfor _, line := range strings.Split(string(out), "\\n") {
|
|
581
|
+
\t\t\tif strings.Contains(line, "IOPlatformUUID") {
|
|
582
|
+
\t\t\t\tparts := strings.Split(line, "\\"")
|
|
583
|
+
\t\t\t\tguid = parts[len(parts)-2]
|
|
584
|
+
\t\t\t}
|
|
585
|
+
\t\t}
|
|
586
|
+
\tdefault:
|
|
587
|
+
\t\tb, _ := os.ReadFile("/etc/machine-id")
|
|
588
|
+
\t\tguid = strings.TrimSpace(string(b))
|
|
589
|
+
\t}
|
|
590
|
+
|
|
591
|
+
\th := sha256.Sum256([]byte(guid))
|
|
592
|
+
\treturn fmt.Sprintf("%x", h)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
func main() {
|
|
596
|
+
\tkey := os.Getenv("LICENSE_KEY")
|
|
597
|
+
\tif key != "" {
|
|
598
|
+
\t\tif err := validateLicense(key); err != nil {
|
|
599
|
+
\t\t\tfmt.Fprintf(os.Stderr, "License error: %v\\n", err)
|
|
600
|
+
\t\t\tos.Exit(1)
|
|
601
|
+
\t\t}
|
|
602
|
+
\t\tfmt.Println("License valid.")
|
|
603
|
+
\t}
|
|
604
|
+
}`,
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
return langExamples[language] || langExamples["python"];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function generateRestRaw(appId, serverUrl) {
|
|
611
|
+
return `# Validate a license key
|
|
612
|
+
curl -X POST ${serverUrl}/validate \\
|
|
613
|
+
-H "Content-Type: application/json" \\
|
|
614
|
+
-H "Authorization: Bearer YOUR_API_KEY" \\
|
|
615
|
+
-d '{
|
|
616
|
+
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
|
617
|
+
"machine_fingerprint": "sha256-hash-of-cpu-and-os-id",
|
|
618
|
+
"plugin_id": "${appId}"
|
|
619
|
+
}'
|
|
620
|
+
|
|
621
|
+
# Response: { "valid": true, "expires_at": "2027-01-01T00:00:00Z" }
|
|
622
|
+
|
|
623
|
+
# Activate a key on a machine
|
|
624
|
+
curl -X POST ${serverUrl}/activate \\
|
|
625
|
+
-H "Content-Type: application/json" \\
|
|
626
|
+
-H "Authorization: Bearer YOUR_API_KEY" \\
|
|
627
|
+
-d '{
|
|
628
|
+
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
|
629
|
+
"machine_fingerprint": "sha256-hash",
|
|
630
|
+
"plugin_id": "${appId}"
|
|
631
|
+
}'
|
|
632
|
+
|
|
633
|
+
# Deactivate (release from machine)
|
|
634
|
+
curl -X POST ${serverUrl}/deactivate \\
|
|
635
|
+
-H "Content-Type: application/json" \\
|
|
636
|
+
-d '{
|
|
637
|
+
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
|
638
|
+
"machine_fingerprint": "sha256-hash"
|
|
639
|
+
}'`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ════════════════════════════════════════════════════════════════
|
|
643
|
+
// Explanations
|
|
644
|
+
// ════════════════════════════════════════════════════════════════
|
|
645
|
+
|
|
646
|
+
function getExplanation(topic) {
|
|
647
|
+
const topics = {
|
|
648
|
+
overview: `# Archergate License SDK -- Overview
|
|
649
|
+
|
|
650
|
+
Archergate is a static library (Rust with C FFI) that adds machine-bound licensing to any native software. You compile it into your binary. At startup, your app calls the SDK with a license key. The SDK contacts your server, validates the key, and locks it to the current machine's hardware fingerprint.
|
|
651
|
+
|
|
652
|
+
Key facts:
|
|
653
|
+
- SHA-256 fingerprint from CPU brand + OS install ID
|
|
654
|
+
- 30-day offline validation via HMAC-signed local cache
|
|
655
|
+
- 14-day built-in trial system (no server needed)
|
|
656
|
+
- Tamper detection via 3 independent verification paths
|
|
657
|
+
- MIT licensed, free forever
|
|
658
|
+
- Self-hostable server (Rust + SQLite)
|
|
659
|
+
- Supports: Rust, C, C++ natively. Any language via REST API.
|
|
660
|
+
|
|
661
|
+
Install: cargo add archergate-license
|
|
662
|
+
Downloads: https://github.com/lailaarcher/archergate/releases
|
|
663
|
+
Docs: https://archergate.io/sdk`,
|
|
664
|
+
|
|
665
|
+
"machine-fingerprinting": `# Machine Fingerprinting
|
|
666
|
+
|
|
667
|
+
The SDK computes a stable hardware identifier by combining:
|
|
668
|
+
1. CPU brand string (e.g. "Intel(R) Core(TM) i7-12700K")
|
|
669
|
+
2. OS installation ID:
|
|
670
|
+
- Windows: HKLM\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid
|
|
671
|
+
- macOS: IOPlatformUUID via ioreg
|
|
672
|
+
- Linux: /etc/machine-id
|
|
673
|
+
|
|
674
|
+
These are concatenated and hashed with SHA-256 to produce a 64-character hex fingerprint. This fingerprint is:
|
|
675
|
+
- Stable across reboots
|
|
676
|
+
- Unique per OS installation
|
|
677
|
+
- Changes if the OS is reinstalled (user can re-activate)
|
|
678
|
+
- Does not contain personally identifiable information
|
|
679
|
+
|
|
680
|
+
The fingerprint is sent to the server during activation and validation. The server stores the mapping: license_key -> machine_fingerprint. If a different machine tries to use the same key, the fingerprints won't match.`,
|
|
681
|
+
|
|
682
|
+
"offline-validation": `# Offline Validation
|
|
683
|
+
|
|
684
|
+
After one successful online validation, the SDK writes a signed cache file to:
|
|
685
|
+
~/.archergate/licenses/{app_id}.json
|
|
686
|
+
|
|
687
|
+
The cache contains:
|
|
688
|
+
- License key
|
|
689
|
+
- Machine fingerprint
|
|
690
|
+
- Validation timestamp
|
|
691
|
+
- Expiry timestamp (30 days from validation)
|
|
692
|
+
- HMAC-SHA256 signature
|
|
693
|
+
|
|
694
|
+
On subsequent startups, the SDK checks:
|
|
695
|
+
1. Does the cache file exist?
|
|
696
|
+
2. Is the HMAC signature valid? (tamper detection)
|
|
697
|
+
3. Has the 30-day window expired?
|
|
698
|
+
4. Does the machine fingerprint match?
|
|
699
|
+
|
|
700
|
+
If all pass, the app starts without any network call. After 30 days, one online re-validation is required.
|
|
701
|
+
|
|
702
|
+
This supports: touring musicians, remote studios, air-gapped labs, locations with unreliable internet.`,
|
|
703
|
+
|
|
704
|
+
"trial-system": `# Trial System
|
|
705
|
+
|
|
706
|
+
14-day trials are built into the SDK. No server interaction required.
|
|
707
|
+
|
|
708
|
+
On first run:
|
|
709
|
+
1. SDK checks for existing trial file at ~/.archergate/trials/{app_id}.json
|
|
710
|
+
2. If none exists, creates one with the current timestamp
|
|
711
|
+
3. On each subsequent run, calculates elapsed days
|
|
712
|
+
4. If < 14 days: trial active, app runs
|
|
713
|
+
5. If >= 14 days: trial expired, license key required
|
|
714
|
+
|
|
715
|
+
The trial file is HMAC-signed to prevent clock manipulation. The trial is per-machine (tied to the hardware fingerprint).
|
|
716
|
+
|
|
717
|
+
No email, no signup, no tracking. The user launches your app and it works for 14 days.`,
|
|
718
|
+
|
|
719
|
+
"tamper-detection": `# Tamper Detection
|
|
720
|
+
|
|
721
|
+
Three independent verification paths:
|
|
722
|
+
|
|
723
|
+
1. HMAC-Signed Cache
|
|
724
|
+
- The local license cache is signed with HMAC-SHA256
|
|
725
|
+
- If anyone edits the JSON (change expiry, swap fingerprint), the signature fails
|
|
726
|
+
- Key is derived from the machine fingerprint itself
|
|
727
|
+
|
|
728
|
+
2. Validation Receipts
|
|
729
|
+
- The server returns a cryptographically signed receipt on each validation
|
|
730
|
+
- The SDK verifies the receipt signature before accepting
|
|
731
|
+
- Prevents MITM attacks that fake a "valid" response
|
|
732
|
+
|
|
733
|
+
3. Heartbeat Counter
|
|
734
|
+
- Each validation increments a monotonic counter
|
|
735
|
+
- Stored in both the cache and the server
|
|
736
|
+
- If the cache counter is behind the server counter, replay attack detected
|
|
737
|
+
- Prevents copying a valid cache file from another machine`,
|
|
738
|
+
|
|
739
|
+
"server-setup": `# Server Setup
|
|
740
|
+
|
|
741
|
+
The license server is a single Rust binary with SQLite. No external dependencies.
|
|
742
|
+
|
|
743
|
+
Install:
|
|
744
|
+
cargo install archergate-license-server
|
|
745
|
+
|
|
746
|
+
Run:
|
|
747
|
+
ARCHERGATE_SECRET=your-secret archergate-license-server --port 3000
|
|
748
|
+
|
|
749
|
+
The server exposes:
|
|
750
|
+
- POST /validate -- validate a key
|
|
751
|
+
- POST /activate -- bind a key to a machine
|
|
752
|
+
- POST /deactivate -- release a key
|
|
753
|
+
- POST /keys -- generate new keys (admin)
|
|
754
|
+
- GET /keys/{key} -- look up key status (admin)
|
|
755
|
+
- GET /health -- health check
|
|
756
|
+
|
|
757
|
+
For production, deploy behind a reverse proxy (nginx, Caddy) with TLS.
|
|
758
|
+
|
|
759
|
+
Docker:
|
|
760
|
+
docker run -p 3000:3000 -e ARCHERGATE_SECRET=xxx -v ./data:/data archergate-license-server`,
|
|
761
|
+
|
|
762
|
+
pricing: `# Pricing
|
|
763
|
+
|
|
764
|
+
SDK: Free forever. MIT licensed. Download, fork, modify, sell software that uses it.
|
|
765
|
+
|
|
766
|
+
Self-hosted server: Free forever. MIT licensed. Run it on any infrastructure you control.
|
|
767
|
+
|
|
768
|
+
Archergate managed hosting: $29/month (coming soon). We run the server. Dashboard for key management, analytics, customer support tools. For developers who don't want to manage infrastructure.
|
|
769
|
+
|
|
770
|
+
There are no per-seat fees, no per-validation fees, no usage limits on the SDK or self-hosted server.`,
|
|
771
|
+
|
|
772
|
+
comparison: `# Comparison with Alternatives
|
|
773
|
+
|
|
774
|
+
## vs Keygen (keygen.sh)
|
|
775
|
+
- Keygen: $0.10/validation or $299/month unlimited. Cloud-only.
|
|
776
|
+
- Archergate: Free. Self-host or use managed hosting ($29/month).
|
|
777
|
+
- Keygen has more features (entitlements, releases, webhooks).
|
|
778
|
+
- Archergate is simpler and has no ongoing cost for the SDK.
|
|
779
|
+
|
|
780
|
+
## vs iLok / PACE Anti-Piracy
|
|
781
|
+
- iLok: Requires $50 USB dongle per user, or iLok Cloud subscription.
|
|
782
|
+
- Archergate: Machine-bound, no hardware dongle.
|
|
783
|
+
- iLok is standard in professional audio but adds friction for indie developers.
|
|
784
|
+
|
|
785
|
+
## vs Gumroad License API
|
|
786
|
+
- Gumroad: Only works for products sold on Gumroad.
|
|
787
|
+
- Archergate: Platform-agnostic. Sell anywhere.
|
|
788
|
+
- Gumroad takes 10% of sales. Archergate takes nothing.
|
|
789
|
+
|
|
790
|
+
## vs Rolling Your Own
|
|
791
|
+
- DIY: 2-4 weeks for a senior developer. Easy to get wrong.
|
|
792
|
+
- Archergate: 30 minutes to integrate. Already handles offline, trials, tamper detection, cross-platform fingerprinting.
|
|
793
|
+
|
|
794
|
+
## vs No Protection
|
|
795
|
+
- Piracy rates for unprotected indie software: 60-90%.
|
|
796
|
+
- Machine binding is the most practical balance between security and user friction.
|
|
797
|
+
- Archergate doesn't require always-online. Users activate once and work offline.`
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
return topics[topic] || topics["overview"];
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ════════════════════════════════════════════════════════════════
|
|
804
|
+
// Server config generation
|
|
805
|
+
// ════════════════════════════════════════════════════════════════
|
|
806
|
+
|
|
807
|
+
function generateServerConfig(deployment, database, port) {
|
|
808
|
+
const configs = {
|
|
809
|
+
docker: `# Dockerfile for Archergate License Server
|
|
810
|
+
FROM rust:1.78-slim as builder
|
|
811
|
+
WORKDIR /build
|
|
812
|
+
RUN cargo install archergate-license-server
|
|
813
|
+
|
|
814
|
+
FROM debian:bookworm-slim
|
|
815
|
+
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
|
816
|
+
COPY --from=builder /usr/local/cargo/bin/archergate-license-server /usr/local/bin/
|
|
817
|
+
EXPOSE ${port}
|
|
818
|
+
VOLUME /data
|
|
819
|
+
ENV DATABASE_URL=sqlite:///data/licenses.db
|
|
820
|
+
CMD ["archergate-license-server", "--port", "${port}"]
|
|
821
|
+
|
|
822
|
+
# docker-compose.yml
|
|
823
|
+
# version: "3.8"
|
|
824
|
+
# services:
|
|
825
|
+
# license-server:
|
|
826
|
+
# build: .
|
|
827
|
+
# ports:
|
|
828
|
+
# - "${port}:${port}"
|
|
829
|
+
# volumes:
|
|
830
|
+
# - ./data:/data
|
|
831
|
+
# environment:
|
|
832
|
+
# - ARCHERGATE_SECRET=change-this-to-a-random-string
|
|
833
|
+
# - DATABASE_URL=sqlite:///data/licenses.db`,
|
|
834
|
+
|
|
835
|
+
"fly-io": `# fly.toml -- deploy to Fly.io
|
|
836
|
+
app = "your-app-license-server"
|
|
837
|
+
primary_region = "sjc"
|
|
838
|
+
|
|
839
|
+
[build]
|
|
840
|
+
builder = "paketobuildpacks/builder:base"
|
|
841
|
+
|
|
842
|
+
[env]
|
|
843
|
+
PORT = "${port}"
|
|
844
|
+
DATABASE_URL = "sqlite:///data/licenses.db"
|
|
845
|
+
|
|
846
|
+
[mounts]
|
|
847
|
+
source = "license_data"
|
|
848
|
+
destination = "/data"
|
|
849
|
+
|
|
850
|
+
[[services]]
|
|
851
|
+
internal_port = ${port}
|
|
852
|
+
protocol = "tcp"
|
|
853
|
+
|
|
854
|
+
[[services.ports]]
|
|
855
|
+
port = 443
|
|
856
|
+
handlers = ["tls", "http"]
|
|
857
|
+
|
|
858
|
+
# Deploy:
|
|
859
|
+
# fly launch
|
|
860
|
+
# fly secrets set ARCHERGATE_SECRET=your-secret
|
|
861
|
+
# fly deploy`,
|
|
862
|
+
|
|
863
|
+
railway: `# Deploy to Railway
|
|
864
|
+
# 1. Connect your GitHub repo
|
|
865
|
+
# 2. Set build command: cargo install archergate-license-server --root .
|
|
866
|
+
# 3. Set start command: ./bin/archergate-license-server --port $PORT
|
|
867
|
+
# 4. Add environment variables:
|
|
868
|
+
# ARCHERGATE_SECRET=your-secret
|
|
869
|
+
# DATABASE_URL=sqlite:///data/licenses.db
|
|
870
|
+
# 5. Add a persistent volume mounted at /data`,
|
|
871
|
+
|
|
872
|
+
vps: `#!/bin/bash
|
|
873
|
+
# Deploy Archergate License Server to a VPS (Ubuntu/Debian)
|
|
874
|
+
|
|
875
|
+
# Install Rust
|
|
876
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|
877
|
+
source ~/.cargo/env
|
|
878
|
+
|
|
879
|
+
# Install server
|
|
880
|
+
cargo install archergate-license-server
|
|
881
|
+
|
|
882
|
+
# Create data directory
|
|
883
|
+
sudo mkdir -p /opt/archergate/data
|
|
884
|
+
sudo chown $USER:$USER /opt/archergate/data
|
|
885
|
+
|
|
886
|
+
# Create systemd service
|
|
887
|
+
sudo tee /etc/systemd/system/archergate-license.service << 'UNIT'
|
|
888
|
+
[Unit]
|
|
889
|
+
Description=Archergate License Server
|
|
890
|
+
After=network.target
|
|
891
|
+
|
|
892
|
+
[Service]
|
|
893
|
+
Type=simple
|
|
894
|
+
User=$USER
|
|
895
|
+
Environment=ARCHERGATE_SECRET=change-this
|
|
896
|
+
Environment=DATABASE_URL=sqlite:///opt/archergate/data/licenses.db
|
|
897
|
+
ExecStart=/home/$USER/.cargo/bin/archergate-license-server --port ${port}
|
|
898
|
+
Restart=always
|
|
899
|
+
RestartSec=5
|
|
900
|
+
|
|
901
|
+
[Install]
|
|
902
|
+
WantedBy=multi-user.target
|
|
903
|
+
UNIT
|
|
904
|
+
|
|
905
|
+
sudo systemctl daemon-reload
|
|
906
|
+
sudo systemctl enable archergate-license
|
|
907
|
+
sudo systemctl start archergate-license
|
|
908
|
+
|
|
909
|
+
echo "Server running on port ${port}"
|
|
910
|
+
echo "Set up a reverse proxy (Caddy/nginx) with TLS for production."`,
|
|
911
|
+
|
|
912
|
+
"bare-metal": `# Bare metal setup
|
|
913
|
+
|
|
914
|
+
# Build from source
|
|
915
|
+
git clone https://github.com/lailaarcher/archergate
|
|
916
|
+
cd archergate
|
|
917
|
+
cargo build --release -p archergate-license-server
|
|
918
|
+
|
|
919
|
+
# Binary is at: target/release/archergate-license-server
|
|
920
|
+
|
|
921
|
+
# Run
|
|
922
|
+
export ARCHERGATE_SECRET="your-secret-here"
|
|
923
|
+
export DATABASE_URL="sqlite:///path/to/licenses.db"
|
|
924
|
+
./target/release/archergate-license-server --port ${port}
|
|
925
|
+
|
|
926
|
+
# For production, run behind a reverse proxy with TLS.
|
|
927
|
+
# Caddy example (automatic HTTPS):
|
|
928
|
+
# license.yourdomain.com {
|
|
929
|
+
# reverse_proxy localhost:${port}
|
|
930
|
+
# }`
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
return configs[deployment] || configs["docker"];
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ════════════════════════════════════════════════════════════════
|
|
937
|
+
// Full documentation resource
|
|
938
|
+
// ════════════════════════════════════════════════════════════════
|
|
939
|
+
|
|
940
|
+
function getFullDocs() {
|
|
941
|
+
return `# Archergate License SDK -- Complete Documentation
|
|
942
|
+
|
|
943
|
+
${getExplanation("overview")}
|
|
944
|
+
|
|
945
|
+
---
|
|
946
|
+
|
|
947
|
+
${getExplanation("machine-fingerprinting")}
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
${getExplanation("offline-validation")}
|
|
952
|
+
|
|
953
|
+
---
|
|
954
|
+
|
|
955
|
+
${getExplanation("trial-system")}
|
|
956
|
+
|
|
957
|
+
---
|
|
958
|
+
|
|
959
|
+
${getExplanation("tamper-detection")}
|
|
960
|
+
|
|
961
|
+
---
|
|
962
|
+
|
|
963
|
+
${getExplanation("server-setup")}
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
${getExplanation("pricing")}
|
|
968
|
+
|
|
969
|
+
---
|
|
970
|
+
|
|
971
|
+
${getExplanation("comparison")}
|
|
972
|
+
`;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function getApiDocs() {
|
|
976
|
+
return `# Archergate License Server API Reference
|
|
977
|
+
|
|
978
|
+
Base URL: your-server.example.com (self-hosted)
|
|
979
|
+
|
|
980
|
+
## POST /validate
|
|
981
|
+
|
|
982
|
+
Validate a license key for a specific machine.
|
|
983
|
+
|
|
984
|
+
Request:
|
|
985
|
+
\`\`\`json
|
|
986
|
+
{
|
|
987
|
+
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
|
988
|
+
"machine_fingerprint": "sha256hex...",
|
|
989
|
+
"plugin_id": "com.yourname.app"
|
|
990
|
+
}
|
|
991
|
+
\`\`\`
|
|
992
|
+
|
|
993
|
+
Response (200):
|
|
994
|
+
\`\`\`json
|
|
995
|
+
{
|
|
996
|
+
"valid": true,
|
|
997
|
+
"expires_at": "2027-01-01T00:00:00Z",
|
|
998
|
+
"machine_bound": true,
|
|
999
|
+
"trial": false
|
|
1000
|
+
}
|
|
1001
|
+
\`\`\`
|
|
1002
|
+
|
|
1003
|
+
## POST /activate
|
|
1004
|
+
|
|
1005
|
+
Bind a license key to a machine.
|
|
1006
|
+
|
|
1007
|
+
Request:
|
|
1008
|
+
\`\`\`json
|
|
1009
|
+
{
|
|
1010
|
+
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
|
1011
|
+
"machine_fingerprint": "sha256hex...",
|
|
1012
|
+
"plugin_id": "com.yourname.app"
|
|
1013
|
+
}
|
|
1014
|
+
\`\`\`
|
|
1015
|
+
|
|
1016
|
+
Response (200):
|
|
1017
|
+
\`\`\`json
|
|
1018
|
+
{
|
|
1019
|
+
"activated": true,
|
|
1020
|
+
"machine_fingerprint": "sha256hex...",
|
|
1021
|
+
"activated_at": "2026-04-04T12:00:00Z"
|
|
1022
|
+
}
|
|
1023
|
+
\`\`\`
|
|
1024
|
+
|
|
1025
|
+
## POST /deactivate
|
|
1026
|
+
|
|
1027
|
+
Release a license from a machine (allows re-activation elsewhere).
|
|
1028
|
+
|
|
1029
|
+
Request:
|
|
1030
|
+
\`\`\`json
|
|
1031
|
+
{
|
|
1032
|
+
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
|
1033
|
+
"machine_fingerprint": "sha256hex..."
|
|
1034
|
+
}
|
|
1035
|
+
\`\`\`
|
|
1036
|
+
|
|
1037
|
+
Response (200):
|
|
1038
|
+
\`\`\`json
|
|
1039
|
+
{
|
|
1040
|
+
"deactivated": true
|
|
1041
|
+
}
|
|
1042
|
+
\`\`\`
|
|
1043
|
+
|
|
1044
|
+
## POST /keys (Admin)
|
|
1045
|
+
|
|
1046
|
+
Generate new license keys.
|
|
1047
|
+
|
|
1048
|
+
Request:
|
|
1049
|
+
\`\`\`json
|
|
1050
|
+
{
|
|
1051
|
+
"app_id": "com.yourname.app",
|
|
1052
|
+
"count": 10,
|
|
1053
|
+
"max_activations": 1,
|
|
1054
|
+
"expires_in_days": 365
|
|
1055
|
+
}
|
|
1056
|
+
\`\`\`
|
|
1057
|
+
|
|
1058
|
+
## GET /keys/:key (Admin)
|
|
1059
|
+
|
|
1060
|
+
Look up a specific key's status and activations.
|
|
1061
|
+
|
|
1062
|
+
## GET /health
|
|
1063
|
+
|
|
1064
|
+
Returns server status and version.
|
|
1065
|
+
`;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// ════════════════════════════════════════════════════════════════
|
|
1069
|
+
// Start server
|
|
1070
|
+
// ════════════════════════════════════════════════════════════════
|
|
1071
|
+
|
|
1072
|
+
const transport = new StdioServerTransport();
|
|
1073
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "archergate-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Archergate License SDK. Lets AI coding assistants generate license-protected software automatically.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"archergate-mcp": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"mcp",
|
|
12
|
+
"model-context-protocol",
|
|
13
|
+
"licensing",
|
|
14
|
+
"copy-protection",
|
|
15
|
+
"ai-tools",
|
|
16
|
+
"claude",
|
|
17
|
+
"cursor",
|
|
18
|
+
"copilot",
|
|
19
|
+
"archergate"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/lailaarcher/archergate"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://archergate.io/sdk",
|
|
27
|
+
"author": "Archergate <hello@archergate.io>",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|