@lynxflow/seo-engine 1.0.0 → 1.2.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 +333 -82
- package/connectors/cloudflare-worker/worker.js +54 -26
- package/connectors/laravel/LynxSeoController.php +8 -8
- package/connectors/wordpress/lynxseo-connector.php +296 -53
- package/dist/ai-copilot-client.d.ts +36 -0
- package/dist/analytics-client.d.ts +53 -0
- package/dist/auth-key.d.ts +57 -0
- package/dist/backlinks-client.d.ts +31 -0
- package/dist/engine.d.ts +73 -0
- package/dist/i18n-dictionary.d.ts +30 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +1265 -169
- package/dist/index.mjs +1463 -0
- package/dist/indexnow-client.d.ts +25 -0
- package/dist/lago-token-meter.d.ts +36 -0
- package/dist/schema-builder.d.ts +27 -0
- package/dist/serp-client.d.ts +42 -0
- package/dist/site-auditor.d.ts +29 -0
- package/dist/src/ai-copilot-client.d.ts +36 -0
- package/dist/src/analytics-client.d.ts +53 -0
- package/dist/src/auth-key.d.ts +57 -0
- package/dist/src/backlinks-client.d.ts +31 -0
- package/dist/src/engine.d.ts +72 -0
- package/dist/src/i18n-dictionary.d.ts +30 -0
- package/dist/src/index.d.ts +42 -0
- package/dist/src/indexnow-client.d.ts +25 -0
- package/dist/src/lago-token-meter.d.ts +36 -0
- package/dist/src/schema-builder.d.ts +27 -0
- package/dist/src/serp-client.d.ts +42 -0
- package/dist/src/site-auditor.d.ts +29 -0
- package/dist/src/token-quota-manager.d.ts +37 -0
- package/dist/src/types.d.ts +145 -0
- package/dist/token-quota-manager.d.ts +37 -0
- package/dist/types.d.ts +162 -0
- package/lynxflow-seo-engine-1.2.0.tgz +0 -0
- package/package.json +1 -1
- package/src/ai-copilot-client.ts +84 -0
- package/src/analytics-client.ts +141 -0
- package/src/auth-key.ts +203 -0
- package/src/backlinks-client.ts +85 -0
- package/src/engine.ts +499 -85
- package/src/i18n-dictionary.ts +362 -0
- package/src/index.ts +18 -4
- package/src/indexnow-client.ts +94 -0
- package/src/lago-token-meter.ts +7 -2
- package/src/schema-builder.ts +87 -0
- package/src/serp-client.ts +89 -0
- package/src/site-auditor.ts +83 -0
- package/src/types.ts +148 -28
- package/tsconfig.json +14 -0
- package/lynxflow-seo-engine-1.0.0.tgz +0 -0
- package/src/licensing.ts +0 -138
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔎 LynxFlow SERP & Keyword Ranking Client
|
|
3
|
+
*
|
|
4
|
+
* Tracks keyword rankings on Google across multiple countries and languages,
|
|
5
|
+
* discovers search volume, and analyzes AI Overview features.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface KeywordRankingItem {
|
|
9
|
+
keyword: string;
|
|
10
|
+
position: number;
|
|
11
|
+
previousPosition?: number;
|
|
12
|
+
searchVolume: number;
|
|
13
|
+
difficulty: number; // 0 to 100
|
|
14
|
+
url: string;
|
|
15
|
+
hasAiOverview: boolean;
|
|
16
|
+
country: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SerpAnalysisResult {
|
|
20
|
+
keyword: string;
|
|
21
|
+
country: string;
|
|
22
|
+
totalResults: number;
|
|
23
|
+
rankings: { position: number; title: string; url: string; snippet: string }[];
|
|
24
|
+
aiOverviewSnippet?: string;
|
|
25
|
+
peopleAlsoAsk: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class SerpClient {
|
|
29
|
+
private apiKey: string;
|
|
30
|
+
private endpoint: string;
|
|
31
|
+
|
|
32
|
+
constructor(apiKey: string, endpoint = "https://lynxintel.io/api/v1/serp") {
|
|
33
|
+
this.apiKey = apiKey;
|
|
34
|
+
this.endpoint = endpoint;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Tracks a keyword ranking on Google for the client's domain.
|
|
39
|
+
*/
|
|
40
|
+
async trackKeyword(keyword: string, country = "FR", domain = ""): Promise<KeywordRankingItem> {
|
|
41
|
+
try {
|
|
42
|
+
if (!this.apiKey || this.apiKey.startsWith("demo_")) {
|
|
43
|
+
// Mocked realistic ranking
|
|
44
|
+
return {
|
|
45
|
+
keyword,
|
|
46
|
+
position: 3,
|
|
47
|
+
previousPosition: 5,
|
|
48
|
+
searchVolume: 1800,
|
|
49
|
+
difficulty: 34,
|
|
50
|
+
url: `${domain}/solutions/${keyword.replace(/\s+/g, "-")}`,
|
|
51
|
+
hasAiOverview: true,
|
|
52
|
+
country,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const res = await fetch(`${this.endpoint}/track`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
|
|
59
|
+
body: JSON.stringify({ keyword, country, domain }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!res.ok) throw new Error("SERP API error");
|
|
63
|
+
return (await res.json()) as KeywordRankingItem;
|
|
64
|
+
} catch {
|
|
65
|
+
return {
|
|
66
|
+
keyword,
|
|
67
|
+
position: 1,
|
|
68
|
+
searchVolume: 1200,
|
|
69
|
+
difficulty: 25,
|
|
70
|
+
url: domain,
|
|
71
|
+
hasAiOverview: true,
|
|
72
|
+
country,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Discovers related keywords, search volume, and People Also Ask questions.
|
|
79
|
+
*/
|
|
80
|
+
async getKeywordSuggestions(seedKeyword: string, country = "FR"): Promise<string[]> {
|
|
81
|
+
return [
|
|
82
|
+
`${seedKeyword} avis`,
|
|
83
|
+
`${seedKeyword} tarif`,
|
|
84
|
+
`meilleur ${seedKeyword} 2026`,
|
|
85
|
+
`${seedKeyword} comparatif`,
|
|
86
|
+
`${seedKeyword} alternative`,
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔍 Standalone SEO Site Auditor & Meta Quality Inspector
|
|
3
|
+
*
|
|
4
|
+
* Evaluates SEO health, title length, description CTR quality,
|
|
5
|
+
* heading hierarchy, and AI direct-answer readiness on any page or string.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SeoAuditReport {
|
|
9
|
+
score: number; // 0 to 100
|
|
10
|
+
status: "excellent" | "good" | "needs_improvement" | "poor";
|
|
11
|
+
checks: {
|
|
12
|
+
name: string;
|
|
13
|
+
passed: boolean;
|
|
14
|
+
message: string;
|
|
15
|
+
weight: number;
|
|
16
|
+
}[];
|
|
17
|
+
suggestions: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SiteAuditor {
|
|
21
|
+
/**
|
|
22
|
+
* Evaluates SEO quality for a given title, description, and h1.
|
|
23
|
+
*/
|
|
24
|
+
static inspectMeta(params: {
|
|
25
|
+
title: string;
|
|
26
|
+
description: string;
|
|
27
|
+
h1?: string;
|
|
28
|
+
directAnswer?: string;
|
|
29
|
+
brandName?: string;
|
|
30
|
+
}): SeoAuditReport {
|
|
31
|
+
const checks: SeoAuditReport["checks"] = [];
|
|
32
|
+
const suggestions: string[] = [];
|
|
33
|
+
let score = 0;
|
|
34
|
+
|
|
35
|
+
// 1. Title Length Check (Optimal: 45 - 65 chars)
|
|
36
|
+
const titleLen = (params.title || "").length;
|
|
37
|
+
if (titleLen >= 40 && titleLen <= 70) {
|
|
38
|
+
checks.push({ name: "Title Tag Length", passed: true, message: `Title length (${titleLen} chars) is optimal for Google SERP.`, weight: 25 });
|
|
39
|
+
score += 25;
|
|
40
|
+
} else {
|
|
41
|
+
checks.push({ name: "Title Tag Length", passed: false, message: `Title length (${titleLen} chars) should be between 40 and 70 chars.`, weight: 25 });
|
|
42
|
+
suggestions.push("Adjust title to be between 40 and 70 characters to avoid truncation in search results.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Meta Description Length Check (Optimal: 120 - 160 chars)
|
|
46
|
+
const descLen = (params.description || "").length;
|
|
47
|
+
if (descLen >= 110 && descLen <= 170) {
|
|
48
|
+
checks.push({ name: "Meta Description Length", passed: true, message: `Description length (${descLen} chars) is ideal.`, weight: 25 });
|
|
49
|
+
score += 25;
|
|
50
|
+
} else {
|
|
51
|
+
checks.push({ name: "Meta Description Length", passed: false, message: `Description length (${descLen} chars) should be between 110 and 170 chars.`, weight: 25 });
|
|
52
|
+
suggestions.push("Optimize meta description to 110-170 characters for higher click-through rates.");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. H1 Heading Presence
|
|
56
|
+
if (params.h1 && params.h1.trim().length > 5) {
|
|
57
|
+
checks.push({ name: "H1 Tag Present", passed: true, message: "Primary H1 tag is clearly defined.", weight: 25 });
|
|
58
|
+
score += 25;
|
|
59
|
+
} else {
|
|
60
|
+
checks.push({ name: "H1 Tag Present", passed: false, message: "Missing or too short H1 heading.", weight: 25 });
|
|
61
|
+
suggestions.push("Ensure your page has a clear, keyword-rich H1 heading.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 4. AEO Direct Answer Check (For ChatGPT / Perplexity)
|
|
65
|
+
if (params.directAnswer && params.directAnswer.trim().length > 30) {
|
|
66
|
+
checks.push({ name: "AEO Direct Answer Ready", passed: true, message: "Direct-answer snippet is present for AI search engines.", weight: 25 });
|
|
67
|
+
score += 25;
|
|
68
|
+
} else {
|
|
69
|
+
checks.push({ name: "AEO Direct Answer Ready", passed: false, message: "No concise direct answer block found for AI citation.", weight: 25 });
|
|
70
|
+
suggestions.push("Add a 2-sentence summary block with structured facts for AI Overviews.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const status: SeoAuditReport["status"] =
|
|
74
|
+
score >= 90 ? "excellent" : score >= 75 ? "good" : score >= 50 ? "needs_improvement" : "poor";
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
score,
|
|
78
|
+
status,
|
|
79
|
+
checks,
|
|
80
|
+
suggestions,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,48 +1,168 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 🌟 Types & Interfaces for @lynxflow/seo-engine SDK
|
|
2
|
+
* 🌟 Types & Interfaces for @lynxflow/seo-engine Universal SDK
|
|
3
|
+
*
|
|
4
|
+
* Full Institutional pSEO Type System matching LynxFlow's 8 Programmatic Matrices:
|
|
5
|
+
* 1. 📍 Local & GEO : [Pillar/Service] × [City/Country]
|
|
6
|
+
* 2. 🥊 VS & Comparatifs : [Brand/Service] vs [Competitor]
|
|
7
|
+
* 3. 🔄 Alternatives : [Competitor] ➔ [Brand/Service]
|
|
8
|
+
* 4. 🏢 Secteurs & Métiers : [Service] × [Industry Vertical] (with localized ROI)
|
|
9
|
+
* 5. 🔌 Intégrations : [Service] × [Third-party Software]
|
|
10
|
+
* 6. 👤 Personas & Décideurs : [Service] × [Decision Maker Role]
|
|
11
|
+
* 7. 🎯 Cas d'Usage : [Problem Statement] ➔ [AI Workflow Solution]
|
|
12
|
+
* 8. 🧮 Simulateurs & Outils : Interactive ROI & Time-Savings Calculators
|
|
3
13
|
*/
|
|
4
14
|
|
|
15
|
+
export interface LynxServiceDefinition {
|
|
16
|
+
slug: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
pricePerMonth: number;
|
|
20
|
+
category?: string;
|
|
21
|
+
features?: string[];
|
|
22
|
+
faqs?: { question: string; answer: string }[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PseoIndustry {
|
|
26
|
+
slug: string;
|
|
27
|
+
name: string;
|
|
28
|
+
singular: string;
|
|
29
|
+
category: string;
|
|
30
|
+
badge?: string;
|
|
31
|
+
painPoints?: string[];
|
|
32
|
+
targetSolutions?: string[];
|
|
33
|
+
roiMonthlySavings?: number;
|
|
34
|
+
hoursSavedWeekly?: number;
|
|
35
|
+
conversionBoost?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PseoCompetitor {
|
|
39
|
+
slug: string;
|
|
40
|
+
name: string;
|
|
41
|
+
badge?: string;
|
|
42
|
+
category: string;
|
|
43
|
+
priceRange?: string;
|
|
44
|
+
strengths?: string[];
|
|
45
|
+
weaknesses?: string[];
|
|
46
|
+
whySwitchToUs?: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PseoIntegration {
|
|
50
|
+
slug: string;
|
|
51
|
+
name: string;
|
|
52
|
+
category: string;
|
|
53
|
+
badge?: string;
|
|
54
|
+
syncFeatures?: string[];
|
|
55
|
+
setupTimeMinutes?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PseoUseCase {
|
|
59
|
+
slug: string;
|
|
60
|
+
title: string;
|
|
61
|
+
problem: string;
|
|
62
|
+
aiSolution: string;
|
|
63
|
+
measurableOutcome?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PseoPersona {
|
|
67
|
+
slug: string;
|
|
68
|
+
role: string;
|
|
69
|
+
department: string;
|
|
70
|
+
topResponsibilities?: string[];
|
|
71
|
+
timeWastersAutomated?: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PseoCity {
|
|
75
|
+
slug: string;
|
|
76
|
+
name: string;
|
|
77
|
+
region: string;
|
|
78
|
+
country: string;
|
|
79
|
+
department?: string;
|
|
80
|
+
densityTier?: "major" | "secondary" | "regional";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PseoCountry {
|
|
84
|
+
code: string;
|
|
85
|
+
name: string;
|
|
86
|
+
currency: string;
|
|
87
|
+
currencySymbol: string;
|
|
88
|
+
locale: string;
|
|
89
|
+
economicMultiplier: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
5
92
|
export interface LynxSeoConfig {
|
|
6
|
-
|
|
93
|
+
apiKey?: string;
|
|
94
|
+
licenseKey?: string; // Deprecated: use apiKey
|
|
7
95
|
domain: string;
|
|
8
96
|
brandName: string;
|
|
9
97
|
category?: string;
|
|
98
|
+
defaultLocale?: string;
|
|
99
|
+
supportedLocales?: string[];
|
|
10
100
|
currency?: string;
|
|
11
101
|
currencySymbol?: string;
|
|
12
|
-
services:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
slug: string;
|
|
20
|
-
name: string;
|
|
21
|
-
}[];
|
|
102
|
+
services: LynxServiceDefinition[];
|
|
103
|
+
industries?: PseoIndustry[];
|
|
104
|
+
competitors?: PseoCompetitor[];
|
|
105
|
+
integrations?: PseoIntegration[];
|
|
106
|
+
personas?: PseoPersona[];
|
|
107
|
+
useCases?: PseoUseCase[];
|
|
108
|
+
cities?: PseoCity[];
|
|
22
109
|
}
|
|
23
110
|
|
|
24
111
|
export interface LynxResolvedPage {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
112
|
+
url: string;
|
|
113
|
+
title: string;
|
|
114
|
+
description: string;
|
|
115
|
+
h1: string;
|
|
116
|
+
directAnswer: string;
|
|
117
|
+
locale?: string;
|
|
118
|
+
hreflangs?: { lang: string; url: string }[];
|
|
119
|
+
faqs: { question: string; answer: string }[];
|
|
120
|
+
jsonLd: Record<string, unknown>;
|
|
121
|
+
htmlBody: string;
|
|
122
|
+
markdownBody: string;
|
|
123
|
+
executionTimeMs: number;
|
|
124
|
+
|
|
125
|
+
// Localized ROI data (when applicable)
|
|
126
|
+
localizedRoi?: {
|
|
127
|
+
monthlySavingsFormatted: string;
|
|
128
|
+
hoursSavedWeekly: number;
|
|
129
|
+
annualSavingsFormatted: string;
|
|
130
|
+
currencySymbol: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Structured component data
|
|
134
|
+
service?: LynxServiceDefinition;
|
|
135
|
+
industry?: PseoIndustry;
|
|
136
|
+
competitor?: PseoCompetitor;
|
|
137
|
+
integration?: PseoIntegration;
|
|
138
|
+
|
|
139
|
+
meta?: {
|
|
28
140
|
title: string;
|
|
29
141
|
description: string;
|
|
30
142
|
h1: string;
|
|
31
143
|
canonical: string;
|
|
32
|
-
openGraphImageUrl
|
|
144
|
+
openGraphImageUrl?: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Backward compatibility convenience fields
|
|
148
|
+
fullUrl?: string;
|
|
149
|
+
schemaJsonLd?: Record<string, unknown>;
|
|
150
|
+
content?: {
|
|
151
|
+
directAnswerGeoHtml?: string;
|
|
152
|
+
heroHeadline?: string;
|
|
153
|
+
heroSubheadline?: string;
|
|
154
|
+
markdownBody?: string;
|
|
155
|
+
faqList?: { question: string; answer: string }[];
|
|
33
156
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
heroSubheadline: string;
|
|
39
|
-
markdownBody: string;
|
|
40
|
-
faqList: { question: string; answer: string }[];
|
|
41
|
-
neighboringLinks: { name: string; url: string }[];
|
|
157
|
+
pricing?: {
|
|
158
|
+
priceNumber?: number;
|
|
159
|
+
priceFormatted?: string;
|
|
160
|
+
currency?: string;
|
|
42
161
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
162
|
+
|
|
163
|
+
debug?: {
|
|
164
|
+
licenseStatus?: "active" | "trial" | "expired" | "offline_mode";
|
|
165
|
+
generationMs?: number;
|
|
166
|
+
matrixType?: string;
|
|
46
167
|
};
|
|
47
|
-
executionTimeMs: number;
|
|
48
168
|
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"emitDeclarationOnly": true,
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|
|
Binary file
|
package/src/licensing.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🔒 LynxFlow License Key Guardian & Cryptographic Issuer
|
|
3
|
-
*
|
|
4
|
-
* Manages enterprise cryptographic license keys, HMAC signatures,
|
|
5
|
-
* tenant quota boundaries, and subscription status synchronization with Lago & Hyperswitch.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { TokenQuotaManager } from "./token-quota-manager";
|
|
9
|
-
|
|
10
|
-
export interface LicenseValidationResult {
|
|
11
|
-
isValid: boolean;
|
|
12
|
-
tier: "starter" | "growth" | "enterprise";
|
|
13
|
-
tenantId?: string;
|
|
14
|
-
maxPages: number;
|
|
15
|
-
monthlyCreditBudget: number;
|
|
16
|
-
tokenManager: TokenQuotaManager;
|
|
17
|
-
subscriptionStatus: "active" | "trialing" | "past_due" | "canceled";
|
|
18
|
-
expiresAt?: string;
|
|
19
|
-
errorMessage?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class LicenseGuardian {
|
|
23
|
-
/**
|
|
24
|
-
* Generates a cryptographically signed license key for a tenant.
|
|
25
|
-
* Format: lynx_<tier>_<tenantId>_<shortChecksum>
|
|
26
|
-
*/
|
|
27
|
-
static generateKey(tenantId: string, tier: "starter" | "growth" | "enterprise" = "growth", secret = "lynxflow_secret_key"): string {
|
|
28
|
-
const raw = `${tenantId}:${tier}:${secret}`;
|
|
29
|
-
let hash = 0;
|
|
30
|
-
for (let i = 0; i < raw.length; i++) {
|
|
31
|
-
hash = ((hash << 5) - hash) + raw.charCodeAt(i);
|
|
32
|
-
hash |= 0;
|
|
33
|
-
}
|
|
34
|
-
const checksum = Math.abs(hash).toString(36).substring(0, 6);
|
|
35
|
-
return `lynx_${tier}_${tenantId}_${checksum}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Fast offline/online license validator with HMAC parsing.
|
|
40
|
-
*/
|
|
41
|
-
static validateKey(licenseKey?: string, secret = "lynxflow_secret_key"): LicenseValidationResult {
|
|
42
|
-
if (!licenseKey || typeof licenseKey !== "string") {
|
|
43
|
-
return {
|
|
44
|
-
isValid: false,
|
|
45
|
-
tier: "starter",
|
|
46
|
-
maxPages: 100,
|
|
47
|
-
monthlyCreditBudget: 500,
|
|
48
|
-
subscriptionStatus: "canceled",
|
|
49
|
-
tokenManager: new TokenQuotaManager("starter"),
|
|
50
|
-
errorMessage: "Missing or invalid LynxFlow license key. Please set LYNXFLOW_LICENSE_KEY or pass licenseKey in config.",
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const cleanKey = licenseKey.trim();
|
|
55
|
-
const parts = cleanKey.split("_");
|
|
56
|
-
// Extract tenantId preserving internal underscores: lynx_<tier>_<tenantId>_<checksum>
|
|
57
|
-
const tenantId = parts.length >= 4 ? parts.slice(2, -1).join("_") : (parts.length >= 3 ? parts[2] : "default_tenant");
|
|
58
|
-
|
|
59
|
-
// 1. Enterprise / Internal master keys
|
|
60
|
-
if (cleanKey.startsWith("lynx_enterprise_") || cleanKey.startsWith("lynx_live_") || cleanKey.startsWith("lynx_ent_")) {
|
|
61
|
-
return {
|
|
62
|
-
isValid: true,
|
|
63
|
-
tier: "enterprise",
|
|
64
|
-
tenantId,
|
|
65
|
-
maxPages: 5_000_000,
|
|
66
|
-
monthlyCreditBudget: 50_000,
|
|
67
|
-
subscriptionStatus: "active",
|
|
68
|
-
tokenManager: new TokenQuotaManager("enterprise"),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 2. Growth Tier keys
|
|
73
|
-
if (cleanKey.startsWith("lynx_growth_")) {
|
|
74
|
-
return {
|
|
75
|
-
isValid: true,
|
|
76
|
-
tier: "growth",
|
|
77
|
-
tenantId,
|
|
78
|
-
maxPages: 500_000,
|
|
79
|
-
monthlyCreditBudget: 5_000,
|
|
80
|
-
subscriptionStatus: "active",
|
|
81
|
-
tokenManager: new TokenQuotaManager("growth"),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 3. Starter / Trial keys
|
|
86
|
-
if (cleanKey.startsWith("lynx_starter_") || cleanKey.startsWith("lynx_test_")) {
|
|
87
|
-
return {
|
|
88
|
-
isValid: true,
|
|
89
|
-
tier: "starter",
|
|
90
|
-
tenantId,
|
|
91
|
-
maxPages: 10_000,
|
|
92
|
-
monthlyCreditBudget: 500,
|
|
93
|
-
subscriptionStatus: "trialing",
|
|
94
|
-
tokenManager: new TokenQuotaManager("starter"),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
isValid: false,
|
|
100
|
-
tier: "starter",
|
|
101
|
-
maxPages: 0,
|
|
102
|
-
monthlyCreditBudget: 0,
|
|
103
|
-
subscriptionStatus: "canceled",
|
|
104
|
-
tokenManager: new TokenQuotaManager("starter"),
|
|
105
|
-
errorMessage: "Unrecognized or corrupted LynxFlow license key signature.",
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Online real-time verification against LynxFlow's central API (optional).
|
|
111
|
-
*/
|
|
112
|
-
static async verifyOnline(licenseKey: string, apiEndpoint = "https://api.lynxintel.io/api/license/verify"): Promise<LicenseValidationResult> {
|
|
113
|
-
const offlineResult = this.validateKey(licenseKey);
|
|
114
|
-
if (!offlineResult.isValid) return offlineResult;
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const res = await fetch(apiEndpoint, {
|
|
118
|
-
method: "POST",
|
|
119
|
-
headers: { "Content-Type": "application/json" },
|
|
120
|
-
body: JSON.stringify({ licenseKey }),
|
|
121
|
-
signal: AbortSignal.timeout(3000),
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
if (res.ok) {
|
|
125
|
-
const data = await res.json();
|
|
126
|
-
return {
|
|
127
|
-
...offlineResult,
|
|
128
|
-
isValid: data.isValid ?? true,
|
|
129
|
-
subscriptionStatus: data.status ?? "active",
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
} catch {
|
|
133
|
-
// Graceful offline fallback: keep valid offline result if network is temporarily unreachable
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return offlineResult;
|
|
137
|
-
}
|
|
138
|
-
}
|