preflight-scavenger 0.2.0-beta.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 +79 -0
- package/index.js +2320 -0
- package/logger.js +64 -0
- package/mcp-instructions.md +65 -0
- package/package.json +81 -0
- package/remediationEngine.js +243 -0
- package/scaffoldEngine.js +420 -0
- package/src/licensing/licenseManager.js +248 -0
- package/src/mcp/server.js +172 -0
- package/taintTracker.js +393 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const ParserBinding = require("web-tree-sitter");
|
|
4
|
+
|
|
5
|
+
const Parser = ParserBinding.Parser || ParserBinding.default?.Parser || ParserBinding.default || ParserBinding;
|
|
6
|
+
const Language = ParserBinding.Language || ParserBinding.default?.Language;
|
|
7
|
+
const SERVER_ONLY_MODULES = new Set(["fs", "node:fs", "pg", "child_process", "node:child_process"]);
|
|
8
|
+
const CUSTOM_BACKEND_PATTERN = /(?:^|\/)(?:server|backend|db|data|database)(?:\/|$)/i;
|
|
9
|
+
|
|
10
|
+
let parserReady;
|
|
11
|
+
let javascriptLanguage;
|
|
12
|
+
|
|
13
|
+
async function initializeParser() {
|
|
14
|
+
if (!parserReady) {
|
|
15
|
+
parserReady = Parser.init?.();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (parserReady) {
|
|
19
|
+
await parserReady;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!javascriptLanguage) {
|
|
23
|
+
const wasmPath = require.resolve("tree-sitter-javascript/tree-sitter-javascript.wasm");
|
|
24
|
+
javascriptLanguage = await Language.load(wasmPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function parseJavaScript(sourceCode) {
|
|
29
|
+
await initializeParser();
|
|
30
|
+
const parser = new Parser();
|
|
31
|
+
parser.setLanguage(javascriptLanguage);
|
|
32
|
+
return parser.parse(sourceCode);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getNodeText(node, sourceCode) {
|
|
36
|
+
return sourceCode.slice(node.startIndex, node.endIndex);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toByteIndex(sourceCode, stringIndex) {
|
|
40
|
+
return Buffer.byteLength(sourceCode.slice(0, stringIndex), "utf8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toByteRange(sourceCode, node) {
|
|
44
|
+
const raw = getNodeText(node, sourceCode);
|
|
45
|
+
const startIndex = toByteIndex(sourceCode, node.startIndex);
|
|
46
|
+
return {
|
|
47
|
+
startIndex,
|
|
48
|
+
endIndex: startIndex + Buffer.byteLength(raw, "utf8"),
|
|
49
|
+
raw
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function childByFieldName(node, fieldName) {
|
|
54
|
+
return typeof node.childForFieldName === "function" ? node.childForFieldName(fieldName) : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function unquote(value) {
|
|
58
|
+
return value.trim().replace(/;$/, "").trim().replace(/^['"`]|['"`]$/g, "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function walk(node, visitor) {
|
|
62
|
+
if (!node) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
visitor(node);
|
|
67
|
+
for (let index = 0; index < node.childCount; index += 1) {
|
|
68
|
+
walk(node.child(index), visitor);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isClientDirective(rootNode, sourceCode) {
|
|
73
|
+
for (let index = 0; index < rootNode.namedChildCount; index += 1) {
|
|
74
|
+
const child = rootNode.namedChild(index);
|
|
75
|
+
if (child.type === "expression_statement" && unquote(getNodeText(child, sourceCode)) === "use client") {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isServerModule(source) {
|
|
84
|
+
return SERVER_ONLY_MODULES.has(source) || CUSTOM_BACKEND_PATTERN.test(source);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getImportSource(node, sourceCode) {
|
|
88
|
+
for (let index = 0; index < node.namedChildCount; index += 1) {
|
|
89
|
+
const child = node.namedChild(index);
|
|
90
|
+
if (child.type === "string") {
|
|
91
|
+
return unquote(getNodeText(child, sourceCode));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectIdentifiers(node, sourceCode, names = new Set()) {
|
|
99
|
+
if (!node) {
|
|
100
|
+
return names;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (node.type === "identifier") {
|
|
104
|
+
names.add(getNodeText(node, sourceCode));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (let index = 0; index < node.namedChildCount; index += 1) {
|
|
108
|
+
collectIdentifiers(node.namedChild(index), sourceCode, names);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return names;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function collectServerDependencies(rootNode, sourceCode) {
|
|
115
|
+
const dependencies = [];
|
|
116
|
+
const identifiers = new Set();
|
|
117
|
+
|
|
118
|
+
for (let index = 0; index < rootNode.namedChildCount; index += 1) {
|
|
119
|
+
const node = rootNode.namedChild(index);
|
|
120
|
+
if (node.type !== "import_statement") {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const source = getImportSource(node, sourceCode);
|
|
125
|
+
if (!source || !isServerModule(source)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
dependencies.push(getNodeText(node, sourceCode).trim().replace(/;?$/, ";"));
|
|
130
|
+
for (const identifier of collectIdentifiers(node, sourceCode)) {
|
|
131
|
+
identifiers.add(identifier);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { dependencies, identifiers };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function nodeContainsServerIdentifier(node, sourceCode, serverIdentifiers) {
|
|
139
|
+
let found = false;
|
|
140
|
+
walk(node, (child) => {
|
|
141
|
+
if (found || child.type !== "identifier") {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (serverIdentifiers.has(getNodeText(child, sourceCode))) {
|
|
146
|
+
found = true;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return found;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function findFunctionContainer(node) {
|
|
154
|
+
let current = node;
|
|
155
|
+
while (current) {
|
|
156
|
+
if (current.type === "function_declaration") {
|
|
157
|
+
return current;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (current.type === "lexical_declaration" && isFunctionValuedLexicalDeclaration(current)) {
|
|
161
|
+
return current;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (current.type === "arrow_function") {
|
|
165
|
+
let parent = current.parent;
|
|
166
|
+
while (parent) {
|
|
167
|
+
if (parent.type === "lexical_declaration") {
|
|
168
|
+
return parent;
|
|
169
|
+
}
|
|
170
|
+
parent = parent.parent;
|
|
171
|
+
}
|
|
172
|
+
return current;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
current = current.parent;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isFunctionValuedLexicalDeclaration(node) {
|
|
182
|
+
for (let index = 0; index < node.namedChildCount; index += 1) {
|
|
183
|
+
const child = node.namedChild(index);
|
|
184
|
+
if (child.type !== "variable_declarator") {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const value = childByFieldName(child, "value");
|
|
189
|
+
if (value?.type === "arrow_function" || value?.type === "function_expression") {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getFunctionName(container, sourceCode) {
|
|
198
|
+
if (container.type === "function_declaration") {
|
|
199
|
+
const name = childByFieldName(container, "name");
|
|
200
|
+
return name ? getNodeText(name, sourceCode) : "serverAction";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const declarator = container.type === "lexical_declaration"
|
|
204
|
+
? Array.from({ length: container.namedChildCount }, (_, index) => container.namedChild(index))
|
|
205
|
+
.find((child) => child.type === "variable_declarator")
|
|
206
|
+
: null;
|
|
207
|
+
const name = declarator ? childByFieldName(declarator, "name") : null;
|
|
208
|
+
return name ? getNodeText(name, sourceCode) : "serverAction";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeExportedFunction(functionText, functionName) {
|
|
212
|
+
const trimmed = functionText.trim();
|
|
213
|
+
|
|
214
|
+
if (/^export\s+/.test(trimmed)) {
|
|
215
|
+
return trimmed;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (/^(?:const|let|var)\s+/.test(trimmed)) {
|
|
219
|
+
return trimmed.replace(/^(?:const|let|var)\s+/, "export const ");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (/^async\s+function\s+/.test(trimmed) || /^function\s+/.test(trimmed)) {
|
|
223
|
+
return trimmed.replace(/^(async\s+)?function\s+([A-Za-z_$][\w$]*)?/, (match, asyncPrefix = "", name = functionName) => {
|
|
224
|
+
return `export const ${name} = ${asyncPrefix || ""}function`;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return `export const ${functionName} = ${trimmed}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function findServerSideLeaks(rootNode, sourceCode) {
|
|
232
|
+
if (!isClientDirective(rootNode, sourceCode)) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { dependencies, identifiers } = collectServerDependencies(rootNode, sourceCode);
|
|
237
|
+
if (identifiers.size === 0) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
const leaks = [];
|
|
243
|
+
walk(rootNode, (node) => {
|
|
244
|
+
if (node.type !== "call_expression" && node.type !== "new_expression" && node.type !== "member_expression") {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!nodeContainsServerIdentifier(node, sourceCode, identifiers)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const container = findFunctionContainer(node);
|
|
253
|
+
if (!container) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const range = toByteRange(sourceCode, container);
|
|
258
|
+
const seenKey = `${range.startIndex}:${range.endIndex}`;
|
|
259
|
+
if (seen.has(seenKey)) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
seen.add(seenKey);
|
|
264
|
+
leaks.push({
|
|
265
|
+
startIndex: range.startIndex,
|
|
266
|
+
endIndex: range.endIndex,
|
|
267
|
+
rawFunctionText: range.raw,
|
|
268
|
+
functionName: getFunctionName(container, sourceCode),
|
|
269
|
+
dependencies: [...new Set(dependencies)]
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return leaks;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function scaffoldServerActionFile(originalFilePath, functionText, functionName, options = {}) {
|
|
277
|
+
const actionFilePath = path.join(path.dirname(originalFilePath), "actions.ts");
|
|
278
|
+
const dependencies = [...new Set(options.dependencies || [])];
|
|
279
|
+
const lines = [
|
|
280
|
+
"\"use server\";",
|
|
281
|
+
"",
|
|
282
|
+
...dependencies,
|
|
283
|
+
...(dependencies.length > 0 ? [""] : []),
|
|
284
|
+
normalizeExportedFunction(functionText, functionName),
|
|
285
|
+
""
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
await fs.writeFile(actionFilePath, lines.join("\n"), "utf8");
|
|
289
|
+
return actionFilePath;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function byteSplice(source, startIndex, endIndex, replacement = "") {
|
|
293
|
+
const sourceBytes = Buffer.from(source, "utf8");
|
|
294
|
+
const replacementBytes = Buffer.from(replacement, "utf8");
|
|
295
|
+
return Buffer.concat([
|
|
296
|
+
sourceBytes.subarray(0, startIndex),
|
|
297
|
+
replacementBytes,
|
|
298
|
+
sourceBytes.subarray(endIndex)
|
|
299
|
+
]).toString("utf8");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getBridgeImportInsertionIndex(source) {
|
|
303
|
+
const directiveMatch = source.match(/^\s*["']use client["']\s*;?[ \t]*(?:\r?\n)?/);
|
|
304
|
+
return directiveMatch ? directiveMatch[0].length : 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function injectActionBridge(originalSource, startIndex, endIndex, functionName) {
|
|
308
|
+
const withoutFunction = byteSplice(originalSource, startIndex, endIndex, "");
|
|
309
|
+
const bridgeImport = `import { ${functionName} } from './actions';\n`;
|
|
310
|
+
|
|
311
|
+
if (withoutFunction.includes(bridgeImport.trim())) {
|
|
312
|
+
return withoutFunction;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const insertionIndex = getBridgeImportInsertionIndex(withoutFunction);
|
|
316
|
+
return `${withoutFunction.slice(0, insertionIndex)}${bridgeImport}${withoutFunction.slice(insertionIndex)}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function treeContainsUnsafeNode(node) {
|
|
320
|
+
if (!node) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const isMissing = typeof node.isMissing === "function" ? node.isMissing() : node.isMissing === true;
|
|
325
|
+
if (node.type === "ERROR" || node.type === "MISSING" || isMissing) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (let index = 0; index < node.childCount; index += 1) {
|
|
330
|
+
if (treeContainsUnsafeNode(node.child(index))) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function assertSyntaxSafe(sourceCode) {
|
|
339
|
+
const tree = await parseJavaScript(sourceCode);
|
|
340
|
+
try {
|
|
341
|
+
if (treeContainsUnsafeNode(tree.rootNode)) {
|
|
342
|
+
throw new Error("Scaffold Syntax Violation");
|
|
343
|
+
}
|
|
344
|
+
} finally {
|
|
345
|
+
tree.delete?.();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function readExistingFile(filePath) {
|
|
350
|
+
try {
|
|
351
|
+
return await fs.readFile(filePath, "utf8");
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (error.code === "ENOENT") {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function restoreFile(filePath, contents) {
|
|
361
|
+
if (contents === null) {
|
|
362
|
+
try {
|
|
363
|
+
await fs.unlink(filePath);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (error.code !== "ENOENT") {
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
await fs.writeFile(filePath, contents, "utf8");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function applyScaffoldTransaction(originalFilePath, leak) {
|
|
376
|
+
const actionFilePath = path.join(path.dirname(originalFilePath), "actions.ts");
|
|
377
|
+
const originalClient = await fs.readFile(originalFilePath, "utf8");
|
|
378
|
+
const originalActions = await readExistingFile(actionFilePath);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
await scaffoldServerActionFile(originalFilePath, leak.rawFunctionText, leak.functionName, {
|
|
382
|
+
dependencies: leak.dependencies || []
|
|
383
|
+
});
|
|
384
|
+
const nextClient = injectActionBridge(originalClient, leak.startIndex, leak.endIndex, leak.functionName);
|
|
385
|
+
await fs.writeFile(originalFilePath, nextClient, "utf8");
|
|
386
|
+
|
|
387
|
+
await assertSyntaxSafe(await fs.readFile(actionFilePath, "utf8"));
|
|
388
|
+
await assertSyntaxSafe(await fs.readFile(originalFilePath, "utf8"));
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
status: "APPLIED",
|
|
392
|
+
clientFile: originalFilePath,
|
|
393
|
+
actionFile: actionFilePath
|
|
394
|
+
};
|
|
395
|
+
} catch (error) {
|
|
396
|
+
const rollbackErrors = [];
|
|
397
|
+
for (const [filePath, contents] of [
|
|
398
|
+
[actionFilePath, originalActions],
|
|
399
|
+
[originalFilePath, originalClient]
|
|
400
|
+
]) {
|
|
401
|
+
try {
|
|
402
|
+
await restoreFile(filePath, contents);
|
|
403
|
+
} catch (rollbackError) {
|
|
404
|
+
rollbackErrors.push(rollbackError);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (rollbackErrors.length > 0) {
|
|
408
|
+
error.rollbackErrors = rollbackErrors;
|
|
409
|
+
}
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = {
|
|
415
|
+
applyScaffoldTransaction,
|
|
416
|
+
findServerSideLeaks,
|
|
417
|
+
injectActionBridge,
|
|
418
|
+
parseJavaScript,
|
|
419
|
+
scaffoldServerActionFile
|
|
420
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const https = require("node:https");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = ".preflight";
|
|
7
|
+
const CONFIG_FILE = "config.json";
|
|
8
|
+
const FREE_FIX_LIMIT = 5;
|
|
9
|
+
const LEMON_SQUEEZY_ACTIVATE_URL = "https://api.lemonsqueezy.com/v1/licenses/activate";
|
|
10
|
+
const LEMON_SQUEEZY_VALIDATE_URL = "https://api.lemonsqueezy.com/v1/licenses/validate";
|
|
11
|
+
const FREE_FIXES_EXHAUSTED_MESSAGE =
|
|
12
|
+
"\u26a0\ufe0f Free fixes exhausted (5/5). Upgrade to PreFlight Pro for unlimited AI auto-fixes for a one-time payment of $49 / \u20b91999: https://yourwebsite.com/buy";
|
|
13
|
+
const INVALID_LICENSE_MESSAGE =
|
|
14
|
+
"\u274c License is inactive or invalid. Please run 'preflight activate <key>' with a valid key.";
|
|
15
|
+
const ACTIVATION_MESSAGE = "\u2705 PreFlight Pro activated successfully! Unlimited AI auto-fixes unlocked.";
|
|
16
|
+
const EMAIL_MISMATCH_MESSAGE = "\u274c Email does not match the purchase record.";
|
|
17
|
+
const OFFLINE_ERROR_CODES = new Set(["EAI_AGAIN", "ECONNRESET", "ETIMEDOUT", "ENETUNREACH", "ENOTFOUND", "ECONNREFUSED"]);
|
|
18
|
+
|
|
19
|
+
function getConfigPath(homeDir = os.homedir()) {
|
|
20
|
+
return path.join(homeDir, CONFIG_DIR, CONFIG_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function defaultConfig() {
|
|
24
|
+
return {
|
|
25
|
+
freeFixesUsed: 0,
|
|
26
|
+
licenseKey: null,
|
|
27
|
+
instanceId: null
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeConfig(config = {}) {
|
|
32
|
+
const freeFixesUsed = Number.isInteger(config.freeFixesUsed) && config.freeFixesUsed > 0 ? config.freeFixesUsed : 0;
|
|
33
|
+
const licenseKey = typeof config.licenseKey === "string" && config.licenseKey.trim() ? config.licenseKey.trim() : null;
|
|
34
|
+
const instanceId = typeof config.instanceId === "string" && config.instanceId.trim() ? config.instanceId.trim() : null;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
freeFixesUsed,
|
|
38
|
+
licenseKey,
|
|
39
|
+
instanceId
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readConfig(options = {}) {
|
|
44
|
+
const configPath = getConfigPath(options.homeDir);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const raw = await fs.promises.readFile(configPath, "utf8");
|
|
48
|
+
return normalizeConfig(JSON.parse(raw));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code === "ENOENT") {
|
|
51
|
+
return defaultConfig();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function writeConfig(config, options = {}) {
|
|
59
|
+
const configPath = getConfigPath(options.homeDir);
|
|
60
|
+
await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
|
|
61
|
+
await fs.promises.writeFile(configPath, `${JSON.stringify(normalizeConfig(config), null, 2)}\n`, {
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
mode: 0o600
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function postFormUrlEncoded(request) {
|
|
68
|
+
const body = request.body || "";
|
|
69
|
+
const url = new URL(request.url || LEMON_SQUEEZY_VALIDATE_URL);
|
|
70
|
+
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const req = https.request(
|
|
73
|
+
url,
|
|
74
|
+
{
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
Accept: "application/json",
|
|
78
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
79
|
+
"Content-Length": Buffer.byteLength(body)
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
(response) => {
|
|
83
|
+
let responseBody = "";
|
|
84
|
+
response.setEncoding("utf8");
|
|
85
|
+
response.on("data", (chunk) => {
|
|
86
|
+
responseBody += chunk;
|
|
87
|
+
});
|
|
88
|
+
response.on("end", () => {
|
|
89
|
+
try {
|
|
90
|
+
resolve(JSON.parse(responseBody || "{}"));
|
|
91
|
+
} catch (error) {
|
|
92
|
+
reject(error);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
req.on("error", reject);
|
|
99
|
+
req.write(body);
|
|
100
|
+
req.end();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isOfflineError(error) {
|
|
105
|
+
return OFFLINE_ERROR_CODES.has(error?.code);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function freePermission(config) {
|
|
109
|
+
if (config.freeFixesUsed < FREE_FIX_LIMIT) {
|
|
110
|
+
return {
|
|
111
|
+
allowed: true,
|
|
112
|
+
tier: "free",
|
|
113
|
+
remaining: FREE_FIX_LIMIT - config.freeFixesUsed
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
allowed: false,
|
|
119
|
+
tier: "free",
|
|
120
|
+
message: FREE_FIXES_EXHAUSTED_MESSAGE
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function verifyFixPermission(options = {}) {
|
|
125
|
+
const config = await readConfig(options);
|
|
126
|
+
|
|
127
|
+
if (config.licenseKey) {
|
|
128
|
+
const payload = {
|
|
129
|
+
license_key: config.licenseKey
|
|
130
|
+
};
|
|
131
|
+
if (config.instanceId) {
|
|
132
|
+
payload.instance_id = config.instanceId;
|
|
133
|
+
}
|
|
134
|
+
const body = new URLSearchParams(payload).toString();
|
|
135
|
+
const requestLicenseValidation = options.requestLicenseValidation || postFormUrlEncoded;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const result = await requestLicenseValidation({
|
|
139
|
+
url: LEMON_SQUEEZY_VALIDATE_URL,
|
|
140
|
+
body
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (result?.valid) {
|
|
144
|
+
return { allowed: true, tier: "pro" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await writeConfig(
|
|
148
|
+
{
|
|
149
|
+
...config,
|
|
150
|
+
licenseKey: null,
|
|
151
|
+
instanceId: null
|
|
152
|
+
},
|
|
153
|
+
options
|
|
154
|
+
);
|
|
155
|
+
return {
|
|
156
|
+
allowed: false,
|
|
157
|
+
tier: "pro",
|
|
158
|
+
message: INVALID_LICENSE_MESSAGE
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (isOfflineError(error)) {
|
|
162
|
+
return { allowed: true, tier: "pro", offline: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return freePermission(config);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function activateLicenseKey(key, userEmail, options = {}) {
|
|
173
|
+
if (typeof userEmail === "object" && userEmail !== null) {
|
|
174
|
+
options = userEmail;
|
|
175
|
+
userEmail = undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const licenseKey = typeof key === "string" ? key.trim() : "";
|
|
179
|
+
if (!licenseKey) {
|
|
180
|
+
throw new Error("A license key is required.");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const requestLicenseActivation = options.requestLicenseActivation || postFormUrlEncoded;
|
|
184
|
+
const hostname = options.hostname || os.hostname;
|
|
185
|
+
const body = new URLSearchParams({
|
|
186
|
+
license_key: licenseKey,
|
|
187
|
+
instance_name: hostname()
|
|
188
|
+
}).toString();
|
|
189
|
+
const result = await requestLicenseActivation({
|
|
190
|
+
url: LEMON_SQUEEZY_ACTIVATE_URL,
|
|
191
|
+
body
|
|
192
|
+
});
|
|
193
|
+
const instanceId = result?.instance?.id || null;
|
|
194
|
+
|
|
195
|
+
if (!result?.activated || !instanceId) {
|
|
196
|
+
throw new Error(result?.error || "License activation failed.");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (result.meta?.customer_email && result.meta.customer_email !== userEmail) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
message: EMAIL_MISMATCH_MESSAGE
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const config = await readConfig(options);
|
|
207
|
+
await writeConfig(
|
|
208
|
+
{
|
|
209
|
+
...config,
|
|
210
|
+
licenseKey,
|
|
211
|
+
instanceId
|
|
212
|
+
},
|
|
213
|
+
options
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
success: true,
|
|
218
|
+
activated: true,
|
|
219
|
+
message: ACTIVATION_MESSAGE,
|
|
220
|
+
instanceId
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function recordFreeFixUsage(options = {}) {
|
|
225
|
+
const config = await readConfig(options);
|
|
226
|
+
const nextConfig = {
|
|
227
|
+
...config,
|
|
228
|
+
freeFixesUsed: config.freeFixesUsed + 1
|
|
229
|
+
};
|
|
230
|
+
await writeConfig(nextConfig, options);
|
|
231
|
+
return nextConfig;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
ACTIVATION_MESSAGE,
|
|
236
|
+
EMAIL_MISMATCH_MESSAGE,
|
|
237
|
+
FREE_FIXES_EXHAUSTED_MESSAGE,
|
|
238
|
+
FREE_FIX_LIMIT,
|
|
239
|
+
INVALID_LICENSE_MESSAGE,
|
|
240
|
+
LEMON_SQUEEZY_ACTIVATE_URL,
|
|
241
|
+
LEMON_SQUEEZY_VALIDATE_URL,
|
|
242
|
+
activateLicenseKey,
|
|
243
|
+
getConfigPath,
|
|
244
|
+
readConfig,
|
|
245
|
+
recordFreeFixUsage,
|
|
246
|
+
verifyFixPermission,
|
|
247
|
+
writeConfig
|
|
248
|
+
};
|