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