guardrail-core 1.0.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/__tests__/autopilot.test.d.ts +7 -0
- package/dist/__tests__/autopilot.test.d.ts.map +1 -0
- package/dist/__tests__/autopilot.test.js +156 -0
- package/dist/__tests__/tier-config.test.d.ts +9 -0
- package/dist/__tests__/tier-config.test.d.ts.map +1 -0
- package/dist/__tests__/tier-config.test.js +230 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts +2 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash-inline.test.js +62 -0
- package/dist/__tests__/utils/hash.test.d.ts +3 -0
- package/dist/__tests__/utils/hash.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash.test.js +95 -0
- package/dist/__tests__/utils/simple.test.d.ts +1 -0
- package/dist/__tests__/utils/simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/simple.test.js +10 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts +1 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils-simple.test.js +6 -0
- package/dist/__tests__/utils/utils.test.d.ts +15 -0
- package/dist/__tests__/utils/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils.test.js +172 -0
- package/dist/autopilot/autopilot-runner.d.ts +33 -0
- package/dist/autopilot/autopilot-runner.d.ts.map +1 -0
- package/dist/autopilot/autopilot-runner.js +479 -0
- package/dist/autopilot/index.d.ts +6 -0
- package/dist/autopilot/index.d.ts.map +1 -0
- package/dist/autopilot/index.js +25 -0
- package/dist/autopilot/types.d.ts +102 -0
- package/dist/autopilot/types.d.ts.map +1 -0
- package/dist/autopilot/types.js +18 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +22 -0
- package/dist/cache/redis-cache.d.ts +145 -0
- package/dist/cache/redis-cache.d.ts.map +1 -0
- package/dist/cache/redis-cache.js +459 -0
- package/dist/ci/github-actions.d.ts +77 -0
- package/dist/ci/github-actions.d.ts.map +1 -0
- package/dist/ci/github-actions.js +277 -0
- package/dist/ci/index.d.ts +12 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +27 -0
- package/dist/ci/pre-commit.d.ts +65 -0
- package/dist/ci/pre-commit.d.ts.map +1 -0
- package/dist/ci/pre-commit.js +286 -0
- package/dist/entitlements.d.ts +149 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +464 -0
- package/dist/env.d.ts +113 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +204 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts +7 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts.map +1 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.js +250 -0
- package/dist/fix-packs/generate-fix-packs.d.ts +15 -0
- package/dist/fix-packs/generate-fix-packs.d.ts.map +1 -0
- package/dist/fix-packs/generate-fix-packs.js +505 -0
- package/dist/fix-packs/index.d.ts +8 -0
- package/dist/fix-packs/index.d.ts.map +1 -0
- package/dist/fix-packs/index.js +23 -0
- package/dist/fix-packs/types.d.ts +113 -0
- package/dist/fix-packs/types.d.ts.map +1 -0
- package/dist/fix-packs/types.js +71 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/metrics/prometheus.d.ts +99 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +306 -0
- package/dist/quota-ledger.d.ts +119 -0
- package/dist/quota-ledger.d.ts.map +1 -0
- package/dist/quota-ledger.js +462 -0
- package/dist/rbac/__tests__/permissions.test.d.ts +8 -0
- package/dist/rbac/__tests__/permissions.test.d.ts.map +1 -0
- package/dist/rbac/__tests__/permissions.test.js +350 -0
- package/dist/rbac/index.d.ts +9 -0
- package/dist/rbac/index.d.ts.map +1 -0
- package/dist/rbac/index.js +32 -0
- package/dist/rbac/permissions.d.ts +71 -0
- package/dist/rbac/permissions.d.ts.map +1 -0
- package/dist/rbac/permissions.js +247 -0
- package/dist/rbac/types.d.ts +69 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +213 -0
- package/dist/tier-config.d.ts +203 -0
- package/dist/tier-config.d.ts.map +1 -0
- package/dist/tier-config.js +675 -0
- package/dist/types.d.ts +365 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +36 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +127 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/format-validator.test.js +285 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/pipeline.test.js +389 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.js +236 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/workspace.test.js +314 -0
- package/dist/verified-autofix/format-validator.d.ts +101 -0
- package/dist/verified-autofix/format-validator.d.ts.map +1 -0
- package/dist/verified-autofix/format-validator.js +446 -0
- package/dist/verified-autofix/index.d.ts +14 -0
- package/dist/verified-autofix/index.d.ts.map +1 -0
- package/dist/verified-autofix/index.js +39 -0
- package/dist/verified-autofix/pipeline.d.ts +68 -0
- package/dist/verified-autofix/pipeline.d.ts.map +1 -0
- package/dist/verified-autofix/pipeline.js +330 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts +56 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts.map +1 -0
- package/dist/verified-autofix/repo-fingerprint.js +396 -0
- package/dist/verified-autofix/workspace.d.ts +83 -0
- package/dist/verified-autofix/workspace.d.ts.map +1 -0
- package/dist/verified-autofix/workspace.js +454 -0
- package/dist/verified-autofix.d.ts +182 -0
- package/dist/verified-autofix.d.ts.map +1 -0
- package/dist/verified-autofix.js +1021 -0
- package/dist/visualization/dependency-graph.d.ts +79 -0
- package/dist/visualization/dependency-graph.d.ts.map +1 -0
- package/dist/visualization/dependency-graph.js +399 -0
- package/dist/visualization/index.d.ts +5 -0
- package/dist/visualization/index.d.ts.map +1 -0
- package/dist/visualization/index.js +20 -0
- package/package.json +29 -0
- package/src/__tests__/autopilot.test.ts +196 -0
- package/src/__tests__/tier-config.test.ts +289 -0
- package/src/__tests__/utils/hash-inline.test.ts +76 -0
- package/src/__tests__/utils/hash.test.ts +119 -0
- package/src/__tests__/utils/simple.test.ts +10 -0
- package/src/__tests__/utils/utils-simple.test.ts +5 -0
- package/src/__tests__/utils/utils.test.ts +203 -0
- package/src/autopilot/autopilot-runner.ts +503 -0
- package/src/autopilot/index.ts +6 -0
- package/src/autopilot/types.ts +119 -0
- package/src/cache/index.ts +7 -0
- package/src/cache/redis-cache.d.ts +155 -0
- package/src/cache/redis-cache.d.ts.map +1 -0
- package/src/cache/redis-cache.ts +517 -0
- package/src/ci/github-actions.ts +335 -0
- package/src/ci/index.ts +12 -0
- package/src/ci/pre-commit.ts +338 -0
- package/src/db/usage-schema.prisma +114 -0
- package/src/entitlements.ts +570 -0
- package/src/env.d.ts +68 -0
- package/src/env.d.ts.map +1 -0
- package/src/env.ts +247 -0
- package/src/fix-packs/__tests__/generate-fix-packs.test.ts +317 -0
- package/src/fix-packs/generate-fix-packs.ts +577 -0
- package/src/fix-packs/index.ts +8 -0
- package/src/fix-packs/types.ts +206 -0
- package/src/index.d.ts +7 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +12 -0
- package/src/metrics/prometheus.d.ts +104 -0
- package/src/metrics/prometheus.d.ts.map +1 -0
- package/src/metrics/prometheus.ts +446 -0
- package/src/quota-ledger.ts +548 -0
- package/src/rbac/__tests__/permissions.test.ts +446 -0
- package/src/rbac/index.ts +46 -0
- package/src/rbac/permissions.ts +301 -0
- package/src/rbac/types.ts +298 -0
- package/src/tier-config.json +157 -0
- package/src/tier-config.ts +815 -0
- package/src/types.d.ts +365 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.ts +441 -0
- package/src/utils.d.ts +36 -0
- package/src/utils.d.ts.map +1 -0
- package/src/utils.ts +140 -0
- package/src/verified-autofix/__tests__/format-validator.test.ts +335 -0
- package/src/verified-autofix/__tests__/pipeline.test.ts +419 -0
- package/src/verified-autofix/__tests__/repo-fingerprint.test.ts +241 -0
- package/src/verified-autofix/__tests__/workspace.test.ts +373 -0
- package/src/verified-autofix/format-validator.ts +517 -0
- package/src/verified-autofix/index.ts +63 -0
- package/src/verified-autofix/pipeline.ts +403 -0
- package/src/verified-autofix/repo-fingerprint.ts +459 -0
- package/src/verified-autofix/workspace.ts +531 -0
- package/src/verified-autofix.ts +1187 -0
- package/src/visualization/dependency-graph.d.ts +85 -0
- package/src/visualization/dependency-graph.d.ts.map +1 -0
- package/src/visualization/dependency-graph.ts +495 -0
- package/src/visualization/index.ts +5 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Quota Ledger - Server-Authoritative Usage Tracking
|
|
4
|
+
*
|
|
5
|
+
* Implements idempotent usage recording with request IDs to prevent double-counting.
|
|
6
|
+
* All quota checks are validated server-side before allowing operations.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY: CLI cannot bypass quota checks by modifying local files.
|
|
9
|
+
* The server is the single source of truth for usage data.
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.getPendingCount = exports.syncPendingUsage = exports.getUsageSummary = exports.recordUsage = exports.checkQuota = exports.quotaLedger = exports.QuotaLedger = void 0;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const crypto = __importStar(require("crypto"));
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// QUOTA LEDGER CLASS
|
|
52
|
+
// ============================================================================
|
|
53
|
+
class QuotaLedger {
|
|
54
|
+
configDir;
|
|
55
|
+
ledgerFile;
|
|
56
|
+
cacheFile;
|
|
57
|
+
apiUrl;
|
|
58
|
+
apiKey;
|
|
59
|
+
constructor() {
|
|
60
|
+
this.configDir = path.join(os.homedir(), '.guardrail');
|
|
61
|
+
this.ledgerFile = path.join(this.configDir, 'usage-ledger.json');
|
|
62
|
+
this.cacheFile = path.join(this.configDir, 'quota-cache.json');
|
|
63
|
+
this.apiUrl = process.env['GUARDRAIL_API_URL'] || 'https://api.getguardrail.io';
|
|
64
|
+
this.apiKey = process.env['GUARDRAIL_API_KEY'] || null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generate unique request ID for idempotency
|
|
68
|
+
*/
|
|
69
|
+
generateRequestId() {
|
|
70
|
+
const timestamp = Date.now().toString(36);
|
|
71
|
+
const random = crypto.randomBytes(8).toString('hex');
|
|
72
|
+
return `req_${timestamp}_${random}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if action is allowed based on quota
|
|
76
|
+
* Always validates with server when possible
|
|
77
|
+
*/
|
|
78
|
+
async checkQuota(action) {
|
|
79
|
+
const requestId = this.generateRequestId();
|
|
80
|
+
// Try server-authoritative check first
|
|
81
|
+
if (this.apiKey) {
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(`${this.apiUrl}/api/usage/check`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: {
|
|
86
|
+
'Content-Type': 'application/json',
|
|
87
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
88
|
+
'X-Request-ID': requestId,
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({ action }),
|
|
91
|
+
});
|
|
92
|
+
if (response.ok) {
|
|
93
|
+
const result = await response.json();
|
|
94
|
+
// Cache the result
|
|
95
|
+
await this.cacheQuotaResult(action, result);
|
|
96
|
+
return {
|
|
97
|
+
...result,
|
|
98
|
+
source: 'server',
|
|
99
|
+
requestId,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Server unreachable, fall back to cache/offline
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Fallback to cached data
|
|
108
|
+
const cached = await this.getCachedQuota(action);
|
|
109
|
+
if (cached) {
|
|
110
|
+
return {
|
|
111
|
+
...cached,
|
|
112
|
+
source: 'cache',
|
|
113
|
+
requestId,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Offline mode - allow with warning
|
|
117
|
+
return {
|
|
118
|
+
allowed: true,
|
|
119
|
+
current: 0,
|
|
120
|
+
limit: -1,
|
|
121
|
+
remaining: -1,
|
|
122
|
+
reason: 'Offline mode - usage will be synced when online',
|
|
123
|
+
source: 'offline',
|
|
124
|
+
requestId,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Record usage with idempotency guarantee
|
|
129
|
+
* Uses request ID to prevent double-counting on retries
|
|
130
|
+
*/
|
|
131
|
+
async recordUsage(action, count = 1, requestId) {
|
|
132
|
+
const id = requestId || this.generateRequestId();
|
|
133
|
+
// Check for duplicate request
|
|
134
|
+
if (await this.isDuplicateRequest(id)) {
|
|
135
|
+
const existing = await this.getExistingResult(id);
|
|
136
|
+
if (existing)
|
|
137
|
+
return existing;
|
|
138
|
+
}
|
|
139
|
+
const entry = {
|
|
140
|
+
id,
|
|
141
|
+
action,
|
|
142
|
+
count,
|
|
143
|
+
timestamp: new Date().toISOString(),
|
|
144
|
+
synced: false,
|
|
145
|
+
};
|
|
146
|
+
// Try server-authoritative recording
|
|
147
|
+
if (this.apiKey) {
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(`${this.apiUrl}/api/usage/record`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
154
|
+
'X-Request-ID': id,
|
|
155
|
+
'X-Idempotency-Key': id,
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify({ action, count, requestId: id }),
|
|
158
|
+
});
|
|
159
|
+
if (response.ok) {
|
|
160
|
+
const result = await response.json();
|
|
161
|
+
entry.synced = true;
|
|
162
|
+
await this.saveLedgerEntry(entry);
|
|
163
|
+
return {
|
|
164
|
+
success: true,
|
|
165
|
+
requestId: id,
|
|
166
|
+
newUsage: result.newUsage,
|
|
167
|
+
remaining: result.remaining,
|
|
168
|
+
source: 'server',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// Handle rate limit or quota exceeded
|
|
172
|
+
if (response.status === 429 || response.status === 402) {
|
|
173
|
+
const error = await response.json();
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
requestId: id,
|
|
177
|
+
newUsage: 0,
|
|
178
|
+
remaining: 0,
|
|
179
|
+
source: 'server',
|
|
180
|
+
error: error.message || 'Quota exceeded',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Server unreachable, queue for later sync
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Queue for later sync
|
|
189
|
+
await this.saveLedgerEntry(entry);
|
|
190
|
+
return {
|
|
191
|
+
success: true,
|
|
192
|
+
requestId: id,
|
|
193
|
+
newUsage: count,
|
|
194
|
+
remaining: -1,
|
|
195
|
+
source: 'queued',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get usage summary for current billing period
|
|
200
|
+
*/
|
|
201
|
+
async getUsageSummary() {
|
|
202
|
+
if (this.apiKey) {
|
|
203
|
+
try {
|
|
204
|
+
const response = await fetch(`${this.apiUrl}/api/usage/summary`, {
|
|
205
|
+
method: 'GET',
|
|
206
|
+
headers: {
|
|
207
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
if (response.ok) {
|
|
211
|
+
return await response.json();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Fall through to local
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Return local summary
|
|
219
|
+
return this.getLocalSummary();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Sync pending offline usage to server
|
|
223
|
+
*/
|
|
224
|
+
async syncPendingUsage() {
|
|
225
|
+
const entries = await this.getUnsyncedEntries();
|
|
226
|
+
let synced = 0;
|
|
227
|
+
let failed = 0;
|
|
228
|
+
const errors = [];
|
|
229
|
+
if (!this.apiKey || entries.length === 0) {
|
|
230
|
+
return { synced: 0, failed: 0, errors: [] };
|
|
231
|
+
}
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
try {
|
|
234
|
+
const response = await fetch(`${this.apiUrl}/api/usage/record`, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: {
|
|
237
|
+
'Content-Type': 'application/json',
|
|
238
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
239
|
+
'X-Request-ID': entry.id,
|
|
240
|
+
'X-Idempotency-Key': entry.id,
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify({
|
|
243
|
+
action: entry.action,
|
|
244
|
+
count: entry.count,
|
|
245
|
+
requestId: entry.id,
|
|
246
|
+
timestamp: entry.timestamp,
|
|
247
|
+
}),
|
|
248
|
+
});
|
|
249
|
+
if (response.ok) {
|
|
250
|
+
entry.synced = true;
|
|
251
|
+
await this.updateLedgerEntry(entry);
|
|
252
|
+
synced++;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
failed++;
|
|
256
|
+
errors.push(`Failed to sync ${entry.id}: ${response.statusText}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
failed++;
|
|
261
|
+
errors.push(`Failed to sync ${entry.id}: ${e.message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { synced, failed, errors };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get count of pending offline usage
|
|
268
|
+
*/
|
|
269
|
+
async getPendingCount() {
|
|
270
|
+
const entries = await this.getUnsyncedEntries();
|
|
271
|
+
return entries.length;
|
|
272
|
+
}
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// PRIVATE HELPERS
|
|
275
|
+
// ============================================================================
|
|
276
|
+
async ensureDir() {
|
|
277
|
+
try {
|
|
278
|
+
await fs.promises.mkdir(this.configDir, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Exists
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async saveLedgerEntry(entry) {
|
|
285
|
+
await this.ensureDir();
|
|
286
|
+
let ledger = [];
|
|
287
|
+
try {
|
|
288
|
+
const content = await fs.promises.readFile(this.ledgerFile, 'utf8');
|
|
289
|
+
ledger = JSON.parse(content);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// New file
|
|
293
|
+
}
|
|
294
|
+
// Check for duplicate
|
|
295
|
+
const existing = ledger.findIndex(e => e.id === entry.id);
|
|
296
|
+
if (existing >= 0) {
|
|
297
|
+
ledger[existing] = entry;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
ledger.push(entry);
|
|
301
|
+
}
|
|
302
|
+
// Keep only last 1000 entries
|
|
303
|
+
if (ledger.length > 1000) {
|
|
304
|
+
ledger = ledger.slice(-1000);
|
|
305
|
+
}
|
|
306
|
+
await fs.promises.writeFile(this.ledgerFile, JSON.stringify(ledger, null, 2));
|
|
307
|
+
}
|
|
308
|
+
async updateLedgerEntry(entry) {
|
|
309
|
+
await this.saveLedgerEntry(entry);
|
|
310
|
+
}
|
|
311
|
+
async getUnsyncedEntries() {
|
|
312
|
+
try {
|
|
313
|
+
const content = await fs.promises.readFile(this.ledgerFile, 'utf8');
|
|
314
|
+
const ledger = JSON.parse(content);
|
|
315
|
+
return ledger.filter(e => !e.synced);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async isDuplicateRequest(id) {
|
|
322
|
+
try {
|
|
323
|
+
const content = await fs.promises.readFile(this.ledgerFile, 'utf8');
|
|
324
|
+
const ledger = JSON.parse(content);
|
|
325
|
+
return ledger.some(e => e.id === id);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async getExistingResult(id) {
|
|
332
|
+
try {
|
|
333
|
+
const content = await fs.promises.readFile(this.ledgerFile, 'utf8');
|
|
334
|
+
const ledger = JSON.parse(content);
|
|
335
|
+
const entry = ledger.find(e => e.id === id);
|
|
336
|
+
if (entry) {
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
requestId: entry.id,
|
|
340
|
+
newUsage: entry.count,
|
|
341
|
+
remaining: -1,
|
|
342
|
+
source: entry.synced ? 'server' : 'queued',
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Not found
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
async cacheQuotaResult(action, result) {
|
|
352
|
+
await this.ensureDir();
|
|
353
|
+
let cache = {};
|
|
354
|
+
try {
|
|
355
|
+
const content = await fs.promises.readFile(this.cacheFile, 'utf8');
|
|
356
|
+
cache = JSON.parse(content);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// New cache
|
|
360
|
+
}
|
|
361
|
+
cache[action] = {
|
|
362
|
+
...result,
|
|
363
|
+
cachedAt: new Date().toISOString(),
|
|
364
|
+
};
|
|
365
|
+
await fs.promises.writeFile(this.cacheFile, JSON.stringify(cache, null, 2));
|
|
366
|
+
}
|
|
367
|
+
async getCachedQuota(action) {
|
|
368
|
+
try {
|
|
369
|
+
const content = await fs.promises.readFile(this.cacheFile, 'utf8');
|
|
370
|
+
const cache = JSON.parse(content);
|
|
371
|
+
const cached = cache[action];
|
|
372
|
+
if (cached) {
|
|
373
|
+
// Cache valid for 5 minutes
|
|
374
|
+
const age = Date.now() - new Date(cached.cachedAt).getTime();
|
|
375
|
+
if (age < 5 * 60 * 1000) {
|
|
376
|
+
return {
|
|
377
|
+
allowed: cached.allowed,
|
|
378
|
+
current: cached.current,
|
|
379
|
+
limit: cached.limit,
|
|
380
|
+
remaining: cached.remaining,
|
|
381
|
+
source: 'cache',
|
|
382
|
+
requestId: this.generateRequestId(),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// No cache
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
async getLocalSummary() {
|
|
393
|
+
try {
|
|
394
|
+
const content = await fs.promises.readFile(this.ledgerFile, 'utf8');
|
|
395
|
+
const ledger = JSON.parse(content);
|
|
396
|
+
// Calculate current month's usage
|
|
397
|
+
const now = new Date();
|
|
398
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
399
|
+
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
400
|
+
const thisMonth = ledger.filter(e => {
|
|
401
|
+
const ts = new Date(e.timestamp);
|
|
402
|
+
return ts >= monthStart && ts <= monthEnd;
|
|
403
|
+
});
|
|
404
|
+
const usage = {
|
|
405
|
+
scans: 0,
|
|
406
|
+
reality: 0,
|
|
407
|
+
agent: 0,
|
|
408
|
+
fix: 0,
|
|
409
|
+
gate: 0,
|
|
410
|
+
};
|
|
411
|
+
for (const entry of thisMonth) {
|
|
412
|
+
switch (entry.action) {
|
|
413
|
+
case 'scan':
|
|
414
|
+
case 'scan_truth':
|
|
415
|
+
usage.scans += entry.count;
|
|
416
|
+
break;
|
|
417
|
+
case 'reality':
|
|
418
|
+
usage.reality += entry.count;
|
|
419
|
+
break;
|
|
420
|
+
case 'agent':
|
|
421
|
+
usage.agent += entry.count;
|
|
422
|
+
break;
|
|
423
|
+
case 'fix':
|
|
424
|
+
usage.fix += entry.count;
|
|
425
|
+
break;
|
|
426
|
+
case 'gate':
|
|
427
|
+
usage.gate += entry.count;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
tier: 'unknown',
|
|
433
|
+
period: {
|
|
434
|
+
start: monthStart.toISOString(),
|
|
435
|
+
end: monthEnd.toISOString(),
|
|
436
|
+
},
|
|
437
|
+
usage,
|
|
438
|
+
limits: { scans: -1, reality: -1, agent: -1 },
|
|
439
|
+
remaining: { scans: -1, reality: -1, agent: -1 },
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
exports.QuotaLedger = QuotaLedger;
|
|
448
|
+
// ============================================================================
|
|
449
|
+
// SINGLETON EXPORT
|
|
450
|
+
// ============================================================================
|
|
451
|
+
exports.quotaLedger = new QuotaLedger();
|
|
452
|
+
// Convenience exports
|
|
453
|
+
const checkQuota = (action) => exports.quotaLedger.checkQuota(action);
|
|
454
|
+
exports.checkQuota = checkQuota;
|
|
455
|
+
const recordUsage = (action, count, requestId) => exports.quotaLedger.recordUsage(action, count, requestId);
|
|
456
|
+
exports.recordUsage = recordUsage;
|
|
457
|
+
const getUsageSummary = () => exports.quotaLedger.getUsageSummary();
|
|
458
|
+
exports.getUsageSummary = getUsageSummary;
|
|
459
|
+
const syncPendingUsage = () => exports.quotaLedger.syncPendingUsage();
|
|
460
|
+
exports.syncPendingUsage = syncPendingUsage;
|
|
461
|
+
const getPendingCount = () => exports.quotaLedger.getPendingCount();
|
|
462
|
+
exports.getPendingCount = getPendingCount;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.test.d.ts","sourceRoot":"","sources":["../../../src/rbac/__tests__/permissions.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|