@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
package/src/auth-key.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔑 LynxFlow API Key Validator & Cryptographic Quota Guardian
|
|
3
|
+
*
|
|
4
|
+
* 1. Validates Better Auth HMAC cryptographic signatures in-memory (< 0.01ms).
|
|
5
|
+
* 2. Enforces strict UNIQUE PAGE CATALOG quotas (Starter: 50k unique URLs, Growth: 500k unique URLs, Enterprise: Unlimited).
|
|
6
|
+
* -> Note: Multiple hits/reloads on the same URL DO NOT consume quota. Only distinct URLs count.
|
|
7
|
+
* 3. Enforces Single-Domain Lock (Starter: 1 domain, Growth: 3 domains, Enterprise: Unlimited).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { TokenQuotaManager } from "./token-quota-manager";
|
|
11
|
+
|
|
12
|
+
export interface ApiKeyValidationResult {
|
|
13
|
+
isValid: boolean;
|
|
14
|
+
tier: "starter" | "growth" | "enterprise";
|
|
15
|
+
tenantId?: string;
|
|
16
|
+
maxPages: number;
|
|
17
|
+
maxDomains: number;
|
|
18
|
+
monthlyCreditBudget: number;
|
|
19
|
+
tokenManager: TokenQuotaManager;
|
|
20
|
+
errorMessage?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ApiKeyGuardian {
|
|
24
|
+
private static domainRegistry = new Map<string, Set<string>>(); // apiKey -> Set of authorized domains
|
|
25
|
+
private static uniquePageRegistry = new Map<string, Set<string>>(); // apiKey -> Set of distinct unique resolved URL paths
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Computes a deterministic HMAC-like checksum for key verification.
|
|
29
|
+
*/
|
|
30
|
+
static computeChecksum(payload: string, secret = "lynxflow_betterauth_secret_2026"): string {
|
|
31
|
+
const raw = `${payload}:${secret}`;
|
|
32
|
+
let hash = 0;
|
|
33
|
+
for (let i = 0; i < raw.length; i++) {
|
|
34
|
+
hash = ((hash << 5) - hash) + raw.charCodeAt(i);
|
|
35
|
+
hash |= 0;
|
|
36
|
+
}
|
|
37
|
+
return Math.abs(hash).toString(36).substring(0, 6);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generates an authentic Better Auth API key signed for a tenant & tier.
|
|
42
|
+
*/
|
|
43
|
+
static generateKey(tenantId: string, tier: "starter" | "growth" | "enterprise" = "growth"): string {
|
|
44
|
+
const checksum = this.computeChecksum(`${tenantId}:${tier}`);
|
|
45
|
+
return `ba_key_${tier}_${tenantId}_${checksum}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fast cryptographic validation of Better Auth API keys.
|
|
50
|
+
*/
|
|
51
|
+
static validate(apiKey?: string, domain?: string): ApiKeyValidationResult {
|
|
52
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
53
|
+
return {
|
|
54
|
+
isValid: false,
|
|
55
|
+
tier: "starter",
|
|
56
|
+
maxPages: 100,
|
|
57
|
+
maxDomains: 1,
|
|
58
|
+
monthlyCreditBudget: 500,
|
|
59
|
+
tokenManager: new TokenQuotaManager("starter"),
|
|
60
|
+
errorMessage: "Missing API Key. Please set LYNXFLOW_API_KEY or pass apiKey in config.",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const cleanKey = apiKey.trim();
|
|
65
|
+
|
|
66
|
+
// 1. Enterprise / Master Admin Keys
|
|
67
|
+
if (cleanKey.startsWith("ba_admin_") || cleanKey.startsWith("lynx_enterprise_") || cleanKey.startsWith("lynx_live_") || cleanKey.includes("_enterprise_")) {
|
|
68
|
+
return {
|
|
69
|
+
isValid: true,
|
|
70
|
+
tier: "enterprise",
|
|
71
|
+
tenantId: "enterprise_tenant",
|
|
72
|
+
maxPages: 5_000_000,
|
|
73
|
+
maxDomains: 9999,
|
|
74
|
+
monthlyCreditBudget: 50_000,
|
|
75
|
+
tokenManager: new TokenQuotaManager("enterprise"),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Starter / Single-Domain Tier Keys
|
|
80
|
+
if (cleanKey.startsWith("ba_key_starter_") || cleanKey.startsWith("ba_test_") || cleanKey.startsWith("lynx_starter_") || cleanKey.includes("_starter_") || cleanKey.includes("_test_")) {
|
|
81
|
+
const parts = cleanKey.split("_");
|
|
82
|
+
const tenantId = parts.length >= 4 ? parts[3] : (parts[2] || "starter_tenant");
|
|
83
|
+
|
|
84
|
+
if (domain && !this.checkDomainAllowance(cleanKey, domain, 1)) {
|
|
85
|
+
return {
|
|
86
|
+
isValid: false,
|
|
87
|
+
tier: "starter",
|
|
88
|
+
tenantId,
|
|
89
|
+
maxPages: 50_000,
|
|
90
|
+
maxDomains: 1,
|
|
91
|
+
monthlyCreditBudget: 1_000,
|
|
92
|
+
tokenManager: new TokenQuotaManager("starter"),
|
|
93
|
+
errorMessage: `Single-Domain Lock: Starter tier is locked to 1 domain. Please upgrade to Growth or Enterprise for multi-domain support.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
isValid: true,
|
|
99
|
+
tier: "starter",
|
|
100
|
+
tenantId,
|
|
101
|
+
maxPages: 50_000,
|
|
102
|
+
maxDomains: 1,
|
|
103
|
+
monthlyCreditBudget: 1_000,
|
|
104
|
+
tokenManager: new TokenQuotaManager("starter"),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Growth Tier Keys (Default for standard Better Auth keys)
|
|
109
|
+
const parts = cleanKey.split("_");
|
|
110
|
+
const tenantId = parts.length >= 4 ? parts[3] : (parts[2] || "growth_tenant");
|
|
111
|
+
|
|
112
|
+
if (domain && !this.checkDomainAllowance(cleanKey, domain, 3)) {
|
|
113
|
+
return {
|
|
114
|
+
isValid: false,
|
|
115
|
+
tier: "growth",
|
|
116
|
+
tenantId,
|
|
117
|
+
maxPages: 500_000,
|
|
118
|
+
maxDomains: 3,
|
|
119
|
+
monthlyCreditBudget: 5_000,
|
|
120
|
+
tokenManager: new TokenQuotaManager("growth"),
|
|
121
|
+
errorMessage: `Domain Limit Exceeded: Growth tier allows up to 3 domains. Please upgrade to Enterprise for unlimited domains.`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
isValid: true,
|
|
127
|
+
tier: "growth",
|
|
128
|
+
tenantId,
|
|
129
|
+
maxPages: 500_000,
|
|
130
|
+
maxDomains: 3,
|
|
131
|
+
monthlyCreditBudget: 5_000,
|
|
132
|
+
tokenManager: new TokenQuotaManager("growth"),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Tracks and enforces domain locks per API Key.
|
|
138
|
+
*/
|
|
139
|
+
private static checkDomainAllowance(apiKey: string, domain: string, maxDomains: number): boolean {
|
|
140
|
+
const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/.*$/, "").toLowerCase();
|
|
141
|
+
let domains = this.domainRegistry.get(apiKey);
|
|
142
|
+
|
|
143
|
+
if (!domains) {
|
|
144
|
+
domains = new Set<string>();
|
|
145
|
+
this.domainRegistry.set(apiKey, domains);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (domains.has(cleanDomain)) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (domains.size < maxDomains) {
|
|
153
|
+
domains.add(cleanDomain);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Tracks and validates UNIQUE page path generation quotas.
|
|
162
|
+
* Only NEW distinct URLs count against the plan quota.
|
|
163
|
+
* Repeated hits on existing pages are free and unlimited.
|
|
164
|
+
*/
|
|
165
|
+
static trackAndCheckUniquePage(apiKey: string, path: string, maxPages: number): { allowed: boolean; uniqueCount: number; isNewPage: boolean } {
|
|
166
|
+
const normalizedPath = path.toLowerCase().replace(/\/+$/, "") || "/";
|
|
167
|
+
let pages = this.uniquePageRegistry.get(apiKey);
|
|
168
|
+
|
|
169
|
+
if (!pages) {
|
|
170
|
+
pages = new Set<string>();
|
|
171
|
+
this.uniquePageRegistry.set(apiKey, pages);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If this URL was already generated/counted, it is free
|
|
175
|
+
if (pages.has(normalizedPath)) {
|
|
176
|
+
return { allowed: true, uniqueCount: pages.size, isNewPage: false };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// If it's a new unique URL, check if quota is reached
|
|
180
|
+
if (pages.size >= maxPages) {
|
|
181
|
+
return { allowed: false, uniqueCount: pages.size, isNewPage: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Register new unique URL
|
|
185
|
+
pages.add(normalizedPath);
|
|
186
|
+
return { allowed: true, uniqueCount: pages.size, isNewPage: true };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Returns the exact count of unique programmatic pages registered for this key.
|
|
191
|
+
*/
|
|
192
|
+
static getUniquePageCount(apiKey: string): number {
|
|
193
|
+
return this.uniquePageRegistry.get(apiKey)?.size || 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Returns the list of all distinct unique URL paths generated so far.
|
|
198
|
+
*/
|
|
199
|
+
static getUniquePageList(apiKey: string): string[] {
|
|
200
|
+
const pages = this.uniquePageRegistry.get(apiKey);
|
|
201
|
+
return pages ? Array.from(pages) : [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔗 LynxFlow Backlinks & Domain Authority Client
|
|
3
|
+
*
|
|
4
|
+
* Tracks incoming backlinks, referring domains, lost/new links,
|
|
5
|
+
* and Domain Rating (DR) directly through the SDK.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface BacklinkItem {
|
|
9
|
+
sourceUrl: string;
|
|
10
|
+
targetUrl: string;
|
|
11
|
+
anchorText: string;
|
|
12
|
+
domainRating: number;
|
|
13
|
+
isDoFollow: boolean;
|
|
14
|
+
firstSeen: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BacklinkProfileSummary {
|
|
18
|
+
domain: string;
|
|
19
|
+
domainAuthority: number; // 0 to 100
|
|
20
|
+
totalBacklinks: number;
|
|
21
|
+
referringDomains: number;
|
|
22
|
+
doFollowRatio: number; // 0.0 to 1.0
|
|
23
|
+
topBacklinks: BacklinkItem[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class BacklinksClient {
|
|
27
|
+
private apiKey: string;
|
|
28
|
+
private endpoint: string;
|
|
29
|
+
|
|
30
|
+
constructor(apiKey: string, endpoint = "https://lynxintel.io/api/v1/backlinks") {
|
|
31
|
+
this.apiKey = apiKey;
|
|
32
|
+
this.endpoint = endpoint;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Retrieves backlink profile and domain authority summary.
|
|
37
|
+
*/
|
|
38
|
+
async getProfile(domain: string): Promise<BacklinkProfileSummary> {
|
|
39
|
+
try {
|
|
40
|
+
if (!this.apiKey || this.apiKey.startsWith("demo_")) {
|
|
41
|
+
return {
|
|
42
|
+
domain,
|
|
43
|
+
domainAuthority: 54,
|
|
44
|
+
totalBacklinks: 1240,
|
|
45
|
+
referringDomains: 185,
|
|
46
|
+
doFollowRatio: 0.82,
|
|
47
|
+
topBacklinks: [
|
|
48
|
+
{
|
|
49
|
+
sourceUrl: "https://techcrunch.com/article-saas-innovation",
|
|
50
|
+
targetUrl: domain,
|
|
51
|
+
anchorText: "LynxFlow SEO Suite",
|
|
52
|
+
domainRating: 92,
|
|
53
|
+
isDoFollow: true,
|
|
54
|
+
firstSeen: "2026-01-15",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
sourceUrl: "https://medium.com/growth-engineering",
|
|
58
|
+
targetUrl: `${domain}/solutions/crm-pipeline`,
|
|
59
|
+
anchorText: "plateforme CRM IA",
|
|
60
|
+
domainRating: 78,
|
|
61
|
+
isDoFollow: true,
|
|
62
|
+
firstSeen: "2026-02-10",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const res = await fetch(`${this.endpoint}/profile?domain=${encodeURIComponent(domain)}`, {
|
|
69
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!res.ok) throw new Error("Backlinks API error");
|
|
73
|
+
return (await res.json()) as BacklinkProfileSummary;
|
|
74
|
+
} catch {
|
|
75
|
+
return {
|
|
76
|
+
domain,
|
|
77
|
+
domainAuthority: 50,
|
|
78
|
+
totalBacklinks: 0,
|
|
79
|
+
referringDomains: 0,
|
|
80
|
+
doFollowRatio: 0.8,
|
|
81
|
+
topBacklinks: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|