@rafaylabs/animation-mcp 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +111 -2
- package/bin/cli.js +50 -0
- package/dist/api/catalog-client.d.ts +27 -0
- package/dist/api/catalog-client.js +67 -0
- package/dist/api/server.d.ts +29 -0
- package/dist/api/server.js +55 -0
- package/dist/auth/cli-login.d.ts +3 -0
- package/dist/auth/cli-login.js +121 -0
- package/dist/auth/client.d.ts +45 -0
- package/dist/auth/client.js +170 -0
- package/dist/auth/config.d.ts +36 -0
- package/dist/auth/config.js +106 -0
- package/dist/auth/device-login.d.ts +2 -0
- package/dist/auth/device-login.js +63 -0
- package/dist/auth/product.d.ts +17 -0
- package/dist/auth/product.js +9 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +262 -0
- package/dist/theming/classifier.d.ts +16 -0
- package/dist/theming/classifier.js +74 -0
- package/dist/theming/color-walker.d.ts +19 -0
- package/dist/theming/color-walker.js +303 -0
- package/dist/theming/engine.d.ts +29 -0
- package/dist/theming/engine.js +144 -0
- package/dist/theming/oklch.d.ts +63 -0
- package/dist/theming/oklch.js +147 -0
- package/dist/tools/code.d.ts +20 -0
- package/dist/tools/code.js +104 -0
- package/dist/tools/get.d.ts +13 -0
- package/dist/tools/get.js +10 -0
- package/dist/tools/save.d.ts +22 -0
- package/dist/tools/save.js +78 -0
- package/dist/tools/schemas.d.ts +137 -0
- package/dist/tools/schemas.js +50 -0
- package/dist/tools/search.d.ts +29 -0
- package/dist/tools/search.js +22 -0
- package/dist/tools/theme.d.ts +28 -0
- package/dist/tools/theme.js +66 -0
- package/dist/utils/lottie-file.d.ts +5 -0
- package/dist/utils/lottie-file.js +47 -0
- package/dist/utils/workspace.d.ts +20 -0
- package/dist/utils/workspace.js +116 -0
- package/package.json +56 -4
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { loadConfig, saveConfig, grantLicense, isLicenseValid } from './config.js';
|
|
2
|
+
import { LICENSE_PRODUCT, isProductConfigured } from './product.js';
|
|
3
|
+
const DEFAULT_API_BASE = process.env.ANIMATION_MCP_API_URL || 'https://getrafaylabs.com/api/lottie';
|
|
4
|
+
const LS_LICENSE_API = 'https://api.lemonsqueezy.com/v1/licenses';
|
|
5
|
+
const NOT_CONFIGURED = 'Licence verification is not configured in this build yet. Please try again later or contact info@getrafaylabs.com.';
|
|
6
|
+
async function lemonSqueezy(endpoint, params) {
|
|
7
|
+
const response = await fetch(`${LS_LICENSE_API}/${endpoint}`, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
10
|
+
body: new URLSearchParams(params),
|
|
11
|
+
signal: AbortSignal.timeout(15_000),
|
|
12
|
+
});
|
|
13
|
+
return (await response.json());
|
|
14
|
+
}
|
|
15
|
+
/** A key from someone else's Lemon Squeezy store must never unlock our product. */
|
|
16
|
+
function productMismatch(meta, expected) {
|
|
17
|
+
return !(Number(meta?.store_id) === expected.storeId && expected.productIds.includes(Number(meta?.product_id)));
|
|
18
|
+
}
|
|
19
|
+
function badStatus(data) {
|
|
20
|
+
const status = data?.license_key?.status;
|
|
21
|
+
if (status === 'expired')
|
|
22
|
+
return 'This licence key has expired.';
|
|
23
|
+
if (status === 'disabled')
|
|
24
|
+
return 'This licence key has been disabled.';
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Activates a Lemon Squeezy licence key on this machine.
|
|
29
|
+
* Fails closed: any problem (network, wrong product, not configured) means "not licensed".
|
|
30
|
+
*/
|
|
31
|
+
export async function verifyLicenseKey(key, instanceName = 'Developer Machine', expected = LICENSE_PRODUCT) {
|
|
32
|
+
const cleaned = key.trim();
|
|
33
|
+
if (!cleaned)
|
|
34
|
+
return { valid: false, tier: 'free', error: 'Licence key cannot be empty.' };
|
|
35
|
+
if (!isProductConfigured(expected))
|
|
36
|
+
return { valid: false, tier: 'free', error: NOT_CONFIGURED };
|
|
37
|
+
try {
|
|
38
|
+
const data = await lemonSqueezy('activate', { license_key: cleaned, instance_name: instanceName });
|
|
39
|
+
if (!data.activated) {
|
|
40
|
+
return { valid: false, tier: 'free', error: data.error || 'Invalid or expired licence key.' };
|
|
41
|
+
}
|
|
42
|
+
if (productMismatch(data.meta, expected)) {
|
|
43
|
+
// Free the activation slot we just consumed, then refuse.
|
|
44
|
+
if (data.instance?.id) {
|
|
45
|
+
lemonSqueezy('deactivate', { license_key: cleaned, instance_id: data.instance.id }).catch(() => { });
|
|
46
|
+
}
|
|
47
|
+
return { valid: false, tier: 'free', error: 'This licence key is not for RafayLabs Animation MCP.' };
|
|
48
|
+
}
|
|
49
|
+
const statusError = badStatus(data);
|
|
50
|
+
if (statusError)
|
|
51
|
+
return { valid: false, tier: 'free', error: statusError };
|
|
52
|
+
return {
|
|
53
|
+
valid: true,
|
|
54
|
+
tier: 'lifetime',
|
|
55
|
+
email: data.meta?.customer_email,
|
|
56
|
+
instanceId: data.instance?.id,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return { valid: false, tier: 'free', error: `Network error verifying licence: ${err.message}` };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function revalidate(key, instanceId, expected) {
|
|
64
|
+
if (!instanceId)
|
|
65
|
+
return verifyLicenseKey(key, 'Developer Machine', expected);
|
|
66
|
+
if (!isProductConfigured(expected))
|
|
67
|
+
return { valid: false, tier: 'free', error: NOT_CONFIGURED };
|
|
68
|
+
try {
|
|
69
|
+
const data = await lemonSqueezy('validate', { license_key: key, instance_id: instanceId });
|
|
70
|
+
if (!data.valid || productMismatch(data.meta, expected) || badStatus(data)) {
|
|
71
|
+
return { valid: false, tier: 'free', error: data.error || 'Licence is no longer valid.' };
|
|
72
|
+
}
|
|
73
|
+
return { valid: true, tier: 'lifetime', email: data.meta?.customer_email, instanceId };
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
return { valid: false, tier: 'free', error: `Network error verifying licence: ${err.message}` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Decides whether this run is a paid one. Used by every quota-limited tool.
|
|
81
|
+
* - A verified, unexpired, untampered cached licence counts.
|
|
82
|
+
* - ANIMATION_MCP_KEY is only a way to *supply* a key: it is always checked with Lemon Squeezy.
|
|
83
|
+
* - Anything else is the free tier.
|
|
84
|
+
*/
|
|
85
|
+
export async function resolveLicense(expected = LICENSE_PRODUCT) {
|
|
86
|
+
const config = loadConfig();
|
|
87
|
+
const envKey = process.env.ANIMATION_MCP_KEY?.trim();
|
|
88
|
+
const key = envKey || config.licenseKey;
|
|
89
|
+
if (isLicenseValid(config) && (!envKey || config.licenseKey === envKey))
|
|
90
|
+
return true;
|
|
91
|
+
if (!key)
|
|
92
|
+
return false; // account-token sessions cannot be re-checked silently: sign in again
|
|
93
|
+
const result = await revalidate(key, config.licenseKey === key ? config.instanceId : undefined, expected);
|
|
94
|
+
if (!result.valid)
|
|
95
|
+
return false;
|
|
96
|
+
grantLicense(config, { email: result.email ?? config.email, licenseKey: key, instanceId: result.instanceId });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Frees this machine's activation on Lemon Squeezy so the key can be used on another device.
|
|
101
|
+
* Returns ok:true when the activation is gone (or was already gone).
|
|
102
|
+
*/
|
|
103
|
+
export async function deactivateLicense(key, instanceId) {
|
|
104
|
+
try {
|
|
105
|
+
const data = await lemonSqueezy('deactivate', { license_key: key, instance_id: instanceId });
|
|
106
|
+
if (data.deactivated)
|
|
107
|
+
return { ok: true };
|
|
108
|
+
// Already removed on the Lemon Squeezy side: nothing left to free.
|
|
109
|
+
if (/not found/i.test(String(data.error ?? '')))
|
|
110
|
+
return { ok: true };
|
|
111
|
+
return { ok: false, error: data.error || 'Lemon Squeezy did not confirm the deactivation.' };
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return { ok: false, error: `Network error: ${err.message}` };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const EMAIL_UNAVAILABLE = 'Email sign-in is not available yet. Use your Lemon Squeezy licence key, or continue on the free tier.';
|
|
118
|
+
/**
|
|
119
|
+
* Email magic-code sign-in. Requires the server endpoint; fails closed when it is missing.
|
|
120
|
+
*/
|
|
121
|
+
export async function requestEmailMagicCode(email) {
|
|
122
|
+
try {
|
|
123
|
+
const res = await fetch(`${DEFAULT_API_BASE}/auth/magic-code`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
body: JSON.stringify({ email: email.trim().toLowerCase() }),
|
|
127
|
+
signal: AbortSignal.timeout(15_000),
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
return { success: false, message: EMAIL_UNAVAILABLE };
|
|
131
|
+
const data = (await res.json());
|
|
132
|
+
return { success: true, message: data.message || `Code sent to ${email}` };
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return { success: false, message: EMAIL_UNAVAILABLE };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Verifies a magic code with the server. The paid tier is granted only from a server
|
|
140
|
+
* response that carries a token; there is no local fallback.
|
|
141
|
+
*/
|
|
142
|
+
export async function verifyEmailMagicCode(email, code) {
|
|
143
|
+
const cleanEmail = email.trim().toLowerCase();
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch(`${DEFAULT_API_BASE}/auth/verify-code`, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: { 'Content-Type': 'application/json' },
|
|
148
|
+
body: JSON.stringify({ email: cleanEmail, code: code.trim() }),
|
|
149
|
+
signal: AbortSignal.timeout(15_000),
|
|
150
|
+
});
|
|
151
|
+
if (!res.ok)
|
|
152
|
+
return { success: false, error: EMAIL_UNAVAILABLE };
|
|
153
|
+
const data = (await res.json());
|
|
154
|
+
if (!data?.token)
|
|
155
|
+
return { success: false, error: 'The server did not confirm this code.' };
|
|
156
|
+
const config = loadConfig();
|
|
157
|
+
if (data.tier === 'lifetime') {
|
|
158
|
+
grantLicense(config, { email: cleanEmail, licenseKey: data.licenseKey, token: data.token });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
config.email = cleanEmail;
|
|
162
|
+
config.tier = 'free';
|
|
163
|
+
saveConfig(config);
|
|
164
|
+
}
|
|
165
|
+
return { success: true, config };
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return { success: false, error: EMAIL_UNAVAILABLE };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface UserConfig {
|
|
2
|
+
email?: string;
|
|
3
|
+
licenseKey?: string;
|
|
4
|
+
tier: 'free' | 'lifetime';
|
|
5
|
+
token?: string;
|
|
6
|
+
instanceId?: string;
|
|
7
|
+
/** Sign-in token from getrafaylabs.com (the server decides plan and limits). */
|
|
8
|
+
serverToken?: string;
|
|
9
|
+
githubLogin?: string;
|
|
10
|
+
cachedUntil?: string;
|
|
11
|
+
/** Tamper-evidence signature over the paid-licence fields (see signLicense). */
|
|
12
|
+
sig?: string;
|
|
13
|
+
downloadsToday: number;
|
|
14
|
+
searchesToday: number;
|
|
15
|
+
lastResetDate: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Loads user config from ~/.animation-mcp/config.json with daily quota resets
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadConfig(): UserConfig;
|
|
21
|
+
/**
|
|
22
|
+
* Persists config to ~/.animation-mcp/config.json
|
|
23
|
+
*/
|
|
24
|
+
export declare function saveConfig(config: UserConfig): void;
|
|
25
|
+
/** Marks a config as a verified paid licence (call only after a successful server/LS check). */
|
|
26
|
+
export declare function grantLicense(config: UserConfig, grant: {
|
|
27
|
+
email?: string;
|
|
28
|
+
licenseKey?: string;
|
|
29
|
+
token?: string;
|
|
30
|
+
instanceId?: string;
|
|
31
|
+
}): UserConfig;
|
|
32
|
+
/**
|
|
33
|
+
* True only for a paid licence that was verified, has not expired (7-day cache) and
|
|
34
|
+
* whose fields have not been edited by hand. An environment variable alone never counts.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isLicenseValid(config: UserConfig): boolean;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
// Resolved on every call (not at import time) so the home folder can be changed in tests.
|
|
6
|
+
const configDir = () => path.join(os.homedir(), '.animation-mcp');
|
|
7
|
+
const configFile = () => path.join(configDir(), 'config.json');
|
|
8
|
+
function getTodayString() {
|
|
9
|
+
return new Date().toISOString().split('T')[0];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Loads user config from ~/.animation-mcp/config.json with daily quota resets
|
|
13
|
+
*/
|
|
14
|
+
export function loadConfig() {
|
|
15
|
+
try {
|
|
16
|
+
if (fs.existsSync(configFile())) {
|
|
17
|
+
const data = JSON.parse(fs.readFileSync(configFile(), 'utf-8'));
|
|
18
|
+
const today = getTodayString();
|
|
19
|
+
// Reset daily counts if new day
|
|
20
|
+
if (data.lastResetDate !== today) {
|
|
21
|
+
data.downloadsToday = 0;
|
|
22
|
+
data.searchesToday = 0;
|
|
23
|
+
data.lastResetDate = today;
|
|
24
|
+
saveConfig(data);
|
|
25
|
+
}
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// Fall back to default free config
|
|
31
|
+
}
|
|
32
|
+
const defaultConfig = {
|
|
33
|
+
tier: 'free',
|
|
34
|
+
downloadsToday: 0,
|
|
35
|
+
searchesToday: 0,
|
|
36
|
+
lastResetDate: getTodayString(),
|
|
37
|
+
};
|
|
38
|
+
saveConfig(defaultConfig);
|
|
39
|
+
return defaultConfig;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Persists config to ~/.animation-mcp/config.json
|
|
43
|
+
*/
|
|
44
|
+
export function saveConfig(config) {
|
|
45
|
+
try {
|
|
46
|
+
if (!fs.existsSync(configDir())) {
|
|
47
|
+
fs.mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
48
|
+
}
|
|
49
|
+
fs.writeFileSync(configFile(), JSON.stringify(config, null, 2), {
|
|
50
|
+
mode: 0o600,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error('Failed to save config:', err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const LICENSE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
58
|
+
/**
|
|
59
|
+
* HMAC over the licence fields, keyed to this machine + user.
|
|
60
|
+
*
|
|
61
|
+
* This only stops the trivial "edit config.json and write tier: lifetime" bypass.
|
|
62
|
+
* The secret lives in published client code, so it is NOT real security: real
|
|
63
|
+
* enforcement of paid features has to happen on the server.
|
|
64
|
+
*/
|
|
65
|
+
function signLicense(config) {
|
|
66
|
+
const secret = crypto
|
|
67
|
+
.createHash('sha256')
|
|
68
|
+
.update(`rafaylabs-animation-mcp:${os.hostname()}:${os.userInfo().username}`)
|
|
69
|
+
.digest();
|
|
70
|
+
const payload = [
|
|
71
|
+
config.tier,
|
|
72
|
+
config.licenseKey ?? '',
|
|
73
|
+
config.token ?? '',
|
|
74
|
+
config.email ?? '',
|
|
75
|
+
config.instanceId ?? '',
|
|
76
|
+
config.cachedUntil ?? '',
|
|
77
|
+
].join('|');
|
|
78
|
+
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
79
|
+
}
|
|
80
|
+
/** Marks a config as a verified paid licence (call only after a successful server/LS check). */
|
|
81
|
+
export function grantLicense(config, grant) {
|
|
82
|
+
config.tier = 'lifetime';
|
|
83
|
+
config.email = grant.email ?? config.email;
|
|
84
|
+
config.licenseKey = grant.licenseKey;
|
|
85
|
+
config.token = grant.token;
|
|
86
|
+
config.instanceId = grant.instanceId;
|
|
87
|
+
config.cachedUntil = new Date(Date.now() + LICENSE_TTL_MS).toISOString();
|
|
88
|
+
config.sig = signLicense(config);
|
|
89
|
+
saveConfig(config);
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* True only for a paid licence that was verified, has not expired (7-day cache) and
|
|
94
|
+
* whose fields have not been edited by hand. An environment variable alone never counts.
|
|
95
|
+
*/
|
|
96
|
+
export function isLicenseValid(config) {
|
|
97
|
+
if (config.tier !== 'lifetime' || !(config.licenseKey || config.token))
|
|
98
|
+
return false;
|
|
99
|
+
if (!config.cachedUntil || !config.sig)
|
|
100
|
+
return false;
|
|
101
|
+
if (new Date(config.cachedUntil).getTime() <= Date.now())
|
|
102
|
+
return false;
|
|
103
|
+
const expected = Buffer.from(signLicense(config));
|
|
104
|
+
const actual = Buffer.from(config.sig);
|
|
105
|
+
return expected.length === actual.length && crypto.timingSafeEqual(expected, actual);
|
|
106
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
3
|
+
import { SERVER_URL, fetchMe } from '../api/server.js';
|
|
4
|
+
function openBrowser(url) {
|
|
5
|
+
try {
|
|
6
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : ['xdg-open', [url]];
|
|
7
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
8
|
+
child.on('error', () => { });
|
|
9
|
+
child.unref();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
/* the link is also printed, so this is optional */
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
/** GitHub sign-in without any secret on this machine (same idea as `gh auth login`). Returns the GitHub username. */
|
|
17
|
+
export async function deviceLogin() {
|
|
18
|
+
let start;
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(`${SERVER_URL}/api/mcp/auth/device`, { method: 'POST', signal: AbortSignal.timeout(15_000) });
|
|
21
|
+
if (!res.ok)
|
|
22
|
+
throw new Error(`HTTP ${res.status}`);
|
|
23
|
+
start = await res.json();
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
throw new Error(`Could not start sign-in (${err.message}). Check your internet connection and try again.`);
|
|
27
|
+
}
|
|
28
|
+
console.log(`\nOpen this link and sign in with GitHub:\n\n ${start.verification_url}\n`);
|
|
29
|
+
console.log(`Your code: ${start.user_code} (check it matches on the page)\n`);
|
|
30
|
+
openBrowser(start.verification_url);
|
|
31
|
+
console.log('⏳ Waiting for you to finish in the browser...');
|
|
32
|
+
const deadline = Date.now() + Number(start.expires_in ?? 600) * 1000;
|
|
33
|
+
const interval = Math.max(2, Number(start.interval ?? 5)) * 1000;
|
|
34
|
+
while (Date.now() < deadline) {
|
|
35
|
+
await sleep(interval);
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await fetch(`${SERVER_URL}/api/mcp/auth/token`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify({ device_code: start.device_code }),
|
|
42
|
+
signal: AbortSignal.timeout(15_000),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue; // brief network blip: keep waiting
|
|
47
|
+
}
|
|
48
|
+
const json = await res.json().catch(() => ({}));
|
|
49
|
+
if (json.access_token) {
|
|
50
|
+
const config = loadConfig();
|
|
51
|
+
config.serverToken = json.access_token;
|
|
52
|
+
saveConfig(config);
|
|
53
|
+
const me = await fetchMe();
|
|
54
|
+
config.githubLogin = me.github_login;
|
|
55
|
+
saveConfig(config);
|
|
56
|
+
return me.github_login;
|
|
57
|
+
}
|
|
58
|
+
if (json.error === 'authorization_pending')
|
|
59
|
+
continue;
|
|
60
|
+
throw new Error('Sign-in expired or was cancelled. Run `animation-mcp login` again.');
|
|
61
|
+
}
|
|
62
|
+
throw new Error('Sign-in timed out. Run `animation-mcp login` again.');
|
|
63
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Lemon Squeezy product a licence key must belong to.
|
|
3
|
+
*
|
|
4
|
+
* These are hard-coded on purpose (NOT read from env vars): an env var would let
|
|
5
|
+
* anyone point the check at their own store and mint valid-looking keys.
|
|
6
|
+
*
|
|
7
|
+
* Fill these in from your Lemon Squeezy dashboard before publishing:
|
|
8
|
+
* Store ID -> Settings > Stores
|
|
9
|
+
* Product ID -> Products > (your product) > the number in the URL / API
|
|
10
|
+
* Until both are set, licence activation fails closed (nobody gets a paid tier).
|
|
11
|
+
*/
|
|
12
|
+
export interface ProductBinding {
|
|
13
|
+
storeId: number;
|
|
14
|
+
productIds: number[];
|
|
15
|
+
}
|
|
16
|
+
export declare const LICENSE_PRODUCT: ProductBinding;
|
|
17
|
+
export declare function isProductConfigured(p?: ProductBinding): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const LICENSE_PRODUCT = {
|
|
2
|
+
storeId: 520930, // RafayLabs store (confirm it matches meta.store_id on a live-mode key)
|
|
3
|
+
// "Animation MCP Lifetime". This ID is from TEST mode. Before going live, create the
|
|
4
|
+
// product in live mode and add its (different) ID here, then remove the test one.
|
|
5
|
+
productIds: [1517699],
|
|
6
|
+
};
|
|
7
|
+
export function isProductConfigured(p = LICENSE_PRODUCT) {
|
|
8
|
+
return p.storeId > 0 && p.productIds.length > 0;
|
|
9
|
+
}
|
package/dist/index.d.ts
ADDED