archicore 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 +530 -0
- package/dist/analyzers/dead-code.d.ts +95 -0
- package/dist/analyzers/dead-code.js +327 -0
- package/dist/analyzers/duplication.d.ts +90 -0
- package/dist/analyzers/duplication.js +344 -0
- package/dist/analyzers/security.d.ts +79 -0
- package/dist/analyzers/security.js +484 -0
- package/dist/architecture/index.d.ts +35 -0
- package/dist/architecture/index.js +249 -0
- package/dist/cli/commands/analyzers.d.ts +6 -0
- package/dist/cli/commands/analyzers.js +431 -0
- package/dist/cli/commands/export.d.ts +6 -0
- package/dist/cli/commands/export.js +78 -0
- package/dist/cli/commands/index.d.ts +8 -0
- package/dist/cli/commands/index.js +8 -0
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.js +140 -0
- package/dist/cli/commands/interactive.d.ts +7 -0
- package/dist/cli/commands/interactive.js +522 -0
- package/dist/cli/commands/projects.d.ts +6 -0
- package/dist/cli/commands/projects.js +249 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/ui/box.d.ts +17 -0
- package/dist/cli/ui/box.js +62 -0
- package/dist/cli/ui/colors.d.ts +49 -0
- package/dist/cli/ui/colors.js +86 -0
- package/dist/cli/ui/index.d.ts +9 -0
- package/dist/cli/ui/index.js +9 -0
- package/dist/cli/ui/prompt.d.ts +34 -0
- package/dist/cli/ui/prompt.js +122 -0
- package/dist/cli/ui/spinner.d.ts +29 -0
- package/dist/cli/ui/spinner.js +80 -0
- package/dist/cli/ui/table.d.ts +33 -0
- package/dist/cli/ui/table.js +84 -0
- package/dist/cli/utils/config.d.ts +23 -0
- package/dist/cli/utils/config.js +73 -0
- package/dist/cli/utils/index.d.ts +6 -0
- package/dist/cli/utils/index.js +6 -0
- package/dist/cli/utils/session.d.ts +27 -0
- package/dist/cli/utils/session.js +117 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +295 -0
- package/dist/code-index/ast-parser.d.ts +16 -0
- package/dist/code-index/ast-parser.js +330 -0
- package/dist/code-index/dependency-graph.d.ts +16 -0
- package/dist/code-index/dependency-graph.js +161 -0
- package/dist/code-index/index.d.ts +44 -0
- package/dist/code-index/index.js +124 -0
- package/dist/code-index/symbol-extractor.d.ts +13 -0
- package/dist/code-index/symbol-extractor.js +150 -0
- package/dist/export/index.d.ts +92 -0
- package/dist/export/index.js +676 -0
- package/dist/github/github-service.d.ts +146 -0
- package/dist/github/github-service.js +609 -0
- package/dist/impact-engine/index.d.ts +25 -0
- package/dist/impact-engine/index.js +284 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +149 -0
- package/dist/metrics/index.d.ts +136 -0
- package/dist/metrics/index.js +525 -0
- package/dist/orchestrator/deepseek-optimizer.d.ts +67 -0
- package/dist/orchestrator/deepseek-optimizer.js +320 -0
- package/dist/orchestrator/index.d.ts +34 -0
- package/dist/orchestrator/index.js +305 -0
- package/dist/pr-guardian/index.d.ts +143 -0
- package/dist/pr-guardian/index.js +553 -0
- package/dist/refactoring/index.d.ts +108 -0
- package/dist/refactoring/index.js +580 -0
- package/dist/rules-engine/index.d.ts +129 -0
- package/dist/rules-engine/index.js +482 -0
- package/dist/semantic-memory/embedding-service.d.ts +24 -0
- package/dist/semantic-memory/embedding-service.js +120 -0
- package/dist/semantic-memory/index.d.ts +45 -0
- package/dist/semantic-memory/index.js +206 -0
- package/dist/semantic-memory/vector-store.d.ts +27 -0
- package/dist/semantic-memory/vector-store.js +166 -0
- package/dist/server/index.d.ts +28 -0
- package/dist/server/index.js +141 -0
- package/dist/server/middleware/api-auth.d.ts +43 -0
- package/dist/server/middleware/api-auth.js +256 -0
- package/dist/server/routes/admin.d.ts +5 -0
- package/dist/server/routes/admin.js +123 -0
- package/dist/server/routes/api.d.ts +7 -0
- package/dist/server/routes/api.js +362 -0
- package/dist/server/routes/auth.d.ts +16 -0
- package/dist/server/routes/auth.js +191 -0
- package/dist/server/routes/developer.d.ts +8 -0
- package/dist/server/routes/developer.js +439 -0
- package/dist/server/routes/github.d.ts +7 -0
- package/dist/server/routes/github.js +495 -0
- package/dist/server/routes/upload.d.ts +7 -0
- package/dist/server/routes/upload.js +196 -0
- package/dist/server/services/api-key-service.d.ts +81 -0
- package/dist/server/services/api-key-service.js +281 -0
- package/dist/server/services/auth-service.d.ts +40 -0
- package/dist/server/services/auth-service.js +315 -0
- package/dist/server/services/project-service.d.ts +123 -0
- package/dist/server/services/project-service.js +533 -0
- package/dist/server/services/token-service.d.ts +107 -0
- package/dist/server/services/token-service.js +416 -0
- package/dist/server/services/upload-service.d.ts +93 -0
- package/dist/server/services/upload-service.js +464 -0
- package/dist/types/api.d.ts +188 -0
- package/dist/types/api.js +86 -0
- package/dist/types/github.d.ts +335 -0
- package/dist/types/github.js +5 -0
- package/dist/types/index.d.ts +265 -0
- package/dist/types/index.js +32 -0
- package/dist/types/user.d.ts +69 -0
- package/dist/types/user.js +42 -0
- package/dist/utils/file-utils.d.ts +20 -0
- package/dist/utils/file-utils.js +163 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.js +41 -0
- package/dist/watcher/index.d.ts +125 -0
- package/dist/watcher/index.js +397 -0
- package/package.json +71 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Service for ArchiCore Developer API
|
|
3
|
+
*
|
|
4
|
+
* Подсчёт токенов, биллинг и rate limiting
|
|
5
|
+
*/
|
|
6
|
+
import { randomBytes } from 'crypto';
|
|
7
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { PRICING_TIERS, OPERATION_TOKEN_COSTS } from '../../types/api.js';
|
|
10
|
+
import { Logger } from '../../utils/logger.js';
|
|
11
|
+
const DATA_DIR = '.archicore';
|
|
12
|
+
const USAGE_FILE = 'token-usage.json';
|
|
13
|
+
const BILLING_FILE = 'billing.json';
|
|
14
|
+
const RATE_LIMITS_FILE = 'rate-limits.json';
|
|
15
|
+
export class TokenService {
|
|
16
|
+
dataDir;
|
|
17
|
+
usageRecords = [];
|
|
18
|
+
billingAccounts = [];
|
|
19
|
+
rateLimitStates = [];
|
|
20
|
+
initialized = false;
|
|
21
|
+
constructor(dataDir = DATA_DIR) {
|
|
22
|
+
this.dataDir = dataDir;
|
|
23
|
+
}
|
|
24
|
+
async ensureInitialized() {
|
|
25
|
+
if (this.initialized)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
await mkdir(this.dataDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
// Load usage records
|
|
32
|
+
try {
|
|
33
|
+
const path = join(this.dataDir, USAGE_FILE);
|
|
34
|
+
const data = await readFile(path, 'utf-8');
|
|
35
|
+
const parsed = JSON.parse(data);
|
|
36
|
+
this.usageRecords = parsed.records || [];
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
this.usageRecords = [];
|
|
40
|
+
}
|
|
41
|
+
// Load billing accounts
|
|
42
|
+
try {
|
|
43
|
+
const path = join(this.dataDir, BILLING_FILE);
|
|
44
|
+
const data = await readFile(path, 'utf-8');
|
|
45
|
+
const parsed = JSON.parse(data);
|
|
46
|
+
this.billingAccounts = parsed.accounts || [];
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
this.billingAccounts = [];
|
|
50
|
+
}
|
|
51
|
+
// Load rate limit states
|
|
52
|
+
try {
|
|
53
|
+
const path = join(this.dataDir, RATE_LIMITS_FILE);
|
|
54
|
+
const data = await readFile(path, 'utf-8');
|
|
55
|
+
const parsed = JSON.parse(data);
|
|
56
|
+
this.rateLimitStates = parsed.states || [];
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
this.rateLimitStates = [];
|
|
60
|
+
}
|
|
61
|
+
this.initialized = true;
|
|
62
|
+
}
|
|
63
|
+
async saveUsage() {
|
|
64
|
+
const path = join(this.dataDir, USAGE_FILE);
|
|
65
|
+
await writeFile(path, JSON.stringify({ records: this.usageRecords }, null, 2));
|
|
66
|
+
}
|
|
67
|
+
async saveBilling() {
|
|
68
|
+
const path = join(this.dataDir, BILLING_FILE);
|
|
69
|
+
await writeFile(path, JSON.stringify({ accounts: this.billingAccounts }, null, 2));
|
|
70
|
+
}
|
|
71
|
+
async saveRateLimits() {
|
|
72
|
+
const path = join(this.dataDir, RATE_LIMITS_FILE);
|
|
73
|
+
await writeFile(path, JSON.stringify({ states: this.rateLimitStates }, null, 2));
|
|
74
|
+
}
|
|
75
|
+
// ===== TOKEN CALCULATION =====
|
|
76
|
+
/**
|
|
77
|
+
* Подсчёт токенов для операции
|
|
78
|
+
*/
|
|
79
|
+
calculateTokens(operation, params) {
|
|
80
|
+
const costs = OPERATION_TOKEN_COSTS[operation];
|
|
81
|
+
const fileCount = params.fileCount || 0;
|
|
82
|
+
const contentSizeKb = params.contentSizeKb || 0;
|
|
83
|
+
// Базовый расчёт токенов
|
|
84
|
+
let inputTokens = costs.base + (fileCount * costs.perFile) + (contentSizeKb * costs.perKb);
|
|
85
|
+
// Добавляем токены за текст (примерно 1 токен = 4 символа)
|
|
86
|
+
if (params.inputText) {
|
|
87
|
+
inputTokens += Math.ceil(params.inputText.length / 4);
|
|
88
|
+
}
|
|
89
|
+
let outputTokens = 0;
|
|
90
|
+
if (params.outputText) {
|
|
91
|
+
outputTokens = Math.ceil(params.outputText.length / 4);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
inputTokens,
|
|
95
|
+
outputTokens,
|
|
96
|
+
totalTokens: inputTokens + outputTokens
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Расчёт стоимости в центах
|
|
101
|
+
*/
|
|
102
|
+
calculateCost(totalTokens, pricingTier) {
|
|
103
|
+
const tier = PRICING_TIERS[pricingTier] || PRICING_TIERS.starter;
|
|
104
|
+
// Цена за 1M токенов → цена за токен → умножаем на количество
|
|
105
|
+
const pricePerToken = tier.pricePerMillionTokens / 1_000_000;
|
|
106
|
+
return Math.ceil(totalTokens * pricePerToken * 100); // в центах
|
|
107
|
+
}
|
|
108
|
+
// ===== USAGE TRACKING =====
|
|
109
|
+
/**
|
|
110
|
+
* Запись использования токенов
|
|
111
|
+
*/
|
|
112
|
+
async recordUsage(apiKeyId, userId, operation, tokens, projectId, metadata) {
|
|
113
|
+
await this.ensureInitialized();
|
|
114
|
+
// Получаем billing account для расчёта стоимости
|
|
115
|
+
const billing = await this.getBillingAccount(userId);
|
|
116
|
+
const cost = this.calculateCost(tokens.totalTokens, billing.pricingTier);
|
|
117
|
+
const usage = {
|
|
118
|
+
id: 'usage_' + randomBytes(12).toString('hex'),
|
|
119
|
+
apiKeyId,
|
|
120
|
+
userId,
|
|
121
|
+
operation,
|
|
122
|
+
inputTokens: tokens.inputTokens,
|
|
123
|
+
outputTokens: tokens.outputTokens,
|
|
124
|
+
totalTokens: tokens.totalTokens,
|
|
125
|
+
cost,
|
|
126
|
+
timestamp: new Date(),
|
|
127
|
+
projectId,
|
|
128
|
+
metadata
|
|
129
|
+
};
|
|
130
|
+
this.usageRecords.push(usage);
|
|
131
|
+
await this.saveUsage();
|
|
132
|
+
// Обновляем billing account
|
|
133
|
+
await this.updateBillingUsage(userId, tokens.totalTokens, cost);
|
|
134
|
+
Logger.debug(`Token usage recorded: ${tokens.totalTokens} tokens, $${(cost / 100).toFixed(4)}`);
|
|
135
|
+
return usage;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Получение истории использования
|
|
139
|
+
*/
|
|
140
|
+
async getUsageHistory(userId, options) {
|
|
141
|
+
await this.ensureInitialized();
|
|
142
|
+
let records = this.usageRecords.filter(r => r.userId === userId);
|
|
143
|
+
if (options.apiKeyId) {
|
|
144
|
+
records = records.filter(r => r.apiKeyId === options.apiKeyId);
|
|
145
|
+
}
|
|
146
|
+
if (options.operation) {
|
|
147
|
+
records = records.filter(r => r.operation === options.operation);
|
|
148
|
+
}
|
|
149
|
+
// Сортируем по дате (новые первые)
|
|
150
|
+
records.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
151
|
+
const offset = options.offset || 0;
|
|
152
|
+
const limit = options.limit || 100;
|
|
153
|
+
return records.slice(offset, offset + limit);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Получение статистики использования
|
|
157
|
+
*/
|
|
158
|
+
async getUsageStats(userId, period) {
|
|
159
|
+
await this.ensureInitialized();
|
|
160
|
+
const now = new Date();
|
|
161
|
+
let startDate;
|
|
162
|
+
switch (period) {
|
|
163
|
+
case 'hour':
|
|
164
|
+
startDate = new Date(now.getTime() - 60 * 60 * 1000);
|
|
165
|
+
break;
|
|
166
|
+
case 'day':
|
|
167
|
+
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
168
|
+
break;
|
|
169
|
+
case 'week':
|
|
170
|
+
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
171
|
+
break;
|
|
172
|
+
case 'month':
|
|
173
|
+
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
const records = this.usageRecords.filter(r => r.userId === userId && new Date(r.timestamp) >= startDate);
|
|
177
|
+
// Агрегация по операциям
|
|
178
|
+
const byOperation = {};
|
|
179
|
+
for (const op of Object.keys(OPERATION_TOKEN_COSTS)) {
|
|
180
|
+
const opRecords = records.filter(r => r.operation === op);
|
|
181
|
+
byOperation[op] = {
|
|
182
|
+
requests: opRecords.length,
|
|
183
|
+
tokens: opRecords.reduce((sum, r) => sum + r.totalTokens, 0),
|
|
184
|
+
cost: opRecords.reduce((sum, r) => sum + r.cost, 0)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Агрегация по дням (для week и month)
|
|
188
|
+
let byDay;
|
|
189
|
+
if (period === 'week' || period === 'month') {
|
|
190
|
+
const dayMap = new Map();
|
|
191
|
+
for (const r of records) {
|
|
192
|
+
const date = new Date(r.timestamp).toISOString().split('T')[0];
|
|
193
|
+
const existing = dayMap.get(date) || { requests: 0, tokens: 0, cost: 0 };
|
|
194
|
+
existing.requests++;
|
|
195
|
+
existing.tokens += r.totalTokens;
|
|
196
|
+
existing.cost += r.cost;
|
|
197
|
+
dayMap.set(date, existing);
|
|
198
|
+
}
|
|
199
|
+
byDay = Array.from(dayMap.entries())
|
|
200
|
+
.map(([date, stats]) => ({ date, ...stats }))
|
|
201
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
period,
|
|
205
|
+
startDate,
|
|
206
|
+
endDate: now,
|
|
207
|
+
totalRequests: records.length,
|
|
208
|
+
totalTokens: records.reduce((sum, r) => sum + r.totalTokens, 0),
|
|
209
|
+
totalCost: records.reduce((sum, r) => sum + r.cost, 0),
|
|
210
|
+
byOperation,
|
|
211
|
+
byDay
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// ===== BILLING =====
|
|
215
|
+
/**
|
|
216
|
+
* Получение или создание billing account
|
|
217
|
+
*/
|
|
218
|
+
async getBillingAccount(userId) {
|
|
219
|
+
await this.ensureInitialized();
|
|
220
|
+
let account = this.billingAccounts.find(a => a.userId === userId);
|
|
221
|
+
if (!account) {
|
|
222
|
+
// Создаём новый account
|
|
223
|
+
const now = new Date();
|
|
224
|
+
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
225
|
+
account = {
|
|
226
|
+
id: 'billing_' + randomBytes(12).toString('hex'),
|
|
227
|
+
userId,
|
|
228
|
+
pricingTier: 'free',
|
|
229
|
+
balance: 0,
|
|
230
|
+
tokensUsedThisMonth: 0,
|
|
231
|
+
tokensIncludedThisMonth: PRICING_TIERS.free.includedTokens,
|
|
232
|
+
billingCycleStart: now,
|
|
233
|
+
billingCycleEnd: endOfMonth,
|
|
234
|
+
autoRecharge: false,
|
|
235
|
+
autoRechargeThreshold: 500, // $5
|
|
236
|
+
autoRechargeAmount: 2000, // $20
|
|
237
|
+
paymentMethods: [],
|
|
238
|
+
invoices: []
|
|
239
|
+
};
|
|
240
|
+
this.billingAccounts.push(account);
|
|
241
|
+
await this.saveBilling();
|
|
242
|
+
}
|
|
243
|
+
return account;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Обновление использования в billing account
|
|
247
|
+
*/
|
|
248
|
+
async updateBillingUsage(userId, tokens, cost) {
|
|
249
|
+
const account = await this.getBillingAccount(userId);
|
|
250
|
+
account.tokensUsedThisMonth += tokens;
|
|
251
|
+
// Списываем с баланса если превышены included tokens
|
|
252
|
+
const tier = PRICING_TIERS[account.pricingTier] || PRICING_TIERS.free;
|
|
253
|
+
if (account.tokensUsedThisMonth > tier.includedTokens) {
|
|
254
|
+
account.balance -= cost;
|
|
255
|
+
}
|
|
256
|
+
await this.saveBilling();
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Пополнение баланса
|
|
260
|
+
*/
|
|
261
|
+
async addCredits(userId, amount) {
|
|
262
|
+
await this.ensureInitialized();
|
|
263
|
+
const account = await this.getBillingAccount(userId);
|
|
264
|
+
account.balance += amount;
|
|
265
|
+
await this.saveBilling();
|
|
266
|
+
Logger.info(`Added ${amount} cents to user ${userId} balance`);
|
|
267
|
+
return account;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Изменение pricing tier
|
|
271
|
+
*/
|
|
272
|
+
async updatePricingTier(userId, tier) {
|
|
273
|
+
await this.ensureInitialized();
|
|
274
|
+
const account = await this.getBillingAccount(userId);
|
|
275
|
+
const newTier = PRICING_TIERS[tier];
|
|
276
|
+
if (!newTier) {
|
|
277
|
+
throw new Error(`Invalid pricing tier: ${tier}`);
|
|
278
|
+
}
|
|
279
|
+
account.pricingTier = tier;
|
|
280
|
+
account.tokensIncludedThisMonth = newTier.includedTokens;
|
|
281
|
+
await this.saveBilling();
|
|
282
|
+
Logger.info(`Updated user ${userId} to tier ${tier}`);
|
|
283
|
+
return account;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Проверка достаточности баланса
|
|
287
|
+
*/
|
|
288
|
+
async checkBalance(userId, estimatedTokens) {
|
|
289
|
+
const account = await this.getBillingAccount(userId);
|
|
290
|
+
const tier = PRICING_TIERS[account.pricingTier] || PRICING_TIERS.free;
|
|
291
|
+
const tokensRemaining = Math.max(0, tier.includedTokens - account.tokensUsedThisMonth);
|
|
292
|
+
const tokensToCharge = Math.max(0, estimatedTokens - tokensRemaining);
|
|
293
|
+
const estimatedCost = this.calculateCost(tokensToCharge, account.pricingTier);
|
|
294
|
+
const allowed = account.balance >= estimatedCost || tokensRemaining >= estimatedTokens;
|
|
295
|
+
return {
|
|
296
|
+
allowed,
|
|
297
|
+
balance: account.balance,
|
|
298
|
+
estimatedCost,
|
|
299
|
+
tokensRemaining
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
// ===== RATE LIMITING =====
|
|
303
|
+
/**
|
|
304
|
+
* Проверка rate limit
|
|
305
|
+
*/
|
|
306
|
+
async checkRateLimit(apiKeyId, limits) {
|
|
307
|
+
await this.ensureInitialized();
|
|
308
|
+
const now = new Date();
|
|
309
|
+
let state = this.rateLimitStates.find(s => s.apiKeyId === apiKeyId);
|
|
310
|
+
if (!state) {
|
|
311
|
+
state = {
|
|
312
|
+
apiKeyId,
|
|
313
|
+
requestsThisMinute: 0,
|
|
314
|
+
requestsToday: 0,
|
|
315
|
+
tokensThisMinute: 0,
|
|
316
|
+
tokensToday: 0,
|
|
317
|
+
minuteResetAt: new Date(now.getTime() + 60 * 1000),
|
|
318
|
+
dayResetAt: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)
|
|
319
|
+
};
|
|
320
|
+
this.rateLimitStates.push(state);
|
|
321
|
+
}
|
|
322
|
+
// Reset counters if needed
|
|
323
|
+
if (new Date(state.minuteResetAt) <= now) {
|
|
324
|
+
state.requestsThisMinute = 0;
|
|
325
|
+
state.tokensThisMinute = 0;
|
|
326
|
+
state.minuteResetAt = new Date(now.getTime() + 60 * 1000);
|
|
327
|
+
}
|
|
328
|
+
if (new Date(state.dayResetAt) <= now) {
|
|
329
|
+
state.requestsToday = 0;
|
|
330
|
+
state.tokensToday = 0;
|
|
331
|
+
state.dayResetAt = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
332
|
+
}
|
|
333
|
+
// Check limits
|
|
334
|
+
const allowed = state.requestsThisMinute < limits.requestsPerMinute &&
|
|
335
|
+
state.requestsToday < limits.requestsPerDay;
|
|
336
|
+
let retryAfter;
|
|
337
|
+
if (!allowed) {
|
|
338
|
+
if (state.requestsThisMinute >= limits.requestsPerMinute) {
|
|
339
|
+
retryAfter = Math.ceil((new Date(state.minuteResetAt).getTime() - now.getTime()) / 1000);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
retryAfter = Math.ceil((new Date(state.dayResetAt).getTime() - now.getTime()) / 1000);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
allowed,
|
|
347
|
+
remaining: {
|
|
348
|
+
requestsPerMinute: Math.max(0, limits.requestsPerMinute - state.requestsThisMinute),
|
|
349
|
+
requestsPerDay: Math.max(0, limits.requestsPerDay - state.requestsToday),
|
|
350
|
+
tokensPerMinute: Math.max(0, limits.tokensPerMinute - state.tokensThisMinute),
|
|
351
|
+
tokensPerDay: Math.max(0, limits.tokensPerDay - state.tokensToday)
|
|
352
|
+
},
|
|
353
|
+
resetAt: {
|
|
354
|
+
minute: new Date(state.minuteResetAt),
|
|
355
|
+
day: new Date(state.dayResetAt)
|
|
356
|
+
},
|
|
357
|
+
retryAfter
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Инкремент rate limit счётчиков
|
|
362
|
+
*/
|
|
363
|
+
async incrementRateLimit(apiKeyId, tokens) {
|
|
364
|
+
await this.ensureInitialized();
|
|
365
|
+
const state = this.rateLimitStates.find(s => s.apiKeyId === apiKeyId);
|
|
366
|
+
if (state) {
|
|
367
|
+
state.requestsThisMinute++;
|
|
368
|
+
state.requestsToday++;
|
|
369
|
+
state.tokensThisMinute += tokens;
|
|
370
|
+
state.tokensToday += tokens;
|
|
371
|
+
await this.saveRateLimits();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Получение текущего состояния rate limit
|
|
376
|
+
*/
|
|
377
|
+
async getRateLimitState(apiKeyId) {
|
|
378
|
+
await this.ensureInitialized();
|
|
379
|
+
return this.rateLimitStates.find(s => s.apiKeyId === apiKeyId) || null;
|
|
380
|
+
}
|
|
381
|
+
// ===== CLEANUP =====
|
|
382
|
+
/**
|
|
383
|
+
* Очистка старых записей usage (старше 90 дней)
|
|
384
|
+
*/
|
|
385
|
+
async cleanupOldUsageRecords() {
|
|
386
|
+
await this.ensureInitialized();
|
|
387
|
+
const cutoffDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
388
|
+
const initialLength = this.usageRecords.length;
|
|
389
|
+
this.usageRecords = this.usageRecords.filter(r => new Date(r.timestamp) > cutoffDate);
|
|
390
|
+
const removedCount = initialLength - this.usageRecords.length;
|
|
391
|
+
if (removedCount > 0) {
|
|
392
|
+
await this.saveUsage();
|
|
393
|
+
Logger.info(`Cleaned up ${removedCount} old usage records`);
|
|
394
|
+
}
|
|
395
|
+
return removedCount;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Сброс месячных лимитов (вызывается в начале месяца)
|
|
399
|
+
*/
|
|
400
|
+
async resetMonthlyLimits() {
|
|
401
|
+
await this.ensureInitialized();
|
|
402
|
+
const now = new Date();
|
|
403
|
+
for (const account of this.billingAccounts) {
|
|
404
|
+
if (new Date(account.billingCycleEnd) <= now) {
|
|
405
|
+
account.tokensUsedThisMonth = 0;
|
|
406
|
+
account.billingCycleStart = now;
|
|
407
|
+
account.billingCycleEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
408
|
+
const tier = PRICING_TIERS[account.pricingTier] || PRICING_TIERS.free;
|
|
409
|
+
account.tokensIncludedThisMonth = tier.includedTokens;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
await this.saveBilling();
|
|
413
|
+
Logger.info('Monthly limits reset completed');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
//# sourceMappingURL=token-service.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload Service
|
|
3
|
+
*
|
|
4
|
+
* Безопасная загрузка и распаковка ZIP/RAR архивов.
|
|
5
|
+
*
|
|
6
|
+
* Защита от:
|
|
7
|
+
* - ZIP-бомб (проверка соотношения сжатия)
|
|
8
|
+
* - Path traversal атак (../../../etc/passwd)
|
|
9
|
+
* - Опасных файлов (исполняемые, скрипты и т.д.)
|
|
10
|
+
* - Слишком больших файлов
|
|
11
|
+
*/
|
|
12
|
+
export interface UploadConfig {
|
|
13
|
+
maxFileSize: number;
|
|
14
|
+
maxExtractedSize: number;
|
|
15
|
+
maxCompressionRatio: number;
|
|
16
|
+
uploadsDir: string;
|
|
17
|
+
extractDir: string;
|
|
18
|
+
}
|
|
19
|
+
export interface UploadResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
uploadId: string;
|
|
22
|
+
projectPath: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
warnings: string[];
|
|
25
|
+
stats: {
|
|
26
|
+
originalSize: number;
|
|
27
|
+
extractedSize: number;
|
|
28
|
+
fileCount: number;
|
|
29
|
+
skippedFiles: string[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface SecurityScanResult {
|
|
33
|
+
safe: boolean;
|
|
34
|
+
threats: SecurityThreat[];
|
|
35
|
+
warnings: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface SecurityThreat {
|
|
38
|
+
type: 'zip_bomb' | 'path_traversal' | 'dangerous_file' | 'symlink' | 'oversized';
|
|
39
|
+
description: string;
|
|
40
|
+
file?: string;
|
|
41
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
42
|
+
}
|
|
43
|
+
export declare class UploadService {
|
|
44
|
+
private config;
|
|
45
|
+
constructor(config?: Partial<UploadConfig>);
|
|
46
|
+
/**
|
|
47
|
+
* Проверка безопасности архива БЕЗ распаковки
|
|
48
|
+
*/
|
|
49
|
+
scanArchive(filePath: string): Promise<SecurityScanResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Безопасная распаковка архива
|
|
52
|
+
*/
|
|
53
|
+
extractArchive(filePath: string, uploadId: string): Promise<UploadResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Полный процесс загрузки: сохранение + сканирование + распаковка
|
|
56
|
+
*/
|
|
57
|
+
processUpload(buffer: Buffer, originalName: string): Promise<UploadResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Удаление проекта
|
|
60
|
+
*/
|
|
61
|
+
deleteProject(uploadId: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Проверка на path traversal
|
|
64
|
+
*/
|
|
65
|
+
private isPathTraversal;
|
|
66
|
+
/**
|
|
67
|
+
* Очистка пути от опасных элементов
|
|
68
|
+
*/
|
|
69
|
+
private sanitizePath;
|
|
70
|
+
/**
|
|
71
|
+
* Форматирование размера в читаемый вид
|
|
72
|
+
*/
|
|
73
|
+
private formatBytes;
|
|
74
|
+
/**
|
|
75
|
+
* Получить информацию о загруженных проектах
|
|
76
|
+
*/
|
|
77
|
+
listUploadedProjects(): Promise<Array<{
|
|
78
|
+
id: string;
|
|
79
|
+
path: string;
|
|
80
|
+
size: number;
|
|
81
|
+
files: number;
|
|
82
|
+
createdAt: Date;
|
|
83
|
+
}>>;
|
|
84
|
+
/**
|
|
85
|
+
* Подсчёт файлов в директории рекурсивно
|
|
86
|
+
*/
|
|
87
|
+
private countFiles;
|
|
88
|
+
/**
|
|
89
|
+
* Размер директории рекурсивно
|
|
90
|
+
*/
|
|
91
|
+
private getDirSize;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=upload-service.d.ts.map
|