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