@loopstack/quota 0.20.7

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 (73) hide show
  1. package/dist/calculators/__tests__/ai-generate-text-quota.calculator.spec.d.ts +2 -0
  2. package/dist/calculators/__tests__/ai-generate-text-quota.calculator.spec.d.ts.map +1 -0
  3. package/dist/calculators/__tests__/ai-generate-text-quota.calculator.spec.js +63 -0
  4. package/dist/calculators/__tests__/ai-generate-text-quota.calculator.spec.js.map +1 -0
  5. package/dist/calculators/__tests__/processing-time-quota.calculator.spec.d.ts +2 -0
  6. package/dist/calculators/__tests__/processing-time-quota.calculator.spec.d.ts.map +1 -0
  7. package/dist/calculators/__tests__/processing-time-quota.calculator.spec.js +52 -0
  8. package/dist/calculators/__tests__/processing-time-quota.calculator.spec.js.map +1 -0
  9. package/dist/calculators/ai-generate-text-quota.calculator.d.ts +8 -0
  10. package/dist/calculators/ai-generate-text-quota.calculator.d.ts.map +1 -0
  11. package/dist/calculators/ai-generate-text-quota.calculator.js +17 -0
  12. package/dist/calculators/ai-generate-text-quota.calculator.js.map +1 -0
  13. package/dist/calculators/index.d.ts +3 -0
  14. package/dist/calculators/index.d.ts.map +1 -0
  15. package/dist/calculators/index.js +19 -0
  16. package/dist/calculators/index.js.map +1 -0
  17. package/dist/calculators/processing-time-quota.calculator.d.ts +8 -0
  18. package/dist/calculators/processing-time-quota.calculator.d.ts.map +1 -0
  19. package/dist/calculators/processing-time-quota.calculator.js +14 -0
  20. package/dist/calculators/processing-time-quota.calculator.js.map +1 -0
  21. package/dist/index.d.ts +5 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +21 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/interfaces/index.d.ts +4 -0
  26. package/dist/interfaces/index.d.ts.map +1 -0
  27. package/dist/interfaces/index.js +20 -0
  28. package/dist/interfaces/index.js.map +1 -0
  29. package/dist/interfaces/quota-client.interface.d.ts +11 -0
  30. package/dist/interfaces/quota-client.interface.d.ts.map +1 -0
  31. package/dist/interfaces/quota-client.interface.js +5 -0
  32. package/dist/interfaces/quota-client.interface.js.map +1 -0
  33. package/dist/interfaces/quota.interface.d.ts +5 -0
  34. package/dist/interfaces/quota.interface.d.ts.map +1 -0
  35. package/dist/interfaces/quota.interface.js +3 -0
  36. package/dist/interfaces/quota.interface.js.map +1 -0
  37. package/dist/interfaces/tool-quota-calculator.interface.d.ts +7 -0
  38. package/dist/interfaces/tool-quota-calculator.interface.d.ts.map +1 -0
  39. package/dist/interfaces/tool-quota-calculator.interface.js +3 -0
  40. package/dist/interfaces/tool-quota-calculator.interface.js.map +1 -0
  41. package/dist/quota.module.d.ts +16 -0
  42. package/dist/quota.module.d.ts.map +1 -0
  43. package/dist/quota.module.js +88 -0
  44. package/dist/quota.module.js.map +1 -0
  45. package/dist/services/__tests__/quota-calculator-registry.service.spec.d.ts +2 -0
  46. package/dist/services/__tests__/quota-calculator-registry.service.spec.d.ts.map +1 -0
  47. package/dist/services/__tests__/quota-calculator-registry.service.spec.js +45 -0
  48. package/dist/services/__tests__/quota-calculator-registry.service.spec.js.map +1 -0
  49. package/dist/services/__tests__/quota-client.service.spec.d.ts +2 -0
  50. package/dist/services/__tests__/quota-client.service.spec.d.ts.map +1 -0
  51. package/dist/services/__tests__/quota-client.service.spec.js +83 -0
  52. package/dist/services/__tests__/quota-client.service.spec.js.map +1 -0
  53. package/dist/services/__tests__/quota.interceptor.spec.d.ts +2 -0
  54. package/dist/services/__tests__/quota.interceptor.spec.d.ts.map +1 -0
  55. package/dist/services/__tests__/quota.interceptor.spec.js +117 -0
  56. package/dist/services/__tests__/quota.interceptor.spec.js.map +1 -0
  57. package/dist/services/index.d.ts +4 -0
  58. package/dist/services/index.d.ts.map +1 -0
  59. package/dist/services/index.js +20 -0
  60. package/dist/services/index.js.map +1 -0
  61. package/dist/services/quota-calculator-registry.service.d.ts +9 -0
  62. package/dist/services/quota-calculator-registry.service.d.ts.map +1 -0
  63. package/dist/services/quota-calculator-registry.service.js +33 -0
  64. package/dist/services/quota-calculator-registry.service.js.map +1 -0
  65. package/dist/services/quota-client.service.d.ts +12 -0
  66. package/dist/services/quota-client.service.d.ts.map +1 -0
  67. package/dist/services/quota-client.service.js +74 -0
  68. package/dist/services/quota-client.service.js.map +1 -0
  69. package/dist/services/quota.interceptor.d.ts +14 -0
  70. package/dist/services/quota.interceptor.d.ts.map +1 -0
  71. package/dist/services/quota.interceptor.js +66 -0
  72. package/dist/services/quota.interceptor.js.map +1 -0
  73. package/package.json +53 -0
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const quota_client_service_1 = require("../quota-client.service");
4
+ describe('QuotaClientService', () => {
5
+ describe('without Redis (null)', () => {
6
+ let service;
7
+ beforeEach(() => {
8
+ service = new quota_client_service_1.QuotaClientService(null);
9
+ });
10
+ it('checkQuota should return not exceeded with unlimited', async () => {
11
+ const result = await service.checkQuota('user-1', 'default-token');
12
+ expect(result).toEqual({ exceeded: false, used: 0, limit: -1 });
13
+ });
14
+ it('report should complete without error', async () => {
15
+ await expect(service.report('user-1', 'default-token', 100)).resolves.toBeUndefined();
16
+ });
17
+ });
18
+ describe('with Redis mock', () => {
19
+ let service;
20
+ let redisMock;
21
+ beforeEach(() => {
22
+ redisMock = {
23
+ get: jest.fn(),
24
+ incrby: jest.fn(),
25
+ };
26
+ service = new quota_client_service_1.QuotaClientService(redisMock);
27
+ });
28
+ describe('checkQuota', () => {
29
+ it('should return not exceeded when under limit', async () => {
30
+ redisMock.get.mockResolvedValueOnce('50').mockResolvedValueOnce('1000');
31
+ const result = await service.checkQuota('user-1', 'default-token');
32
+ expect(result).toEqual({ exceeded: false, used: 50, limit: 1000 });
33
+ });
34
+ it('should return exceeded when at limit', async () => {
35
+ redisMock.get.mockResolvedValueOnce('1000').mockResolvedValueOnce('1000');
36
+ const result = await service.checkQuota('user-1', 'default-token');
37
+ expect(result).toEqual({ exceeded: true, used: 1000, limit: 1000 });
38
+ });
39
+ it('should return exceeded when over limit', async () => {
40
+ redisMock.get.mockResolvedValueOnce('1500').mockResolvedValueOnce('1000');
41
+ const result = await service.checkQuota('user-1', 'default-token');
42
+ expect(result).toEqual({ exceeded: true, used: 1500, limit: 1000 });
43
+ });
44
+ it('should block when no limit key exists (no quota assigned)', async () => {
45
+ redisMock.get.mockResolvedValueOnce('500').mockResolvedValueOnce(null);
46
+ const result = await service.checkQuota('user-1', 'default-token');
47
+ expect(result).toEqual({ exceeded: true, used: 500, limit: 0 });
48
+ });
49
+ it('should treat missing used as 0', async () => {
50
+ redisMock.get.mockResolvedValueOnce(null).mockResolvedValueOnce('1000');
51
+ const result = await service.checkQuota('user-1', 'default-token');
52
+ expect(result).toEqual({ exceeded: false, used: 0, limit: 1000 });
53
+ });
54
+ it('should treat explicit limit of -1 as unlimited', async () => {
55
+ redisMock.get.mockResolvedValueOnce('999999').mockResolvedValueOnce('-1');
56
+ const result = await service.checkQuota('user-1', 'default-token');
57
+ expect(result).toEqual({ exceeded: false, used: 999999, limit: -1 });
58
+ });
59
+ it('should block when no limit key and no usage exist', async () => {
60
+ redisMock.get.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
61
+ const result = await service.checkQuota('user-1', 'default-token');
62
+ expect(result).toEqual({ exceeded: true, used: 0, limit: 0 });
63
+ });
64
+ it('should fail-open on Redis error', async () => {
65
+ redisMock.get.mockRejectedValue(new Error('Connection refused'));
66
+ const result = await service.checkQuota('user-1', 'default-token');
67
+ expect(result).toEqual({ exceeded: false, used: 0, limit: -1 });
68
+ });
69
+ });
70
+ describe('report', () => {
71
+ it('should increment the used key', async () => {
72
+ redisMock.incrby.mockResolvedValue(150);
73
+ await service.report('user-1', 'default-token', 150);
74
+ expect(redisMock.incrby).toHaveBeenCalledWith('user:user-1:quota:default-token:used', 150);
75
+ });
76
+ it('should not throw on Redis error', async () => {
77
+ redisMock.incrby.mockRejectedValue(new Error('Connection refused'));
78
+ await expect(service.report('user-1', 'default-token', 100)).resolves.toBeUndefined();
79
+ });
80
+ });
81
+ });
82
+ });
83
+ //# sourceMappingURL=quota-client.service.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-client.service.spec.js","sourceRoot":"","sources":["../../../src/services/__tests__/quota-client.service.spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=quota.interceptor.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.interceptor.spec.d.ts","sourceRoot":"","sources":["../../../src/services/__tests__/quota.interceptor.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const quota_calculator_registry_service_1 = require("../quota-calculator-registry.service");
4
+ const quota_interceptor_1 = require("../quota.interceptor");
5
+ class FakeTool {
6
+ name;
7
+ constructor(name) {
8
+ this.name = name;
9
+ Object.defineProperty(this.constructor, 'name', { value: name });
10
+ }
11
+ }
12
+ function createContext(toolName, overrides) {
13
+ return {
14
+ tool: new FakeTool(toolName),
15
+ args: undefined,
16
+ runContext: { userId: 'user-1' },
17
+ ...overrides,
18
+ };
19
+ }
20
+ describe('QuotaInterceptor', () => {
21
+ let interceptor;
22
+ let quotaClientService;
23
+ let registry;
24
+ beforeEach(() => {
25
+ quotaClientService = {
26
+ checkQuota: jest.fn().mockResolvedValue({ exceeded: false, used: 0, limit: -1 }),
27
+ report: jest.fn().mockResolvedValue(undefined),
28
+ };
29
+ registry = new quota_calculator_registry_service_1.QuotaCalculatorRegistry();
30
+ interceptor = new quota_interceptor_1.QuotaInterceptor(quotaClientService, registry);
31
+ });
32
+ describe('beforeExecute', () => {
33
+ it('should check processing time quota even when no calculator is registered', async () => {
34
+ const context = createContext('UnknownTool');
35
+ await interceptor.beforeExecute(context);
36
+ expect(quotaClientService.checkQuota).toHaveBeenCalledWith('user-1', 'processing-time-ms');
37
+ expect(quotaClientService.checkQuota).toHaveBeenCalledTimes(1);
38
+ });
39
+ it('should throw when processing time quota is exceeded for any tool', async () => {
40
+ quotaClientService.checkQuota.mockResolvedValue({ exceeded: true, used: 60000, limit: 60000 });
41
+ const context = createContext('UnknownTool');
42
+ await expect(interceptor.beforeExecute(context)).rejects.toThrow('Quota exceeded for "processing-time-ms": 60000/60000');
43
+ });
44
+ it('should check both processing time and tool-specific quota', async () => {
45
+ registry.register('AiGenerateText', {
46
+ quotaType: 'default-token',
47
+ calculateQuotaUsage: jest.fn(),
48
+ });
49
+ const context = createContext('AiGenerateText');
50
+ await interceptor.beforeExecute(context);
51
+ expect(quotaClientService.checkQuota).toHaveBeenCalledWith('user-1', 'processing-time-ms');
52
+ expect(quotaClientService.checkQuota).toHaveBeenCalledWith('user-1', 'default-token');
53
+ expect(quotaClientService.checkQuota).toHaveBeenCalledTimes(2);
54
+ });
55
+ it('should throw when tool-specific quota is exceeded', async () => {
56
+ registry.register('AiGenerateText', {
57
+ quotaType: 'default-token',
58
+ calculateQuotaUsage: jest.fn(),
59
+ });
60
+ quotaClientService.checkQuota
61
+ .mockResolvedValueOnce({ exceeded: false, used: 0, limit: -1 })
62
+ .mockResolvedValueOnce({ exceeded: true, used: 1000, limit: 1000 });
63
+ const context = createContext('AiGenerateText');
64
+ await expect(interceptor.beforeExecute(context)).rejects.toThrow('Quota exceeded for "default-token": 1000/1000');
65
+ });
66
+ it('should not throw when all quotas are within limits', async () => {
67
+ registry.register('AiGenerateText', {
68
+ quotaType: 'default-token',
69
+ calculateQuotaUsage: jest.fn(),
70
+ });
71
+ quotaClientService.checkQuota.mockResolvedValue({ exceeded: false, used: 500, limit: 1000 });
72
+ const context = createContext('AiGenerateText');
73
+ await expect(interceptor.beforeExecute(context)).resolves.toBeUndefined();
74
+ });
75
+ });
76
+ describe('afterExecute', () => {
77
+ const result = { data: {} };
78
+ it('should report processing time for every tool', async () => {
79
+ const context = createContext('UnknownTool', { metrics: { durationMs: 250 } });
80
+ await interceptor.afterExecute(context, result);
81
+ expect(quotaClientService.report).toHaveBeenCalledWith('user-1', 'processing-time-ms', 250);
82
+ });
83
+ it('should not report processing time when metrics are missing', async () => {
84
+ const context = createContext('UnknownTool');
85
+ await interceptor.afterExecute(context, result);
86
+ expect(quotaClientService.report).not.toHaveBeenCalled();
87
+ });
88
+ it('should report tool-specific usage when calculator returns usage', async () => {
89
+ registry.register('AiGenerateText', {
90
+ quotaType: 'default-token',
91
+ calculateQuotaUsage: jest.fn().mockReturnValue({ quotaType: 'default-token', actualAmount: 500 }),
92
+ });
93
+ const context = createContext('AiGenerateText', { metrics: { durationMs: 1000 } });
94
+ await interceptor.afterExecute(context, result);
95
+ expect(quotaClientService.report).toHaveBeenCalledWith('user-1', 'processing-time-ms', 1000);
96
+ expect(quotaClientService.report).toHaveBeenCalledWith('user-1', 'default-token', 500);
97
+ expect(quotaClientService.report).toHaveBeenCalledTimes(2);
98
+ });
99
+ it('should not report tool-specific usage when calculator returns null', async () => {
100
+ registry.register('AiGenerateText', {
101
+ quotaType: 'default-token',
102
+ calculateQuotaUsage: jest.fn().mockReturnValue(null),
103
+ });
104
+ const context = createContext('AiGenerateText', { metrics: { durationMs: 100 } });
105
+ await interceptor.afterExecute(context, result);
106
+ expect(quotaClientService.report).toHaveBeenCalledTimes(1);
107
+ expect(quotaClientService.report).toHaveBeenCalledWith('user-1', 'processing-time-ms', 100);
108
+ });
109
+ });
110
+ describe('onError', () => {
111
+ it('should not throw', async () => {
112
+ const context = createContext('AiGenerateText');
113
+ await expect(interceptor.onError(context, new Error('fail'))).resolves.toBeUndefined();
114
+ });
115
+ });
116
+ });
117
+ //# sourceMappingURL=quota.interceptor.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.interceptor.spec.js","sourceRoot":"","sources":["../../../src/services/__tests__/quota.interceptor.spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export * from './quota-calculator-registry.service';
2
+ export * from './quota-client.service';
3
+ export * from './quota.interceptor';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./quota-calculator-registry.service"), exports);
18
+ __exportStar(require("./quota-client.service"), exports);
19
+ __exportStar(require("./quota.interceptor"), exports);
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sEAAoD;AACpD,yDAAuC;AACvC,sDAAoC"}
@@ -0,0 +1,9 @@
1
+ import { ToolQuotaCalculator } from '../interfaces/tool-quota-calculator.interface';
2
+ export declare class QuotaCalculatorRegistry {
3
+ private readonly logger;
4
+ private readonly calculators;
5
+ register(toolClassName: string, calculator: ToolQuotaCalculator): void;
6
+ get(toolClassName: string): ToolQuotaCalculator | undefined;
7
+ has(toolClassName: string): boolean;
8
+ }
9
+ //# sourceMappingURL=quota-calculator-registry.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-calculator-registry.service.d.ts","sourceRoot":"","sources":["../../src/services/quota-calculator-registry.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AAEpF,qBACa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4C;IACnE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA0C;IAEtE,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,mBAAmB,GAAG,IAAI;IAQtE,GAAG,CAAC,aAAa,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI3D,GAAG,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO;CAGpC"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var QuotaCalculatorRegistry_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.QuotaCalculatorRegistry = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ let QuotaCalculatorRegistry = QuotaCalculatorRegistry_1 = class QuotaCalculatorRegistry {
13
+ logger = new common_1.Logger(QuotaCalculatorRegistry_1.name);
14
+ calculators = new Map();
15
+ register(toolClassName, calculator) {
16
+ if (this.calculators.has(toolClassName)) {
17
+ this.logger.warn(`Quota calculator for "${toolClassName}" already registered, overriding`);
18
+ }
19
+ this.calculators.set(toolClassName, calculator);
20
+ this.logger.log(`Registered quota calculator for tool: ${toolClassName}`);
21
+ }
22
+ get(toolClassName) {
23
+ return this.calculators.get(toolClassName);
24
+ }
25
+ has(toolClassName) {
26
+ return this.calculators.has(toolClassName);
27
+ }
28
+ };
29
+ exports.QuotaCalculatorRegistry = QuotaCalculatorRegistry;
30
+ exports.QuotaCalculatorRegistry = QuotaCalculatorRegistry = QuotaCalculatorRegistry_1 = __decorate([
31
+ (0, common_1.Injectable)()
32
+ ], QuotaCalculatorRegistry);
33
+ //# sourceMappingURL=quota-calculator-registry.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-calculator-registry.service.js","sourceRoot":"","sources":["../../src/services/quota-calculator-registry.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAAoD;AAI7C,IAAM,uBAAuB,+BAA7B,MAAM,uBAAuB;IACjB,MAAM,GAAG,IAAI,eAAM,CAAC,yBAAuB,CAAC,IAAI,CAAC,CAAC;IAClD,WAAW,GAAG,IAAI,GAAG,EAA+B,CAAC;IAEtE,QAAQ,CAAC,aAAqB,EAAE,UAA+B;QAC7D,IAAI,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;YACxC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,aAAa,kCAAkC,CAAC,CAAC;QAC7F,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QAChD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,yCAAyC,aAAa,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,GAAG,CAAC,aAAqB;QACvB,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC7C,CAAC;IAED,GAAG,CAAC,aAAqB;QACvB,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC7C,CAAC;CACF,CAAA;AAnBY,0DAAuB;kCAAvB,uBAAuB;IADnC,IAAA,mBAAU,GAAE;GACA,uBAAuB,CAmBnC"}
@@ -0,0 +1,12 @@
1
+ import Redis from 'ioredis';
2
+ import { QuotaCheckResult, QuotaClientServiceInterface } from '../interfaces';
3
+ export declare const QUOTA_REDIS = "QUOTA_REDIS";
4
+ export declare class QuotaClientService implements QuotaClientServiceInterface {
5
+ private readonly redis;
6
+ private readonly logger;
7
+ constructor(redis: Redis | null);
8
+ checkQuota(userId: string, quotaType: string): Promise<QuotaCheckResult>;
9
+ report(userId: string, quotaType: string, amount: number): Promise<void>;
10
+ private key;
11
+ }
12
+ //# sourceMappingURL=quota-client.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-client.service.d.ts","sourceRoot":"","sources":["../../src/services/quota-client.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;AAE9E,eAAO,MAAM,WAAW,gBAAgB,CAAC;AAEzC,qBACa,kBAAmB,YAAW,2BAA2B;IAMlE,OAAO,CAAC,QAAQ,CAAC,KAAK;IALxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;gBAK3C,KAAK,EAAE,KAAK,GAAG,IAAI;IAGhC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAsCxE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB9E,OAAO,CAAC,GAAG;CAGZ"}
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var QuotaClientService_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.QuotaClientService = exports.QUOTA_REDIS = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ exports.QUOTA_REDIS = 'QUOTA_REDIS';
19
+ let QuotaClientService = QuotaClientService_1 = class QuotaClientService {
20
+ redis;
21
+ logger = new common_1.Logger(QuotaClientService_1.name);
22
+ constructor(redis) {
23
+ this.redis = redis;
24
+ }
25
+ async checkQuota(userId, quotaType) {
26
+ if (!this.redis) {
27
+ return { exceeded: false, used: 0, limit: -1 };
28
+ }
29
+ const usedKey = this.key(userId, quotaType, 'used');
30
+ const limitKey = this.key(userId, quotaType, 'limit');
31
+ try {
32
+ const [usedStr, limitStr] = await Promise.all([this.redis.get(usedKey), this.redis.get(limitKey)]);
33
+ const used = usedStr ? parseInt(usedStr, 10) : 0;
34
+ if (limitStr == null) {
35
+ return { exceeded: true, used, limit: 0 };
36
+ }
37
+ const limit = parseInt(limitStr, 10);
38
+ if (limit === -1) {
39
+ return { exceeded: false, used, limit };
40
+ }
41
+ if (used >= limit) {
42
+ return { exceeded: true, used, limit };
43
+ }
44
+ return { exceeded: false, used, limit };
45
+ }
46
+ catch (error) {
47
+ this.logger.warn(`Quota check failed for user ${userId}, quota ${quotaType}: ${String(error)}. Allowing (fail-open).`);
48
+ return { exceeded: false, used: 0, limit: -1 };
49
+ }
50
+ }
51
+ async report(userId, quotaType, amount) {
52
+ if (!this.redis) {
53
+ return;
54
+ }
55
+ const usedKey = this.key(userId, quotaType, 'used');
56
+ try {
57
+ await this.redis.incrby(usedKey, amount);
58
+ }
59
+ catch (error) {
60
+ this.logger.warn(`Quota report failed for user ${userId}, quota ${quotaType}: ${String(error)}. Skipping (fail-open).`);
61
+ }
62
+ }
63
+ key(userId, quotaType, suffix) {
64
+ return `user:${userId}:quota:${quotaType}:${suffix}`;
65
+ }
66
+ };
67
+ exports.QuotaClientService = QuotaClientService;
68
+ exports.QuotaClientService = QuotaClientService = QuotaClientService_1 = __decorate([
69
+ (0, common_1.Injectable)(),
70
+ __param(0, (0, common_1.Optional)()),
71
+ __param(0, (0, common_1.Inject)(exports.QUOTA_REDIS)),
72
+ __metadata("design:paramtypes", [Object])
73
+ ], QuotaClientService);
74
+ //# sourceMappingURL=quota-client.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-client.service.js","sourceRoot":"","sources":["../../src/services/quota-client.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { ToolExecutionContext, ToolExecutionInterceptor, ToolResult } from '@loopstack/common';
2
+ import { QuotaCalculatorRegistry } from './quota-calculator-registry.service';
3
+ import { QuotaClientService } from './quota-client.service';
4
+ export declare class QuotaInterceptor implements ToolExecutionInterceptor {
5
+ private readonly quotaClientService;
6
+ private readonly calculatorRegistry;
7
+ private readonly logger;
8
+ private readonly processingTimeCalculator;
9
+ constructor(quotaClientService: QuotaClientService, calculatorRegistry: QuotaCalculatorRegistry);
10
+ beforeExecute(context: ToolExecutionContext): Promise<void>;
11
+ afterExecute(context: ToolExecutionContext, result: ToolResult): Promise<void>;
12
+ onError(_context: ToolExecutionContext, _error: unknown): Promise<void>;
13
+ }
14
+ //# sourceMappingURL=quota.interceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.interceptor.d.ts","sourceRoot":"","sources":["../../src/services/quota.interceptor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,qBACa,gBAAiB,YAAW,wBAAwB;IAK7D,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IALrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqC;IAC5D,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAuC;gBAG7D,kBAAkB,EAAE,kBAAkB,EACtC,kBAAkB,EAAE,uBAAuB;IAGxD,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB3D,YAAY,CAAC,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB9E,OAAO,CAAC,QAAQ,EAAE,oBAAoB,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAG9E"}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var QuotaInterceptor_1;
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.QuotaInterceptor = void 0;
14
+ const common_1 = require("@nestjs/common");
15
+ const calculators_1 = require("../calculators");
16
+ const quota_calculator_registry_service_1 = require("./quota-calculator-registry.service");
17
+ const quota_client_service_1 = require("./quota-client.service");
18
+ let QuotaInterceptor = QuotaInterceptor_1 = class QuotaInterceptor {
19
+ quotaClientService;
20
+ calculatorRegistry;
21
+ logger = new common_1.Logger(QuotaInterceptor_1.name);
22
+ processingTimeCalculator = new calculators_1.ProcessingTimeQuotaCalculator();
23
+ constructor(quotaClientService, calculatorRegistry) {
24
+ this.quotaClientService = quotaClientService;
25
+ this.calculatorRegistry = calculatorRegistry;
26
+ }
27
+ async beforeExecute(context) {
28
+ const userId = context.runContext.userId;
29
+ const timeCheck = await this.quotaClientService.checkQuota(userId, this.processingTimeCalculator.quotaType);
30
+ if (timeCheck.exceeded) {
31
+ throw new Error(`Quota exceeded for "${this.processingTimeCalculator.quotaType}": ${timeCheck.used}/${timeCheck.limit}`);
32
+ }
33
+ const toolClassName = context.tool.constructor.name;
34
+ const calculator = this.calculatorRegistry.get(toolClassName);
35
+ if (!calculator)
36
+ return;
37
+ const checkResult = await this.quotaClientService.checkQuota(userId, calculator.quotaType);
38
+ if (checkResult.exceeded) {
39
+ throw new Error(`Quota exceeded for "${calculator.quotaType}": ${checkResult.used}/${checkResult.limit}`);
40
+ }
41
+ }
42
+ async afterExecute(context, result) {
43
+ const userId = context.runContext.userId;
44
+ const timeUsage = this.processingTimeCalculator.calculateQuotaUsage(context, result);
45
+ if (timeUsage) {
46
+ await this.quotaClientService.report(userId, timeUsage.quotaType, timeUsage.actualAmount);
47
+ }
48
+ const toolClassName = context.tool.constructor.name;
49
+ const calculator = this.calculatorRegistry.get(toolClassName);
50
+ if (!calculator)
51
+ return;
52
+ const usage = calculator.calculateQuotaUsage(context, result);
53
+ if (!usage)
54
+ return;
55
+ await this.quotaClientService.report(userId, usage.quotaType, usage.actualAmount);
56
+ }
57
+ async onError(_context, _error) {
58
+ }
59
+ };
60
+ exports.QuotaInterceptor = QuotaInterceptor;
61
+ exports.QuotaInterceptor = QuotaInterceptor = QuotaInterceptor_1 = __decorate([
62
+ (0, common_1.Injectable)(),
63
+ __metadata("design:paramtypes", [quota_client_service_1.QuotaClientService,
64
+ quota_calculator_registry_service_1.QuotaCalculatorRegistry])
65
+ ], QuotaInterceptor);
66
+ //# sourceMappingURL=quota.interceptor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.interceptor.js","sourceRoot":"","sources":["../../src/services/quota.interceptor.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAAoD;AAEpD,gDAA+D;AAC/D,2FAA8E;AAC9E,iEAA4D;AAGrD,IAAM,gBAAgB,wBAAtB,MAAM,gBAAgB;IAKR;IACA;IALF,MAAM,GAAG,IAAI,eAAM,CAAC,kBAAgB,CAAC,IAAI,CAAC,CAAC;IAC3C,wBAAwB,GAAG,IAAI,2CAA6B,EAAE,CAAC;IAEhF,YACmB,kBAAsC,EACtC,kBAA2C;QAD3C,uBAAkB,GAAlB,kBAAkB,CAAoB;QACtC,uBAAkB,GAAlB,kBAAkB,CAAyB;IAC3D,CAAC;IAEJ,KAAK,CAAC,aAAa,CAAC,OAA6B;QAC/C,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC;QAGzC,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,wBAAwB,CAAC,SAAS,CAAC,CAAC;QAC5G,IAAI,SAAS,CAAC,QAAQ,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACb,uBAAuB,IAAI,CAAC,wBAAwB,CAAC,SAAS,MAAM,SAAS,CAAC,IAAI,IAAI,SAAS,CAAC,KAAK,EAAE,CACxG,CAAC;QACJ,CAAC;QAGD,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;QACpD,MAAM,UAAU,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC9D,IAAI,CAAC,UAAU;YAAE,OAAO;QAExB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;QAC3F,IAAI,WAAW,CAAC,QAAQ,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,uBAAuB,UAAU,CAAC,SAAS,MAAM,WAAW,CAAC,IAAI,IAAI,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC;QAC5G,CAAC;IACH,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAA6B,EAAE,MAAkB;QAClE,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC;QAGzC,MAAM,SAAS,GAAG,IAAI,CAAC,wBAAwB,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACrF,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;QAC5F,CAAC;QAGD,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;QACpD,MAAM,UAAU,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC9D,IAAI,CAAC,UAAU;YAAE,OAAO;QAExB,MAAM,KAAK,GAAG,UAAU,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC9D,IAAI,CAAC,KAAK;YAAE,OAAO;QAEnB,MAAM,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IACpF,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,QAA8B,EAAE,MAAe;IAE7D,CAAC;CACF,CAAA;AAtDY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,mBAAU,GAAE;qCAM4B,yCAAkB;QAClB,2DAAuB;GANnD,gBAAgB,CAsD5B"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@loopstack/quota",
3
+ "displayName": "Loopstack Quota Module",
4
+ "description": "Opt-in quota tracking and enforcement for Loopstack workflows",
5
+ "version": "0.20.7",
6
+ "license": "BSL",
7
+ "author": {
8
+ "name": "Jakob Klippel",
9
+ "url": "https://www.linkedin.com/in/jakob-klippel/"
10
+ },
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "scripts": {
14
+ "build": "nest build",
15
+ "compile": "tsc --noEmit",
16
+ "format": "prettier --write .",
17
+ "lint": "eslint .",
18
+ "test": "jest --passWithNoTests",
19
+ "watch": "nest build --watch"
20
+ },
21
+ "dependencies": {
22
+ "@loopstack/common": "^0.21.0",
23
+ "@nestjs/common": "^11.1.14",
24
+ "ioredis": "^5.6.1"
25
+ },
26
+ "devDependencies": {
27
+ "@nestjs/cli": "^11.0.16",
28
+ "@types/jest": "^30.0.0",
29
+ "jest": "^30.2.0",
30
+ "ts-jest": "^29.4.6"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "jest": {
36
+ "moduleFileExtensions": [
37
+ "js",
38
+ "json",
39
+ "ts"
40
+ ],
41
+ "rootDir": "src",
42
+ "testRegex": ".*\\.spec\\.ts$",
43
+ "transform": {
44
+ "^.+\\.ts$": "ts-jest"
45
+ },
46
+ "collectCoverageFrom": [
47
+ "**/*.(t|j)s"
48
+ ],
49
+ "coverageDirectory": "../coverage",
50
+ "testEnvironment": "node",
51
+ "maxWorkers": 1
52
+ }
53
+ }