halt-rate 0.2.0 → 0.3.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 +28 -923
- package/dist/adapters/express.d.mts +1 -1
- package/dist/adapters/express.d.ts +1 -1
- package/dist/adapters/next.d.mts +1 -1
- package/dist/adapters/next.d.ts +1 -1
- package/dist/adapters/next.js +23 -1
- package/dist/adapters/next.js.map +1 -1
- package/dist/adapters/next.mjs +23 -1
- package/dist/adapters/next.mjs.map +1 -1
- package/dist/index.d.mts +126 -3
- package/dist/index.d.ts +126 -3
- package/dist/index.js +562 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +547 -2
- package/dist/index.mjs.map +1 -1
- package/dist/limiter-DqVVE0Kl.d.mts +310 -0
- package/dist/limiter-DqVVE0Kl.d.ts +310 -0
- package/package.json +16 -4
- package/dist/limiter-HCt8ADZ2.d.mts +0 -177
- package/dist/limiter-HCt8ADZ2.d.ts +0 -177
package/dist/index.mjs
CHANGED
|
@@ -31,7 +31,8 @@ function normalizePolicy(policy) {
|
|
|
31
31
|
cost: policy.cost ?? 1,
|
|
32
32
|
blockDuration: policy.blockDuration ?? void 0,
|
|
33
33
|
keyExtractor: policy.keyExtractor ?? void 0,
|
|
34
|
-
exemptions: policy.exemptions ?? []
|
|
34
|
+
exemptions: policy.exemptions ?? [],
|
|
35
|
+
plan: policy.plan ?? void 0
|
|
35
36
|
};
|
|
36
37
|
if (normalized.limit <= 0) {
|
|
37
38
|
throw new Error("limit must be positive");
|
|
@@ -402,6 +403,25 @@ var RateLimiter = class {
|
|
|
402
403
|
this.algorithmCache = /* @__PURE__ */ new Map();
|
|
403
404
|
this.otelTracer = options.otelTracer;
|
|
404
405
|
this.metricsRecorder = options.metricsRecorder;
|
|
406
|
+
this.telemetry = options.telemetry;
|
|
407
|
+
}
|
|
408
|
+
/** Fan a finished decision out to the telemetry hooks with rich metadata. */
|
|
409
|
+
emitTelemetry(request, key, decision, policy, cost) {
|
|
410
|
+
if (!this.telemetry) return;
|
|
411
|
+
const metadata = {
|
|
412
|
+
policy: policy.name,
|
|
413
|
+
algorithm: policy.algorithm,
|
|
414
|
+
keyStrategy: policy.keyStrategy,
|
|
415
|
+
endpoint: extractPath(request) ?? void 0,
|
|
416
|
+
cost,
|
|
417
|
+
plan: policy.plan
|
|
418
|
+
};
|
|
419
|
+
this.telemetry.onCheck?.(key, decision, metadata);
|
|
420
|
+
if (decision.allowed) {
|
|
421
|
+
this.telemetry.onAllowed?.(key, decision, metadata);
|
|
422
|
+
} else {
|
|
423
|
+
this.telemetry.onBlocked?.(key, decision, metadata);
|
|
424
|
+
}
|
|
405
425
|
}
|
|
406
426
|
/**
|
|
407
427
|
* Check if request is allowed under rate limit.
|
|
@@ -460,6 +480,7 @@ var RateLimiter = class {
|
|
|
460
480
|
1
|
|
461
481
|
);
|
|
462
482
|
span2?.end?.();
|
|
483
|
+
this.emitTelemetry(request, key, decision2, policy, requestCost);
|
|
463
484
|
return decision2;
|
|
464
485
|
}
|
|
465
486
|
const store = this.store;
|
|
@@ -543,6 +564,7 @@ var RateLimiter = class {
|
|
|
543
564
|
this.metricsRecorder?.("halt.request.blocked", { policy: policy.name }, 1);
|
|
544
565
|
}
|
|
545
566
|
span?.end?.();
|
|
567
|
+
this.emitTelemetry(request, key, decision, policy, requestCost);
|
|
546
568
|
return decision;
|
|
547
569
|
}
|
|
548
570
|
/**
|
|
@@ -1006,6 +1028,7 @@ var GENEROUS_API = {
|
|
|
1006
1028
|
};
|
|
1007
1029
|
var PLAN_FREE = {
|
|
1008
1030
|
name: "free_plan",
|
|
1031
|
+
plan: "free",
|
|
1009
1032
|
limit: 100,
|
|
1010
1033
|
window: 3600,
|
|
1011
1034
|
// 100 requests per hour
|
|
@@ -1015,6 +1038,7 @@ var PLAN_FREE = {
|
|
|
1015
1038
|
};
|
|
1016
1039
|
var PLAN_STARTER = {
|
|
1017
1040
|
name: "starter_plan",
|
|
1041
|
+
plan: "starter",
|
|
1018
1042
|
limit: 500,
|
|
1019
1043
|
window: 3600,
|
|
1020
1044
|
// 500 requests per hour
|
|
@@ -1024,6 +1048,7 @@ var PLAN_STARTER = {
|
|
|
1024
1048
|
};
|
|
1025
1049
|
var PLAN_PRO = {
|
|
1026
1050
|
name: "pro_plan",
|
|
1051
|
+
plan: "pro",
|
|
1027
1052
|
limit: 2e3,
|
|
1028
1053
|
window: 3600,
|
|
1029
1054
|
// 2000 requests per hour
|
|
@@ -1033,6 +1058,7 @@ var PLAN_PRO = {
|
|
|
1033
1058
|
};
|
|
1034
1059
|
var PLAN_BUSINESS = {
|
|
1035
1060
|
name: "business_plan",
|
|
1061
|
+
plan: "business",
|
|
1036
1062
|
limit: 5e3,
|
|
1037
1063
|
window: 3600,
|
|
1038
1064
|
// 5000 requests per hour
|
|
@@ -1042,6 +1068,7 @@ var PLAN_BUSINESS = {
|
|
|
1042
1068
|
};
|
|
1043
1069
|
var PLAN_ENTERPRISE = {
|
|
1044
1070
|
name: "enterprise_plan",
|
|
1071
|
+
plan: "enterprise",
|
|
1045
1072
|
limit: 2e4,
|
|
1046
1073
|
window: 3600,
|
|
1047
1074
|
// 20000 requests per hour
|
|
@@ -1066,6 +1093,524 @@ function getPlanPolicy(planName) {
|
|
|
1066
1093
|
return PLAN_TIERS[normalized];
|
|
1067
1094
|
}
|
|
1068
1095
|
|
|
1069
|
-
|
|
1096
|
+
// src/core/quota.ts
|
|
1097
|
+
var QuotaPeriod = /* @__PURE__ */ ((QuotaPeriod2) => {
|
|
1098
|
+
QuotaPeriod2["HOURLY"] = "hourly";
|
|
1099
|
+
QuotaPeriod2["DAILY"] = "daily";
|
|
1100
|
+
QuotaPeriod2["MONTHLY"] = "monthly";
|
|
1101
|
+
QuotaPeriod2["YEARLY"] = "yearly";
|
|
1102
|
+
return QuotaPeriod2;
|
|
1103
|
+
})(QuotaPeriod || {});
|
|
1104
|
+
var QuotaManager = class {
|
|
1105
|
+
constructor(store, telemetry) {
|
|
1106
|
+
this.store = store;
|
|
1107
|
+
this.telemetry = telemetry;
|
|
1108
|
+
}
|
|
1109
|
+
getQuotaKey(identifier, quotaName) {
|
|
1110
|
+
return `halt:quota:${quotaName}:${identifier}`;
|
|
1111
|
+
}
|
|
1112
|
+
calculateResetTime(period) {
|
|
1113
|
+
const now = /* @__PURE__ */ new Date();
|
|
1114
|
+
switch (period) {
|
|
1115
|
+
case "hourly" /* HOURLY */:
|
|
1116
|
+
const nextHour = new Date(now);
|
|
1117
|
+
nextHour.setHours(now.getHours() + 1, 0, 0, 0);
|
|
1118
|
+
return Math.floor(nextHour.getTime() / 1e3);
|
|
1119
|
+
case "daily" /* DAILY */:
|
|
1120
|
+
const nextDay = new Date(now);
|
|
1121
|
+
nextDay.setDate(now.getDate() + 1);
|
|
1122
|
+
nextDay.setHours(0, 0, 0, 0);
|
|
1123
|
+
return Math.floor(nextDay.getTime() / 1e3);
|
|
1124
|
+
case "monthly" /* MONTHLY */:
|
|
1125
|
+
const nextMonth = new Date(now);
|
|
1126
|
+
if (now.getMonth() === 11) {
|
|
1127
|
+
nextMonth.setFullYear(now.getFullYear() + 1, 0, 1);
|
|
1128
|
+
} else {
|
|
1129
|
+
nextMonth.setMonth(now.getMonth() + 1, 1);
|
|
1130
|
+
}
|
|
1131
|
+
nextMonth.setHours(0, 0, 0, 0);
|
|
1132
|
+
return Math.floor(nextMonth.getTime() / 1e3);
|
|
1133
|
+
case "yearly" /* YEARLY */:
|
|
1134
|
+
const nextYear = new Date(now);
|
|
1135
|
+
nextYear.setFullYear(now.getFullYear() + 1, 0, 1);
|
|
1136
|
+
nextYear.setHours(0, 0, 0, 0);
|
|
1137
|
+
return Math.floor(nextYear.getTime() / 1e3);
|
|
1138
|
+
default:
|
|
1139
|
+
throw new Error(`Invalid quota period: ${period}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
async getQuota(identifier, quota) {
|
|
1143
|
+
const key = this.getQuotaKey(identifier, quota.name);
|
|
1144
|
+
const stored = await this.store.get(key);
|
|
1145
|
+
if (!stored) {
|
|
1146
|
+
return {
|
|
1147
|
+
name: quota.name,
|
|
1148
|
+
limit: quota.limit,
|
|
1149
|
+
period: quota.period,
|
|
1150
|
+
currentUsage: 0,
|
|
1151
|
+
resetAt: this.calculateResetTime(quota.period)
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
const currentQuota = {
|
|
1155
|
+
name: stored.name || quota.name,
|
|
1156
|
+
limit: stored.limit || quota.limit,
|
|
1157
|
+
period: stored.period || quota.period,
|
|
1158
|
+
currentUsage: stored.currentUsage || 0,
|
|
1159
|
+
resetAt: stored.resetAt
|
|
1160
|
+
};
|
|
1161
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1162
|
+
if (currentQuota.resetAt && now >= currentQuota.resetAt) {
|
|
1163
|
+
currentQuota.currentUsage = 0;
|
|
1164
|
+
currentQuota.resetAt = this.calculateResetTime(currentQuota.period);
|
|
1165
|
+
}
|
|
1166
|
+
return currentQuota;
|
|
1167
|
+
}
|
|
1168
|
+
async checkQuota(identifier, quota, cost = 1) {
|
|
1169
|
+
const currentQuota = await this.getQuota(identifier, quota);
|
|
1170
|
+
const allowed = (currentQuota.currentUsage || 0) + cost <= currentQuota.limit;
|
|
1171
|
+
this.telemetry?.onQuotaCheck?.(identifier, currentQuota, allowed);
|
|
1172
|
+
if (!allowed) {
|
|
1173
|
+
this.telemetry?.onQuotaExceeded?.(identifier, currentQuota);
|
|
1174
|
+
}
|
|
1175
|
+
return { allowed, quota: currentQuota };
|
|
1176
|
+
}
|
|
1177
|
+
async consumeQuota(identifier, quota, cost = 1) {
|
|
1178
|
+
const currentQuota = await this.getQuota(identifier, quota);
|
|
1179
|
+
currentQuota.currentUsage = (currentQuota.currentUsage || 0) + cost;
|
|
1180
|
+
const key = this.getQuotaKey(identifier, quota.name);
|
|
1181
|
+
const ttl = (currentQuota.resetAt || 0) - Math.floor(Date.now() / 1e3) + 3600;
|
|
1182
|
+
await this.store.set(key, currentQuota, ttl);
|
|
1183
|
+
return currentQuota;
|
|
1184
|
+
}
|
|
1185
|
+
async resetQuota(identifier, quota) {
|
|
1186
|
+
const key = this.getQuotaKey(identifier, quota.name);
|
|
1187
|
+
await this.store.delete(key);
|
|
1188
|
+
}
|
|
1189
|
+
remaining(quota) {
|
|
1190
|
+
return Math.max(0, quota.limit - (quota.currentUsage || 0));
|
|
1191
|
+
}
|
|
1192
|
+
isExceeded(quota) {
|
|
1193
|
+
return (quota.currentUsage || 0) >= quota.limit;
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
var QUOTA_FREE_MONTHLY = {
|
|
1197
|
+
name: "free_monthly_requests",
|
|
1198
|
+
limit: 1e4,
|
|
1199
|
+
period: "monthly" /* MONTHLY */
|
|
1200
|
+
};
|
|
1201
|
+
var QUOTA_PRO_MONTHLY = {
|
|
1202
|
+
name: "pro_monthly_requests",
|
|
1203
|
+
limit: 1e5,
|
|
1204
|
+
period: "monthly" /* MONTHLY */
|
|
1205
|
+
};
|
|
1206
|
+
var QUOTA_ENTERPRISE_MONTHLY = {
|
|
1207
|
+
name: "enterprise_monthly_requests",
|
|
1208
|
+
limit: 1e6,
|
|
1209
|
+
period: "monthly" /* MONTHLY */
|
|
1210
|
+
};
|
|
1211
|
+
var QUOTA_FREE_DAILY = {
|
|
1212
|
+
name: "free_daily_requests",
|
|
1213
|
+
limit: 500,
|
|
1214
|
+
period: "daily" /* DAILY */
|
|
1215
|
+
};
|
|
1216
|
+
var QUOTA_PRO_DAILY = {
|
|
1217
|
+
name: "pro_daily_requests",
|
|
1218
|
+
limit: 5e3,
|
|
1219
|
+
period: "daily" /* DAILY */
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
// src/core/penalty.ts
|
|
1223
|
+
var PenaltyManager = class {
|
|
1224
|
+
constructor(store, config, telemetry) {
|
|
1225
|
+
this.store = store;
|
|
1226
|
+
this.telemetry = telemetry;
|
|
1227
|
+
this.config = {
|
|
1228
|
+
threshold: config?.threshold ?? 10,
|
|
1229
|
+
duration: config?.duration ?? 3600,
|
|
1230
|
+
multiplier: config?.multiplier ?? 0.5,
|
|
1231
|
+
decayRate: config?.decayRate ?? 1
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
getPenaltyKey(identifier) {
|
|
1235
|
+
return `halt:penalty:${identifier}`;
|
|
1236
|
+
}
|
|
1237
|
+
async getPenalty(identifier) {
|
|
1238
|
+
const key = this.getPenaltyKey(identifier);
|
|
1239
|
+
const stored = await this.store.get(key);
|
|
1240
|
+
if (!stored) {
|
|
1241
|
+
return {
|
|
1242
|
+
abuseScore: 0,
|
|
1243
|
+
penaltyUntil: null,
|
|
1244
|
+
violations: 0,
|
|
1245
|
+
lastViolation: null
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
const penalty = {
|
|
1249
|
+
abuseScore: stored.abuseScore || 0,
|
|
1250
|
+
penaltyUntil: stored.penaltyUntil || null,
|
|
1251
|
+
violations: stored.violations || 0,
|
|
1252
|
+
lastViolation: stored.lastViolation || null
|
|
1253
|
+
};
|
|
1254
|
+
if (penalty.lastViolation) {
|
|
1255
|
+
const hoursElapsed = (Date.now() / 1e3 - penalty.lastViolation) / 3600;
|
|
1256
|
+
const decay = hoursElapsed * this.config.decayRate;
|
|
1257
|
+
penalty.abuseScore = Math.max(0, penalty.abuseScore - decay);
|
|
1258
|
+
}
|
|
1259
|
+
return penalty;
|
|
1260
|
+
}
|
|
1261
|
+
async recordViolation(identifier, severity = 1) {
|
|
1262
|
+
const penalty = await this.getPenalty(identifier);
|
|
1263
|
+
penalty.abuseScore += severity;
|
|
1264
|
+
penalty.violations += 1;
|
|
1265
|
+
penalty.lastViolation = Math.floor(Date.now() / 1e3);
|
|
1266
|
+
let penaltyTriggered = false;
|
|
1267
|
+
if (penalty.abuseScore >= this.config.threshold && !this.isActive(penalty)) {
|
|
1268
|
+
penalty.penaltyUntil = Math.floor(Date.now() / 1e3) + this.config.duration;
|
|
1269
|
+
penaltyTriggered = true;
|
|
1270
|
+
}
|
|
1271
|
+
await this.savePenalty(identifier, penalty);
|
|
1272
|
+
this.telemetry?.onViolation?.(identifier, penalty, severity);
|
|
1273
|
+
if (penaltyTriggered) {
|
|
1274
|
+
this.telemetry?.onPenaltyApplied?.(identifier, penalty);
|
|
1275
|
+
}
|
|
1276
|
+
return penalty;
|
|
1277
|
+
}
|
|
1278
|
+
async applyPenalty(identifier, duration) {
|
|
1279
|
+
const penalty = await this.getPenalty(identifier);
|
|
1280
|
+
penalty.penaltyUntil = Math.floor(Date.now() / 1e3) + (duration ?? this.config.duration);
|
|
1281
|
+
await this.savePenalty(identifier, penalty);
|
|
1282
|
+
this.telemetry?.onPenaltyApplied?.(identifier, penalty);
|
|
1283
|
+
return penalty;
|
|
1284
|
+
}
|
|
1285
|
+
async clearPenalty(identifier) {
|
|
1286
|
+
const key = this.getPenaltyKey(identifier);
|
|
1287
|
+
await this.store.delete(key);
|
|
1288
|
+
}
|
|
1289
|
+
getRateLimitMultiplier(penalty) {
|
|
1290
|
+
if (this.isActive(penalty)) {
|
|
1291
|
+
return this.config.multiplier;
|
|
1292
|
+
}
|
|
1293
|
+
return 1;
|
|
1294
|
+
}
|
|
1295
|
+
isActive(penalty) {
|
|
1296
|
+
if (!penalty.penaltyUntil) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
return Math.floor(Date.now() / 1e3) < penalty.penaltyUntil;
|
|
1300
|
+
}
|
|
1301
|
+
timeRemaining(penalty) {
|
|
1302
|
+
if (!this.isActive(penalty)) {
|
|
1303
|
+
return 0;
|
|
1304
|
+
}
|
|
1305
|
+
return (penalty.penaltyUntil || 0) - Math.floor(Date.now() / 1e3);
|
|
1306
|
+
}
|
|
1307
|
+
async savePenalty(identifier, penalty) {
|
|
1308
|
+
const key = this.getPenaltyKey(identifier);
|
|
1309
|
+
const ttl = 7 * 24 * 3600;
|
|
1310
|
+
await this.store.set(key, penalty, ttl);
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
var PENALTY_LENIENT = {
|
|
1314
|
+
threshold: 20,
|
|
1315
|
+
duration: 1800,
|
|
1316
|
+
// 30 minutes
|
|
1317
|
+
multiplier: 0.75,
|
|
1318
|
+
decayRate: 2
|
|
1319
|
+
};
|
|
1320
|
+
var PENALTY_MODERATE = {
|
|
1321
|
+
threshold: 10,
|
|
1322
|
+
duration: 3600,
|
|
1323
|
+
// 1 hour
|
|
1324
|
+
multiplier: 0.5,
|
|
1325
|
+
decayRate: 1
|
|
1326
|
+
};
|
|
1327
|
+
var PENALTY_STRICT = {
|
|
1328
|
+
threshold: 5,
|
|
1329
|
+
duration: 7200,
|
|
1330
|
+
// 2 hours
|
|
1331
|
+
multiplier: 0.25,
|
|
1332
|
+
decayRate: 0.5
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
// src/core/telemetry.ts
|
|
1336
|
+
var LoggingTelemetry = class {
|
|
1337
|
+
constructor(logger = console) {
|
|
1338
|
+
this.logger = logger;
|
|
1339
|
+
}
|
|
1340
|
+
onCheck(key, decision, metadata) {
|
|
1341
|
+
this.logger.debug(
|
|
1342
|
+
`Rate limit check: key=${key}, allowed=${decision.allowed}, remaining=${decision.remaining}, metadata=${JSON.stringify(metadata)}`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
onAllowed(key, decision, _metadata) {
|
|
1346
|
+
this.logger.info(`Request allowed: key=${key}, remaining=${decision.remaining}`);
|
|
1347
|
+
}
|
|
1348
|
+
onBlocked(key, decision, metadata) {
|
|
1349
|
+
this.logger.warn(
|
|
1350
|
+
`Request blocked: key=${key}, retry_after=${decision.retryAfter}s, metadata=${JSON.stringify(metadata)}`
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
onQuotaCheck(identifier, quota, allowed) {
|
|
1354
|
+
this.logger.debug(
|
|
1355
|
+
`Quota check: identifier=${identifier}, quota=${quota.name}, allowed=${allowed}, remaining=${quota.limit - (quota.currentUsage || 0)}`
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
onQuotaExceeded(identifier, quota) {
|
|
1359
|
+
this.logger.warn(
|
|
1360
|
+
`Quota exceeded: identifier=${identifier}, quota=${quota.name}, limit=${quota.limit}, reset_at=${quota.resetAt}`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
onPenaltyApplied(identifier, penalty) {
|
|
1364
|
+
this.logger.warn(
|
|
1365
|
+
`Penalty applied: identifier=${identifier}, abuse_score=${penalty.abuseScore}, penalty_until=${penalty.penaltyUntil}`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
onViolation(identifier, penalty, severity) {
|
|
1369
|
+
this.logger.info(
|
|
1370
|
+
`Violation recorded: identifier=${identifier}, severity=${severity}, abuse_score=${penalty.abuseScore}, violations=${penalty.violations}`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
var MetricsTelemetry = class {
|
|
1375
|
+
constructor(metricsClient) {
|
|
1376
|
+
this.metricsClient = metricsClient;
|
|
1377
|
+
}
|
|
1378
|
+
onCheck(_key, _decision, metadata) {
|
|
1379
|
+
this.metricsClient.increment("halt.checks.total", this.getTags(metadata));
|
|
1380
|
+
}
|
|
1381
|
+
onAllowed(_key, decision, metadata) {
|
|
1382
|
+
this.metricsClient.increment("halt.requests.allowed", this.getTags(metadata));
|
|
1383
|
+
this.metricsClient.gauge("halt.remaining", decision.remaining, this.getTags(metadata));
|
|
1384
|
+
}
|
|
1385
|
+
onBlocked(_key, _decision, metadata) {
|
|
1386
|
+
this.metricsClient.increment("halt.requests.blocked", this.getTags(metadata));
|
|
1387
|
+
}
|
|
1388
|
+
onQuotaCheck(_identifier, quota, _allowed) {
|
|
1389
|
+
const tags = { quota: quota.name, period: quota.period };
|
|
1390
|
+
this.metricsClient.increment("halt.quota.checks", tags);
|
|
1391
|
+
this.metricsClient.gauge(
|
|
1392
|
+
"halt.quota.remaining",
|
|
1393
|
+
quota.limit - (quota.currentUsage || 0),
|
|
1394
|
+
tags
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
onQuotaExceeded(_identifier, quota) {
|
|
1398
|
+
const tags = { quota: quota.name, period: quota.period };
|
|
1399
|
+
this.metricsClient.increment("halt.quota.exceeded", tags);
|
|
1400
|
+
}
|
|
1401
|
+
onPenaltyApplied(_identifier, penalty) {
|
|
1402
|
+
this.metricsClient.increment("halt.penalties.applied");
|
|
1403
|
+
this.metricsClient.gauge("halt.penalties.abuse_score", penalty.abuseScore);
|
|
1404
|
+
}
|
|
1405
|
+
onViolation(_identifier, _penalty, severity) {
|
|
1406
|
+
this.metricsClient.increment("halt.violations.total");
|
|
1407
|
+
this.metricsClient.histogram("halt.violations.severity", severity);
|
|
1408
|
+
}
|
|
1409
|
+
getTags(metadata) {
|
|
1410
|
+
if (!metadata) {
|
|
1411
|
+
return {};
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
policy: metadata.policy,
|
|
1415
|
+
algorithm: metadata.algorithm
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
var CompositeTelemetry = class {
|
|
1420
|
+
constructor(hooks) {
|
|
1421
|
+
this.hooks = hooks;
|
|
1422
|
+
}
|
|
1423
|
+
onCheck(key, decision, metadata) {
|
|
1424
|
+
this.hooks.forEach((hook) => hook.onCheck?.(key, decision, metadata));
|
|
1425
|
+
}
|
|
1426
|
+
onAllowed(key, decision, metadata) {
|
|
1427
|
+
this.hooks.forEach((hook) => hook.onAllowed?.(key, decision, metadata));
|
|
1428
|
+
}
|
|
1429
|
+
onBlocked(key, decision, metadata) {
|
|
1430
|
+
this.hooks.forEach((hook) => hook.onBlocked?.(key, decision, metadata));
|
|
1431
|
+
}
|
|
1432
|
+
onQuotaCheck(identifier, quota, allowed) {
|
|
1433
|
+
this.hooks.forEach((hook) => hook.onQuotaCheck?.(identifier, quota, allowed));
|
|
1434
|
+
}
|
|
1435
|
+
onQuotaExceeded(identifier, quota) {
|
|
1436
|
+
this.hooks.forEach((hook) => hook.onQuotaExceeded?.(identifier, quota));
|
|
1437
|
+
}
|
|
1438
|
+
onPenaltyApplied(identifier, penalty) {
|
|
1439
|
+
this.hooks.forEach((hook) => hook.onPenaltyApplied?.(identifier, penalty));
|
|
1440
|
+
}
|
|
1441
|
+
onViolation(identifier, penalty, severity) {
|
|
1442
|
+
this.hooks.forEach((hook) => hook.onViolation?.(identifier, penalty, severity));
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
|
|
1446
|
+
// src/core/stats.ts
|
|
1447
|
+
var StatsCollector = class {
|
|
1448
|
+
constructor(options = {}) {
|
|
1449
|
+
this.allowedTotal = 0;
|
|
1450
|
+
this.blockedTotal = 0;
|
|
1451
|
+
this.byPolicy = /* @__PURE__ */ new Map();
|
|
1452
|
+
this.byEndpoint = /* @__PURE__ */ new Map();
|
|
1453
|
+
this.limitedKeys = /* @__PURE__ */ new Map();
|
|
1454
|
+
this.costByPlan = /* @__PURE__ */ new Map();
|
|
1455
|
+
this.quotaExceededCount = 0;
|
|
1456
|
+
this.penaltiesAppliedCount = 0;
|
|
1457
|
+
this.violationsCount = 0;
|
|
1458
|
+
this.topN = options.topN ?? 20;
|
|
1459
|
+
this.maxTrackedKeys = options.maxTrackedKeys ?? 1e4;
|
|
1460
|
+
}
|
|
1461
|
+
onAllowed(_key, _decision, metadata) {
|
|
1462
|
+
this.allowedTotal++;
|
|
1463
|
+
const cost = Number(metadata?.cost ?? 1);
|
|
1464
|
+
this.policyTally(metadata).allowed++;
|
|
1465
|
+
const endpoint = metadata?.endpoint;
|
|
1466
|
+
if (endpoint) {
|
|
1467
|
+
const e = this.endpointTally(endpoint);
|
|
1468
|
+
e.allowed++;
|
|
1469
|
+
e.cost += cost;
|
|
1470
|
+
}
|
|
1471
|
+
const plan = metadata?.plan ?? metadata?.policy ?? "unknown";
|
|
1472
|
+
this.costByPlan.set(plan, (this.costByPlan.get(plan) ?? 0) + cost);
|
|
1473
|
+
}
|
|
1474
|
+
onBlocked(key, _decision, metadata) {
|
|
1475
|
+
this.blockedTotal++;
|
|
1476
|
+
this.policyTally(metadata).blocked++;
|
|
1477
|
+
const endpoint = metadata?.endpoint;
|
|
1478
|
+
if (endpoint) {
|
|
1479
|
+
this.endpointTally(endpoint).blocked++;
|
|
1480
|
+
}
|
|
1481
|
+
if (key) this.trackLimitedKey(key);
|
|
1482
|
+
}
|
|
1483
|
+
onQuotaExceeded(_identifier, _quota) {
|
|
1484
|
+
this.quotaExceededCount++;
|
|
1485
|
+
}
|
|
1486
|
+
onPenaltyApplied(_identifier, _penalty) {
|
|
1487
|
+
this.penaltiesAppliedCount++;
|
|
1488
|
+
}
|
|
1489
|
+
onViolation(_identifier, _penalty, _severity) {
|
|
1490
|
+
this.violationsCount++;
|
|
1491
|
+
}
|
|
1492
|
+
/** Point-in-time view of the aggregated stats. */
|
|
1493
|
+
snapshot() {
|
|
1494
|
+
const topLimitedKeys = [...this.limitedKeys.entries()].sort((a, b) => b[1] - a[1]).slice(0, this.topN).map(([key, blocked]) => ({ key, blocked }));
|
|
1495
|
+
return {
|
|
1496
|
+
allowedTotal: this.allowedTotal,
|
|
1497
|
+
blockedTotal: this.blockedTotal,
|
|
1498
|
+
byPolicy: mapToObject(this.byPolicy),
|
|
1499
|
+
byEndpoint: mapToObject(this.byEndpoint),
|
|
1500
|
+
topLimitedKeys,
|
|
1501
|
+
costByPlan: mapToObject(this.costByPlan),
|
|
1502
|
+
quotaExceeded: this.quotaExceededCount,
|
|
1503
|
+
penaltiesApplied: this.penaltiesAppliedCount,
|
|
1504
|
+
violations: this.violationsCount,
|
|
1505
|
+
trackedKeys: this.limitedKeys.size
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
/** Clear all counters (e.g. after exporting). */
|
|
1509
|
+
reset() {
|
|
1510
|
+
this.allowedTotal = 0;
|
|
1511
|
+
this.blockedTotal = 0;
|
|
1512
|
+
this.byPolicy.clear();
|
|
1513
|
+
this.byEndpoint.clear();
|
|
1514
|
+
this.limitedKeys.clear();
|
|
1515
|
+
this.costByPlan.clear();
|
|
1516
|
+
this.quotaExceededCount = 0;
|
|
1517
|
+
this.penaltiesAppliedCount = 0;
|
|
1518
|
+
this.violationsCount = 0;
|
|
1519
|
+
}
|
|
1520
|
+
policyTally(metadata) {
|
|
1521
|
+
const policy = metadata?.policy ?? "unknown";
|
|
1522
|
+
let t = this.byPolicy.get(policy);
|
|
1523
|
+
if (!t) {
|
|
1524
|
+
t = { allowed: 0, blocked: 0 };
|
|
1525
|
+
this.byPolicy.set(policy, t);
|
|
1526
|
+
}
|
|
1527
|
+
return t;
|
|
1528
|
+
}
|
|
1529
|
+
endpointTally(endpoint) {
|
|
1530
|
+
let e = this.byEndpoint.get(endpoint);
|
|
1531
|
+
if (!e) {
|
|
1532
|
+
e = { allowed: 0, blocked: 0, cost: 0 };
|
|
1533
|
+
this.byEndpoint.set(endpoint, e);
|
|
1534
|
+
}
|
|
1535
|
+
return e;
|
|
1536
|
+
}
|
|
1537
|
+
trackLimitedKey(key) {
|
|
1538
|
+
const current = this.limitedKeys.get(key);
|
|
1539
|
+
if (current !== void 0) {
|
|
1540
|
+
this.limitedKeys.set(key, current + 1);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (this.limitedKeys.size >= this.maxTrackedKeys) {
|
|
1544
|
+
this.evictSmallest();
|
|
1545
|
+
}
|
|
1546
|
+
this.limitedKeys.set(key, 1);
|
|
1547
|
+
}
|
|
1548
|
+
/** Evict the lowest-count tracked key to keep memory bounded. */
|
|
1549
|
+
evictSmallest() {
|
|
1550
|
+
let minKey;
|
|
1551
|
+
let minVal = Infinity;
|
|
1552
|
+
for (const [k, v] of this.limitedKeys) {
|
|
1553
|
+
if (v < minVal) {
|
|
1554
|
+
minVal = v;
|
|
1555
|
+
minKey = k;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (minKey !== void 0) this.limitedKeys.delete(minKey);
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
function mapToObject(map) {
|
|
1562
|
+
const out = {};
|
|
1563
|
+
for (const [k, v] of map) out[k] = v;
|
|
1564
|
+
return out;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/observability/otel.ts
|
|
1568
|
+
var OpenTelemetryMetrics = class {
|
|
1569
|
+
constructor(meter) {
|
|
1570
|
+
this.requests = meter.createCounter("halt.requests", {
|
|
1571
|
+
description: "Total rate-limit checks"
|
|
1572
|
+
});
|
|
1573
|
+
this.blocked = meter.createCounter("halt.blocked", {
|
|
1574
|
+
description: "Rate-limited (blocked) requests"
|
|
1575
|
+
});
|
|
1576
|
+
this.cost = meter.createCounter("halt.cost", {
|
|
1577
|
+
description: "Consumed request cost (weighted endpoints)"
|
|
1578
|
+
});
|
|
1579
|
+
this.quotaExceededCounter = meter.createCounter("halt.quota.exceeded", {
|
|
1580
|
+
description: "Quota-exceeded events"
|
|
1581
|
+
});
|
|
1582
|
+
this.penaltyAppliedCounter = meter.createCounter("halt.penalty.applied", {
|
|
1583
|
+
description: "Penalties applied (abuse controls)"
|
|
1584
|
+
});
|
|
1585
|
+
this.violationsCounter = meter.createCounter("halt.violations", {
|
|
1586
|
+
description: "Recorded abuse violations"
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
onAllowed(_key, _decision, metadata) {
|
|
1590
|
+
const policy = String(metadata?.policy ?? "unknown");
|
|
1591
|
+
this.requests.add(1, { policy, allowed: "true" });
|
|
1592
|
+
const endpoint = metadata?.endpoint;
|
|
1593
|
+
const plan = String(metadata?.plan ?? metadata?.policy ?? "unknown");
|
|
1594
|
+
const cost = Number(metadata?.cost ?? 1);
|
|
1595
|
+
this.cost.add(cost, endpoint ? { endpoint: String(endpoint), plan } : { plan });
|
|
1596
|
+
}
|
|
1597
|
+
onBlocked(_key, _decision, metadata) {
|
|
1598
|
+
const policy = String(metadata?.policy ?? "unknown");
|
|
1599
|
+
this.requests.add(1, { policy, allowed: "false" });
|
|
1600
|
+
const endpoint = metadata?.endpoint;
|
|
1601
|
+
this.blocked.add(1, endpoint ? { policy, endpoint: String(endpoint) } : { policy });
|
|
1602
|
+
}
|
|
1603
|
+
onQuotaExceeded(_identifier, quota) {
|
|
1604
|
+
this.quotaExceededCounter.add(1, { quota: quota.name });
|
|
1605
|
+
}
|
|
1606
|
+
onPenaltyApplied(_identifier, _penalty) {
|
|
1607
|
+
this.penaltyAppliedCounter.add(1);
|
|
1608
|
+
}
|
|
1609
|
+
onViolation(_identifier, _penalty, severity) {
|
|
1610
|
+
this.violationsCounter.add(1, { severity });
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
export { Algorithm, CompositeTelemetry, InMemoryStore, KeyStrategy, LoggingTelemetry, MetricsTelemetry, OpenTelemetryMetrics, PENALTY_LENIENT, PENALTY_MODERATE, PENALTY_STRICT, PenaltyManager, QUOTA_ENTERPRISE_MONTHLY, QUOTA_FREE_DAILY, QUOTA_FREE_MONTHLY, QUOTA_PRO_DAILY, QUOTA_PRO_MONTHLY, QuotaManager, QuotaPeriod, RateLimiter, RedisStore, StatsCollector, extractors_exports as extractors, isAtomicStore, normalizePolicy, presets_exports as presets, toHeaders };
|
|
1070
1615
|
//# sourceMappingURL=index.mjs.map
|
|
1071
1616
|
//# sourceMappingURL=index.mjs.map
|