@ik-firewall/core 2.3.3 → 2.3.4

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.cjs CHANGED
@@ -50,17 +50,12 @@ module.exports = __toCommonJS(index_exports);
50
50
  // src/Registry.ts
51
51
  var fs = null;
52
52
  var path = null;
53
- if (typeof window === "undefined") {
54
- try {
55
- } catch (e) {
56
- }
57
- }
58
53
  var Registry = class _Registry {
59
54
  static instance;
60
55
  registryPath;
61
56
  usagePath;
57
+ hooks;
62
58
  hasWarnedNoFs = false;
63
- hasWarnedSaveError = false;
64
59
  constructor() {
65
60
  this.registryPath = "";
66
61
  this.usagePath = "";
@@ -72,12 +67,6 @@ var Registry = class _Registry {
72
67
  this.usagePath = path.join(process.cwd(), ".ik-adapter", "usage.json");
73
68
  this.ensureDirectory();
74
69
  } catch (e) {
75
- if (!this.hasWarnedNoFs) {
76
- console.warn("\n[IK_REGISTRY] \u26A0\uFE0F WARNING: Node.js environment detected but fs/path modules failed to load. Falling back to in-memory mode.");
77
- console.warn("[IK_REGISTRY] \u{1F4DD} INTEGRATOR NOTICE: Custom configurations (metaprompts, instances) will be LOST upon server restart or lambda spin-down.");
78
- console.warn("[IK_REGISTRY] \u{1F4A1} BEST PRACTICE: Host on a traditional VPS (Node/Docker) or migrate settings to your database if you need persistence on Serverless runtimes.\n");
79
- this.hasWarnedNoFs = true;
80
- }
81
70
  }
82
71
  }
83
72
  }
@@ -87,23 +76,31 @@ var Registry = class _Registry {
87
76
  }
88
77
  return _Registry.instance;
89
78
  }
79
+ setHooks(hooks) {
80
+ this.hooks = hooks;
81
+ }
90
82
  ensureDirectory() {
91
83
  if (!fs || !path || !this.registryPath) return;
92
84
  try {
93
85
  const dir = path.dirname(this.registryPath);
94
- if (!fs.existsSync(dir)) {
95
- fs.mkdirSync(dir, { recursive: true });
96
- }
86
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
97
87
  } catch (e) {
98
88
  }
99
89
  }
100
90
  /**
101
- * Loads all instance configurations from the local registry file.
91
+ * Loads instance configurations with Hook support
102
92
  */
103
93
  load() {
104
- if (!fs || !this.registryPath) return {};
105
- try {
106
- if (fs.existsSync(this.registryPath)) {
94
+ if (this.hooks?.loadState) {
95
+ try {
96
+ const hookData = this.hooks.loadState("ik_registry");
97
+ if (hookData) return JSON.parse(hookData);
98
+ } catch (e) {
99
+ console.warn("[IK_REGISTRY] Registry load via hook failed, falling back...");
100
+ }
101
+ }
102
+ if (fs && this.registryPath && fs.existsSync(this.registryPath)) {
103
+ try {
107
104
  const rawData = fs.readFileSync(this.registryPath, "utf8");
108
105
  try {
109
106
  const decoded = Buffer.from(rawData, "base64").toString("utf8");
@@ -111,69 +108,45 @@ var Registry = class _Registry {
111
108
  } catch (e) {
112
109
  return JSON.parse(rawData);
113
110
  }
111
+ } catch (error) {
112
+ console.error("[IK_REGISTRY] Local load error:", error);
114
113
  }
115
- } catch (error) {
116
- console.error("[IK_REGISTRY] Load error:", error);
117
114
  }
118
115
  return {};
119
116
  }
120
117
  save(data) {
121
- if (!fs || !this.registryPath) {
122
- if (!this.hasWarnedNoFs) {
123
- console.warn("\n[IK_REGISTRY] \u26A0\uFE0F WARNING: No File System (fs) detected. Running in in-memory mode.");
124
- console.warn("[IK_REGISTRY] \u{1F4DD} INTEGRATOR NOTICE: Custom configurations (metaprompts, instances) will be LOST upon server restart or lambda spin-down.");
125
- console.warn("[IK_REGISTRY] \u{1F4A1} BEST PRACTICE: If hosting on Vercel/Serverless or Edge runtimes, do not rely on local JSON for dynamic config changes.");
126
- console.warn("[IK_REGISTRY] \u{1F449} Consider migrating user-generated adapter settings to your database or host on a traditional VPS (Node/Docker) to ensure persistence.\n");
127
- this.hasWarnedNoFs = true;
128
- }
129
- return;
118
+ const jsonStr = JSON.stringify(data, null, 2);
119
+ if (this.hooks?.persistState) {
120
+ this.hooks.persistState("ik_registry", jsonStr);
130
121
  }
131
- try {
132
- this.ensureDirectory();
133
- const jsonStr = JSON.stringify(data, null, 2);
134
- const obfuscated = Buffer.from(jsonStr).toString("base64");
135
- fs.writeFileSync(this.registryPath, obfuscated, "utf8");
136
- } catch (error) {
137
- if (!this.hasWarnedSaveError) {
138
- console.error("\n[IK_REGISTRY] \u{1F6A8} ERROR saving registry.json. Your environment likely has a read-only filesystem (e.g. Vercel Serverless).");
139
- console.error("[IK_REGISTRY] \u{1F4DD} INTEGRATOR NOTICE: Serverless environments wipe or restrict file writes!");
140
- console.error("[IK_REGISTRY] \u{1F4A1} BEST PRACTICE: Host on a traditional VPS (Node/Docker) or intercept state changes via Events/Database to ensure persistence.");
141
- console.error("[IK_REGISTRY] Actual Error:", error.message, "\n");
142
- this.hasWarnedSaveError = true;
122
+ if (fs && this.registryPath) {
123
+ try {
124
+ this.ensureDirectory();
125
+ const obfuscated = Buffer.from(jsonStr).toString("base64");
126
+ fs.writeFileSync(this.registryPath, obfuscated, "utf8");
127
+ } catch (error) {
128
+ if (!this.hasWarnedNoFs) {
129
+ console.warn("[IK_REGISTRY] Local write failed (likely Read-Only FS). Relying on hooks/memory.");
130
+ this.hasWarnedNoFs = true;
131
+ }
143
132
  }
144
133
  }
145
134
  }
146
- /**
147
- * Updates a specific instance configuration.
148
- */
149
135
  updateInstance(instanceId, config) {
150
136
  const all = this.load();
151
- all[instanceId] = {
152
- ...all[instanceId] || {},
153
- ...config,
154
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
155
- };
156
- this.save(all);
157
- }
158
- /**
159
- * Deletes a specific instance configuration.
160
- */
161
- deleteInstance(instanceId) {
162
- const all = this.load();
163
- delete all[instanceId];
137
+ all[instanceId] = { ...all[instanceId] || {}, ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
164
138
  this.save(all);
165
139
  }
166
140
  getUsageFilePath() {
167
141
  return this.usagePath;
168
142
  }
169
143
  exists(filePath) {
170
- return fs && fs.existsSync(filePath);
144
+ return !!(fs && fs.existsSync(filePath));
171
145
  }
172
146
  loadUsage() {
173
147
  if (!fs || !this.usagePath || !fs.existsSync(this.usagePath)) return [];
174
148
  try {
175
- const raw = fs.readFileSync(this.usagePath, "utf8");
176
- return JSON.parse(raw);
149
+ return JSON.parse(fs.readFileSync(this.usagePath, "utf8"));
177
150
  } catch (e) {
178
151
  return [];
179
152
  }
@@ -183,15 +156,12 @@ var Registry = class _Registry {
183
156
  try {
184
157
  fs.writeFileSync(this.usagePath, JSON.stringify(data, null, 2), "utf8");
185
158
  } catch (e) {
186
- console.error("[IK_REGISTRY] Failed to save usage:", e);
187
159
  }
188
160
  }
189
161
  clearUsage() {
190
162
  if (!fs || !this.usagePath) return;
191
163
  try {
192
- if (fs.existsSync(this.usagePath)) {
193
- fs.unlinkSync(this.usagePath);
194
- }
164
+ if (fs.existsSync(this.usagePath)) fs.unlinkSync(this.usagePath);
195
165
  } catch (e) {
196
166
  }
197
167
  }
@@ -236,6 +206,9 @@ var ConfigManager = class {
236
206
  this.config = initialConfig;
237
207
  this.hooks = hooks;
238
208
  this.registry = Registry.getInstance();
209
+ if (hooks) {
210
+ this.registry.setHooks(hooks);
211
+ }
239
212
  this.loadFromRegistry();
240
213
  }
241
214
  getRegistry() {
@@ -243,6 +216,22 @@ var ConfigManager = class {
243
216
  }
244
217
  setHooks(hooks) {
245
218
  this.hooks = hooks;
219
+ this.registry.setHooks(hooks);
220
+ }
221
+ async hydrateFromHooks() {
222
+ if (this.hooks?.loadState) {
223
+ try {
224
+ const state = await this.hooks.loadState("ik_registry");
225
+ if (state) {
226
+ const parsed = JSON.parse(state);
227
+ const registry = this.registry;
228
+ registry.save(parsed);
229
+ this.loadFromRegistry();
230
+ }
231
+ } catch (e) {
232
+ console.warn("[IK_CONFIG] Hydration from hooks failed:", e);
233
+ }
234
+ }
246
235
  }
247
236
  loadFromRegistry() {
248
237
  const instanceConfigs = this.registry.load();
@@ -268,19 +257,16 @@ var ConfigManager = class {
268
257
  try {
269
258
  const baseUrl = this.config.centralReportEndpoint || "https://ik-firewall.vercel.app";
270
259
  const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/sync`;
271
- const instanceConfig2 = this.config.instanceConfigs?.[instanceId];
272
- const registrationEmail = instanceConfig2?.ownerEmail || this.config.ownerEmail;
273
- if ((!instanceConfig2?.licenseKey || instanceConfig2.licenseKey.startsWith("TRIAL-")) && registrationEmail) {
260
+ const registrationEmail = instanceConfig?.ownerEmail || this.config.ownerEmail;
261
+ if ((!instanceConfig?.licenseKey || instanceConfig.licenseKey.startsWith("TRIAL-")) && registrationEmail) {
274
262
  const success = await this.registerInstance(instanceId, registrationEmail);
275
263
  if (success) {
276
264
  this.hooks?.onStatus?.("\u2728 IK_SYNC: Zero-Config registration successful. License issued.");
277
265
  return;
278
- } else {
279
- this.hooks?.onStatus?.("\u26A0\uFE0F IK_SYNC: Auto-registration failed. Continuing with local trial.");
280
266
  }
281
267
  }
282
268
  this.hooks?.onStatus?.(`\u{1F4E1} IK_SYNC: Heartbeat to ${endpoint}...`);
283
- const response = await fetch(`${endpoint}?licenseKey=${instanceConfig2?.licenseKey || ""}&instanceId=${instanceId}&version=2.2.0`);
269
+ const response = await fetch(`${endpoint}?licenseKey=${instanceConfig?.licenseKey || ""}&instanceId=${instanceId}&version=2.3.0`);
284
270
  if (response.ok) {
285
271
  const data = await response.json();
286
272
  if (this.config.hmacSecret) {
@@ -300,9 +286,6 @@ var ConfigManager = class {
300
286
  } else {
301
287
  throw new Error(`Server responded with ${response.status}`);
302
288
  }
303
- if (!instanceConfig2?.firstUseDate) {
304
- this.setInstanceConfig(instanceId, { firstUseDate: now.toISOString() });
305
- }
306
289
  } catch (error) {
307
290
  console.error(`[IK_CONFIG] Remote sync failed (${error.message}). Falling open (Fail-Open mode).`);
308
291
  this.setInstanceConfig(instanceId, {
@@ -310,17 +293,6 @@ var ConfigManager = class {
310
293
  lastSyncDate: now.toISOString()
311
294
  });
312
295
  }
313
- try {
314
- const registry = this.registry;
315
- if (registry.exists(registry.getUsageFilePath())) {
316
- const aggregatedUsage = registry.loadUsage();
317
- if (aggregatedUsage.length > 0) {
318
- this.hooks?.onStatus?.(`\u{1F4CA} IK_SYNC: Found ${aggregatedUsage.length} pending usage reports. Preparation for Daily Flush...`);
319
- this._pendingUsage = aggregatedUsage;
320
- }
321
- }
322
- } catch (e) {
323
- }
324
296
  }
325
297
  getConfig() {
326
298
  return { ...this.config };
@@ -1478,9 +1450,13 @@ var Orchestrator = class {
1478
1450
 
1479
1451
  // src/UsageTracker.ts
1480
1452
  var UsageTracker = class {
1481
- buffer = [];
1482
- MAX_BUFFER_SIZE = 1;
1483
- // Immediate reporting for Edge/Stateless environments
1453
+ aggregationMap = /* @__PURE__ */ new Map();
1454
+ retryQueue = [];
1455
+ lastFlushTime = Date.now();
1456
+ flushInProgress = false;
1457
+ BATCH_SIZE_THRESHOLD = 50;
1458
+ TIME_THRESHOLD_MS = 2e3;
1459
+ MAX_RETRIES = 5;
1484
1460
  hooks;
1485
1461
  config;
1486
1462
  constructor(config, hooks) {
@@ -1488,113 +1464,116 @@ var UsageTracker = class {
1488
1464
  this.hooks = hooks;
1489
1465
  }
1490
1466
  /**
1491
- * Record a single AI interaction
1467
+ * Record a single AI interaction and aggregate in memory
1492
1468
  */
1493
1469
  async logInteraction(params) {
1494
- const { instanceId, model, inputTokens, outputTokens, optimizedTokens = 0, cqScore, routingPath, clientOrigin, trace } = params;
1470
+ const { instanceId, model, inputTokens, outputTokens, optimizedTokens = 0 } = params;
1495
1471
  const tokensSaved = optimizedTokens > 0 ? inputTokens - optimizedTokens : 0;
1496
- const instanceConfig = this.config.getConfig().instanceConfigs?.[instanceId];
1497
- const remotePricing = instanceConfig?.pricing?.[model] || { input: 5e-3, output: 0.015 };
1498
- const costSaved = tokensSaved / 1e3 * remotePricing.input;
1499
- const commissionDue = costSaved * 0.2;
1500
- const data = {
1472
+ const totalTokens = inputTokens + outputTokens;
1473
+ const day = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1474
+ const aggKey = `${instanceId}:${model}:${day}`;
1475
+ const existing = this.aggregationMap.get(aggKey) || {
1476
+ modelName: model,
1477
+ date: day,
1478
+ tokensIn: 0,
1479
+ tokensOut: 0,
1480
+ tokensSaved: 0
1481
+ };
1482
+ existing.tokensIn += inputTokens;
1483
+ existing.tokensOut += outputTokens;
1484
+ existing.tokensSaved += tokensSaved;
1485
+ this.aggregationMap.set(aggKey, existing);
1486
+ this.hooks?.onUsage?.({
1501
1487
  instanceId,
1502
1488
  model_used: model,
1503
- routingPath,
1489
+ routingPath: params.routingPath,
1504
1490
  input_tokens: inputTokens,
1505
1491
  output_tokens: outputTokens,
1506
1492
  optimized_tokens: optimizedTokens,
1507
1493
  tokens_saved: tokensSaved,
1508
- cost_saved: Number(costSaved.toFixed(6)),
1509
- commission_due: Number(commissionDue.toFixed(6)),
1494
+ cost_saved: 0,
1495
+ commission_due: 0,
1510
1496
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1511
1497
  is_local: model === "local",
1512
- cq_score: cqScore,
1513
- clientOrigin,
1514
- trace
1515
- };
1516
- this.buffer.push(data);
1517
- this.hooks?.onUsageReported?.(data);
1518
- await this.persistToLocalBuffer(data);
1498
+ cq_score: params.cqScore,
1499
+ clientOrigin: params.clientOrigin,
1500
+ trace: params.trace
1501
+ });
1502
+ this.hooks?.onUsageAggregate?.(existing);
1503
+ await this.checkFlushConditions(instanceId);
1504
+ }
1505
+ async checkFlushConditions(instanceId) {
1506
+ const now = Date.now();
1507
+ const timeSinceFlush = now - this.lastFlushTime;
1508
+ if (this.aggregationMap.size >= this.BATCH_SIZE_THRESHOLD || timeSinceFlush >= this.TIME_THRESHOLD_MS) {
1509
+ this.performAsyncFlush(instanceId).catch((err) => {
1510
+ console.error("[IK_TRACKER] Background flush failed:", err);
1511
+ });
1512
+ }
1519
1513
  }
1520
- /**
1521
- * Appends usage data to the local .ik-adapter/usage.json file
1522
- */
1523
- async persistToLocalBuffer(data) {
1514
+ async performAsyncFlush(instanceId) {
1515
+ if (this.flushInProgress || this.aggregationMap.size === 0) return;
1516
+ this.flushInProgress = true;
1517
+ const reports = Array.from(this.aggregationMap.values());
1518
+ this.aggregationMap.clear();
1519
+ this.lastFlushTime = Date.now();
1520
+ const licenseKey = this.config.getConfig().instanceConfigs?.[instanceId]?.licenseKey || this.config.get("licenseKey");
1521
+ const payload = {
1522
+ licenseKey: licenseKey || "TRIAL-KEY",
1523
+ instanceId,
1524
+ timestamp: Date.now(),
1525
+ reports
1526
+ };
1524
1527
  try {
1525
- const registry = this.config.getRegistry();
1526
- const usageFile = registry.getUsageFilePath();
1527
- let existingUsage = [];
1528
- if (registry.exists(usageFile)) {
1529
- existingUsage = registry.loadUsage();
1530
- }
1531
- existingUsage.push(data);
1532
- registry.saveUsage(existingUsage);
1533
- } catch (e) {
1534
- console.error("[IK_TRACKER] Failed to persist usage to local buffer:", e);
1528
+ await this.sendBatch(payload);
1529
+ } catch (error) {
1530
+ console.error("[IK_TRACKER] Batch send failed, queuing for retry:", error);
1531
+ this.handleFailure(payload);
1532
+ } finally {
1533
+ this.flushInProgress = false;
1535
1534
  }
1536
1535
  }
1537
- /**
1538
- * Send buffered usage data to the central IK billing service
1539
- */
1540
- async flush(aggregatedData) {
1541
- const usageToFlush = aggregatedData || this.buffer;
1542
- if (usageToFlush.length === 0) return;
1536
+ async sendBatch(payload) {
1543
1537
  const baseUrl = this.config.get("centralReportEndpoint") || "https://ik-firewall.vercel.app";
1544
1538
  const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/report`;
1545
- const hmacSecret = this.config.get("hmacSecret");
1546
- try {
1547
- this.hooks?.onStatus?.(`\u{1F4E4} IK_TRACKER: Flushing ${usageToFlush.length} interactions to ${endpoint}...`);
1548
- const reportsByInstance = {};
1549
- for (const report of usageToFlush) {
1550
- if (!reportsByInstance[report.instanceId]) reportsByInstance[report.instanceId] = [];
1551
- reportsByInstance[report.instanceId].push(report);
1552
- }
1553
- for (const [instanceId, reports] of Object.entries(reportsByInstance)) {
1554
- const instanceConfig = this.config.getConfig().instanceConfigs?.[instanceId];
1555
- const payload = {
1556
- licenseKey: instanceConfig?.licenseKey || "",
1557
- instanceId,
1558
- reports: reports.map((r) => ({
1559
- modelName: r.model_used,
1560
- tokensIn: r.input_tokens,
1561
- tokensOut: r.output_tokens,
1562
- tokensSaved: r.tokens_saved,
1563
- date: r.timestamp
1564
- }))
1565
- };
1566
- if (hmacSecret) {
1567
- const jsonStr = JSON.stringify(payload);
1568
- try {
1569
- const crypto = typeof window === "undefined" ? (await import(
1570
- /* webpackIgnore: true */
1571
- "crypto"
1572
- )).default : null;
1573
- if (crypto && crypto.createHmac) {
1574
- payload.signature = crypto.createHmac("sha256", hmacSecret).update(jsonStr).digest("hex");
1575
- }
1576
- } catch (e) {
1577
- }
1578
- }
1579
- const response = await fetch(endpoint, {
1580
- method: "POST",
1581
- headers: { "Content-Type": "application/json" },
1582
- body: JSON.stringify(payload)
1583
- });
1584
- if (!response.ok) {
1585
- console.error(`[IK_TRACKER] Failed to send aggregate for ${instanceId}: ${response.statusText}`);
1539
+ const hmacSecret = this.config.get("hmacSecret") || process.env.IK_FIREWALL_SECRET;
1540
+ if (hmacSecret) {
1541
+ try {
1542
+ const crypto = typeof window === "undefined" ? (await import("crypto")).default : null;
1543
+ if (crypto && crypto.createHmac) {
1544
+ payload.signature = crypto.createHmac("sha256", hmacSecret).update(JSON.stringify(payload)).digest("hex");
1586
1545
  }
1546
+ } catch (e) {
1587
1547
  }
1588
- this.buffer = [];
1589
- if (aggregatedData) {
1590
- this.config.getRegistry().clearUsage();
1548
+ }
1549
+ const response = await fetch(endpoint, {
1550
+ method: "POST",
1551
+ headers: {
1552
+ "Content-Type": "application/json",
1553
+ "x-ik-signature": payload.signature || ""
1554
+ },
1555
+ body: JSON.stringify(payload)
1556
+ });
1557
+ if (!response.ok) {
1558
+ throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
1559
+ }
1560
+ this.hooks?.onFlushSuccess?.(payload);
1561
+ }
1562
+ async handleFailure(payload) {
1563
+ this.retryQueue.push(payload);
1564
+ if (this.config.getRegistry() && typeof window === "undefined") {
1565
+ try {
1566
+ const registry = this.config.getRegistry();
1567
+ const retryFile = registry.getUsageFilePath().replace("usage.json", "usage.retry.jsonl");
1568
+ } catch (e) {
1591
1569
  }
1592
- } catch (error) {
1593
- console.error("[IK_TRACKER] Failed to flush usage data:", error);
1570
+ }
1571
+ if (this.hooks?.persistState) {
1572
+ await this.hooks.persistState(`ik_retry_${payload.timestamp}`, JSON.stringify(payload));
1594
1573
  }
1595
1574
  }
1596
1575
  getBuffer() {
1597
- return [...this.buffer];
1576
+ return Array.from(this.aggregationMap.values());
1598
1577
  }
1599
1578
  };
1600
1579
 
@@ -1869,20 +1848,20 @@ var IKFirewallCore = class _IKFirewallCore {
1869
1848
  async logConsumption(params) {
1870
1849
  return this.usageTracker.logInteraction(params);
1871
1850
  }
1851
+ hydrated = false;
1872
1852
  /**
1873
1853
  * Primary Analysis Entry Point
1874
1854
  */
1875
1855
  async analyze(input, provider, personaName = "professional", locale, instanceId) {
1856
+ if (!this.hydrated) {
1857
+ await this.configManager.hydrateFromHooks();
1858
+ this.hydrated = true;
1859
+ }
1876
1860
  if (instanceId) {
1877
1861
  this.configManager.ensureInstanceConfig(instanceId);
1878
- const syncPromise = this.configManager.syncRemoteConfig(instanceId).then(async () => {
1879
- const pendingUsage = this.configManager._pendingUsage;
1880
- if (pendingUsage && pendingUsage.length > 0) {
1881
- await this.usageTracker.flush(pendingUsage);
1882
- this.configManager._pendingUsage = null;
1883
- }
1862
+ this.configManager.syncRemoteConfig(instanceId).catch((err) => {
1863
+ console.warn("[IK_CORE] Heartbeat sync failed:", err);
1884
1864
  });
1885
- await syncPromise;
1886
1865
  }
1887
1866
  const mergedConfig = this.configManager.getMergedConfig(instanceId);
1888
1867
  if (instanceId && mergedConfig?.isVerified === false) {
package/dist/index.d.cts CHANGED
@@ -165,12 +165,31 @@ interface UsageData {
165
165
  clientOrigin?: string;
166
166
  trace?: any;
167
167
  }
168
+ interface UsageAggregate {
169
+ modelName: string;
170
+ date: string;
171
+ tokensIn: number;
172
+ tokensOut: number;
173
+ tokensSaved: number;
174
+ }
175
+ interface BatchPayload {
176
+ licenseKey: string;
177
+ instanceId: string;
178
+ timestamp: number;
179
+ reports: UsageAggregate[];
180
+ signature?: string;
181
+ }
168
182
  interface IKHooks {
169
183
  onDNAChange?: (dna: number) => void;
170
184
  onAuditComplete?: (metrics: IKMetrics) => void;
171
185
  onStatus?: (message: string) => void;
172
- onUsageReported?: (usage: UsageData) => void;
186
+ onLicenseIssued?: (license: string) => void;
187
+ onUsage?: (usage: UsageData) => void;
188
+ onUsageAggregate?: (aggregate: UsageAggregate) => void;
189
+ onFlushSuccess?: (payload: BatchPayload) => void;
173
190
  onAuthorityExceeded?: (metrics: IKMetrics, prompt: string) => void | Promise<void>;
191
+ persistState?: (key: string, value: string) => Promise<void> | void;
192
+ loadState?: (key: string) => Promise<string | null> | string | null;
174
193
  }
175
194
 
176
195
  declare abstract class BaseProvider {
@@ -356,6 +375,7 @@ declare class IKFirewallCore {
356
375
  clientOrigin?: string;
357
376
  trace?: any;
358
377
  }): Promise<void>;
378
+ private hydrated;
359
379
  /**
360
380
  * Primary Analysis Entry Point
361
381
  */
@@ -705,4 +725,4 @@ declare const IKBenchmarks: {
705
725
  BIG_TEST: TestPrompt[];
706
726
  };
707
727
 
708
- export { type AIProvider, type AIResponse, AnthropicProvider, BaseProvider, type ContextMode, DeepSeekProvider, Dictionaries, DictionaryRegex, GeminiProvider, HeuristicGatekeeper, IKBenchmarks, type IKConfig, IKFirewallCore, type IKHooks, type IKMetrics, type IKPluginInfo, LocalProvider, type ModelRole, OpenAIProvider, PerplexityProvider, type PromptLength, type PromptType, type ProviderMode, type RuntimeStrategy, type StressReport, type StressTestCase, StressTester, SurgicalTester, type TestCase, type TestPrompt, type TestResult, type UsageData };
728
+ export { type AIProvider, type AIResponse, AnthropicProvider, BaseProvider, type BatchPayload, type ContextMode, DeepSeekProvider, Dictionaries, DictionaryRegex, GeminiProvider, HeuristicGatekeeper, IKBenchmarks, type IKConfig, IKFirewallCore, type IKHooks, type IKMetrics, type IKPluginInfo, LocalProvider, type ModelRole, OpenAIProvider, PerplexityProvider, type PromptLength, type PromptType, type ProviderMode, type RuntimeStrategy, type StressReport, type StressTestCase, StressTester, SurgicalTester, type TestCase, type TestPrompt, type TestResult, type UsageAggregate, type UsageData };
package/dist/index.d.ts CHANGED
@@ -165,12 +165,31 @@ interface UsageData {
165
165
  clientOrigin?: string;
166
166
  trace?: any;
167
167
  }
168
+ interface UsageAggregate {
169
+ modelName: string;
170
+ date: string;
171
+ tokensIn: number;
172
+ tokensOut: number;
173
+ tokensSaved: number;
174
+ }
175
+ interface BatchPayload {
176
+ licenseKey: string;
177
+ instanceId: string;
178
+ timestamp: number;
179
+ reports: UsageAggregate[];
180
+ signature?: string;
181
+ }
168
182
  interface IKHooks {
169
183
  onDNAChange?: (dna: number) => void;
170
184
  onAuditComplete?: (metrics: IKMetrics) => void;
171
185
  onStatus?: (message: string) => void;
172
- onUsageReported?: (usage: UsageData) => void;
186
+ onLicenseIssued?: (license: string) => void;
187
+ onUsage?: (usage: UsageData) => void;
188
+ onUsageAggregate?: (aggregate: UsageAggregate) => void;
189
+ onFlushSuccess?: (payload: BatchPayload) => void;
173
190
  onAuthorityExceeded?: (metrics: IKMetrics, prompt: string) => void | Promise<void>;
191
+ persistState?: (key: string, value: string) => Promise<void> | void;
192
+ loadState?: (key: string) => Promise<string | null> | string | null;
174
193
  }
175
194
 
176
195
  declare abstract class BaseProvider {
@@ -356,6 +375,7 @@ declare class IKFirewallCore {
356
375
  clientOrigin?: string;
357
376
  trace?: any;
358
377
  }): Promise<void>;
378
+ private hydrated;
359
379
  /**
360
380
  * Primary Analysis Entry Point
361
381
  */
@@ -705,4 +725,4 @@ declare const IKBenchmarks: {
705
725
  BIG_TEST: TestPrompt[];
706
726
  };
707
727
 
708
- export { type AIProvider, type AIResponse, AnthropicProvider, BaseProvider, type ContextMode, DeepSeekProvider, Dictionaries, DictionaryRegex, GeminiProvider, HeuristicGatekeeper, IKBenchmarks, type IKConfig, IKFirewallCore, type IKHooks, type IKMetrics, type IKPluginInfo, LocalProvider, type ModelRole, OpenAIProvider, PerplexityProvider, type PromptLength, type PromptType, type ProviderMode, type RuntimeStrategy, type StressReport, type StressTestCase, StressTester, SurgicalTester, type TestCase, type TestPrompt, type TestResult, type UsageData };
728
+ export { type AIProvider, type AIResponse, AnthropicProvider, BaseProvider, type BatchPayload, type ContextMode, DeepSeekProvider, Dictionaries, DictionaryRegex, GeminiProvider, HeuristicGatekeeper, IKBenchmarks, type IKConfig, IKFirewallCore, type IKHooks, type IKMetrics, type IKPluginInfo, LocalProvider, type ModelRole, OpenAIProvider, PerplexityProvider, type PromptLength, type PromptType, type ProviderMode, type RuntimeStrategy, type StressReport, type StressTestCase, StressTester, SurgicalTester, type TestCase, type TestPrompt, type TestResult, type UsageAggregate, type UsageData };
package/dist/index.js CHANGED
@@ -8,17 +8,12 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
  // src/Registry.ts
9
9
  var fs = null;
10
10
  var path = null;
11
- if (typeof window === "undefined") {
12
- try {
13
- } catch (e) {
14
- }
15
- }
16
11
  var Registry = class _Registry {
17
12
  static instance;
18
13
  registryPath;
19
14
  usagePath;
15
+ hooks;
20
16
  hasWarnedNoFs = false;
21
- hasWarnedSaveError = false;
22
17
  constructor() {
23
18
  this.registryPath = "";
24
19
  this.usagePath = "";
@@ -30,12 +25,6 @@ var Registry = class _Registry {
30
25
  this.usagePath = path.join(process.cwd(), ".ik-adapter", "usage.json");
31
26
  this.ensureDirectory();
32
27
  } catch (e) {
33
- if (!this.hasWarnedNoFs) {
34
- console.warn("\n[IK_REGISTRY] \u26A0\uFE0F WARNING: Node.js environment detected but fs/path modules failed to load. Falling back to in-memory mode.");
35
- console.warn("[IK_REGISTRY] \u{1F4DD} INTEGRATOR NOTICE: Custom configurations (metaprompts, instances) will be LOST upon server restart or lambda spin-down.");
36
- console.warn("[IK_REGISTRY] \u{1F4A1} BEST PRACTICE: Host on a traditional VPS (Node/Docker) or migrate settings to your database if you need persistence on Serverless runtimes.\n");
37
- this.hasWarnedNoFs = true;
38
- }
39
28
  }
40
29
  }
41
30
  }
@@ -45,23 +34,31 @@ var Registry = class _Registry {
45
34
  }
46
35
  return _Registry.instance;
47
36
  }
37
+ setHooks(hooks) {
38
+ this.hooks = hooks;
39
+ }
48
40
  ensureDirectory() {
49
41
  if (!fs || !path || !this.registryPath) return;
50
42
  try {
51
43
  const dir = path.dirname(this.registryPath);
52
- if (!fs.existsSync(dir)) {
53
- fs.mkdirSync(dir, { recursive: true });
54
- }
44
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
55
45
  } catch (e) {
56
46
  }
57
47
  }
58
48
  /**
59
- * Loads all instance configurations from the local registry file.
49
+ * Loads instance configurations with Hook support
60
50
  */
61
51
  load() {
62
- if (!fs || !this.registryPath) return {};
63
- try {
64
- if (fs.existsSync(this.registryPath)) {
52
+ if (this.hooks?.loadState) {
53
+ try {
54
+ const hookData = this.hooks.loadState("ik_registry");
55
+ if (hookData) return JSON.parse(hookData);
56
+ } catch (e) {
57
+ console.warn("[IK_REGISTRY] Registry load via hook failed, falling back...");
58
+ }
59
+ }
60
+ if (fs && this.registryPath && fs.existsSync(this.registryPath)) {
61
+ try {
65
62
  const rawData = fs.readFileSync(this.registryPath, "utf8");
66
63
  try {
67
64
  const decoded = Buffer.from(rawData, "base64").toString("utf8");
@@ -69,69 +66,45 @@ var Registry = class _Registry {
69
66
  } catch (e) {
70
67
  return JSON.parse(rawData);
71
68
  }
69
+ } catch (error) {
70
+ console.error("[IK_REGISTRY] Local load error:", error);
72
71
  }
73
- } catch (error) {
74
- console.error("[IK_REGISTRY] Load error:", error);
75
72
  }
76
73
  return {};
77
74
  }
78
75
  save(data) {
79
- if (!fs || !this.registryPath) {
80
- if (!this.hasWarnedNoFs) {
81
- console.warn("\n[IK_REGISTRY] \u26A0\uFE0F WARNING: No File System (fs) detected. Running in in-memory mode.");
82
- console.warn("[IK_REGISTRY] \u{1F4DD} INTEGRATOR NOTICE: Custom configurations (metaprompts, instances) will be LOST upon server restart or lambda spin-down.");
83
- console.warn("[IK_REGISTRY] \u{1F4A1} BEST PRACTICE: If hosting on Vercel/Serverless or Edge runtimes, do not rely on local JSON for dynamic config changes.");
84
- console.warn("[IK_REGISTRY] \u{1F449} Consider migrating user-generated adapter settings to your database or host on a traditional VPS (Node/Docker) to ensure persistence.\n");
85
- this.hasWarnedNoFs = true;
86
- }
87
- return;
76
+ const jsonStr = JSON.stringify(data, null, 2);
77
+ if (this.hooks?.persistState) {
78
+ this.hooks.persistState("ik_registry", jsonStr);
88
79
  }
89
- try {
90
- this.ensureDirectory();
91
- const jsonStr = JSON.stringify(data, null, 2);
92
- const obfuscated = Buffer.from(jsonStr).toString("base64");
93
- fs.writeFileSync(this.registryPath, obfuscated, "utf8");
94
- } catch (error) {
95
- if (!this.hasWarnedSaveError) {
96
- console.error("\n[IK_REGISTRY] \u{1F6A8} ERROR saving registry.json. Your environment likely has a read-only filesystem (e.g. Vercel Serverless).");
97
- console.error("[IK_REGISTRY] \u{1F4DD} INTEGRATOR NOTICE: Serverless environments wipe or restrict file writes!");
98
- console.error("[IK_REGISTRY] \u{1F4A1} BEST PRACTICE: Host on a traditional VPS (Node/Docker) or intercept state changes via Events/Database to ensure persistence.");
99
- console.error("[IK_REGISTRY] Actual Error:", error.message, "\n");
100
- this.hasWarnedSaveError = true;
80
+ if (fs && this.registryPath) {
81
+ try {
82
+ this.ensureDirectory();
83
+ const obfuscated = Buffer.from(jsonStr).toString("base64");
84
+ fs.writeFileSync(this.registryPath, obfuscated, "utf8");
85
+ } catch (error) {
86
+ if (!this.hasWarnedNoFs) {
87
+ console.warn("[IK_REGISTRY] Local write failed (likely Read-Only FS). Relying on hooks/memory.");
88
+ this.hasWarnedNoFs = true;
89
+ }
101
90
  }
102
91
  }
103
92
  }
104
- /**
105
- * Updates a specific instance configuration.
106
- */
107
93
  updateInstance(instanceId, config) {
108
94
  const all = this.load();
109
- all[instanceId] = {
110
- ...all[instanceId] || {},
111
- ...config,
112
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
113
- };
114
- this.save(all);
115
- }
116
- /**
117
- * Deletes a specific instance configuration.
118
- */
119
- deleteInstance(instanceId) {
120
- const all = this.load();
121
- delete all[instanceId];
95
+ all[instanceId] = { ...all[instanceId] || {}, ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
122
96
  this.save(all);
123
97
  }
124
98
  getUsageFilePath() {
125
99
  return this.usagePath;
126
100
  }
127
101
  exists(filePath) {
128
- return fs && fs.existsSync(filePath);
102
+ return !!(fs && fs.existsSync(filePath));
129
103
  }
130
104
  loadUsage() {
131
105
  if (!fs || !this.usagePath || !fs.existsSync(this.usagePath)) return [];
132
106
  try {
133
- const raw = fs.readFileSync(this.usagePath, "utf8");
134
- return JSON.parse(raw);
107
+ return JSON.parse(fs.readFileSync(this.usagePath, "utf8"));
135
108
  } catch (e) {
136
109
  return [];
137
110
  }
@@ -141,15 +114,12 @@ var Registry = class _Registry {
141
114
  try {
142
115
  fs.writeFileSync(this.usagePath, JSON.stringify(data, null, 2), "utf8");
143
116
  } catch (e) {
144
- console.error("[IK_REGISTRY] Failed to save usage:", e);
145
117
  }
146
118
  }
147
119
  clearUsage() {
148
120
  if (!fs || !this.usagePath) return;
149
121
  try {
150
- if (fs.existsSync(this.usagePath)) {
151
- fs.unlinkSync(this.usagePath);
152
- }
122
+ if (fs.existsSync(this.usagePath)) fs.unlinkSync(this.usagePath);
153
123
  } catch (e) {
154
124
  }
155
125
  }
@@ -194,6 +164,9 @@ var ConfigManager = class {
194
164
  this.config = initialConfig;
195
165
  this.hooks = hooks;
196
166
  this.registry = Registry.getInstance();
167
+ if (hooks) {
168
+ this.registry.setHooks(hooks);
169
+ }
197
170
  this.loadFromRegistry();
198
171
  }
199
172
  getRegistry() {
@@ -201,6 +174,22 @@ var ConfigManager = class {
201
174
  }
202
175
  setHooks(hooks) {
203
176
  this.hooks = hooks;
177
+ this.registry.setHooks(hooks);
178
+ }
179
+ async hydrateFromHooks() {
180
+ if (this.hooks?.loadState) {
181
+ try {
182
+ const state = await this.hooks.loadState("ik_registry");
183
+ if (state) {
184
+ const parsed = JSON.parse(state);
185
+ const registry = this.registry;
186
+ registry.save(parsed);
187
+ this.loadFromRegistry();
188
+ }
189
+ } catch (e) {
190
+ console.warn("[IK_CONFIG] Hydration from hooks failed:", e);
191
+ }
192
+ }
204
193
  }
205
194
  loadFromRegistry() {
206
195
  const instanceConfigs = this.registry.load();
@@ -226,19 +215,16 @@ var ConfigManager = class {
226
215
  try {
227
216
  const baseUrl = this.config.centralReportEndpoint || "https://ik-firewall.vercel.app";
228
217
  const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/sync`;
229
- const instanceConfig2 = this.config.instanceConfigs?.[instanceId];
230
- const registrationEmail = instanceConfig2?.ownerEmail || this.config.ownerEmail;
231
- if ((!instanceConfig2?.licenseKey || instanceConfig2.licenseKey.startsWith("TRIAL-")) && registrationEmail) {
218
+ const registrationEmail = instanceConfig?.ownerEmail || this.config.ownerEmail;
219
+ if ((!instanceConfig?.licenseKey || instanceConfig.licenseKey.startsWith("TRIAL-")) && registrationEmail) {
232
220
  const success = await this.registerInstance(instanceId, registrationEmail);
233
221
  if (success) {
234
222
  this.hooks?.onStatus?.("\u2728 IK_SYNC: Zero-Config registration successful. License issued.");
235
223
  return;
236
- } else {
237
- this.hooks?.onStatus?.("\u26A0\uFE0F IK_SYNC: Auto-registration failed. Continuing with local trial.");
238
224
  }
239
225
  }
240
226
  this.hooks?.onStatus?.(`\u{1F4E1} IK_SYNC: Heartbeat to ${endpoint}...`);
241
- const response = await fetch(`${endpoint}?licenseKey=${instanceConfig2?.licenseKey || ""}&instanceId=${instanceId}&version=2.2.0`);
227
+ const response = await fetch(`${endpoint}?licenseKey=${instanceConfig?.licenseKey || ""}&instanceId=${instanceId}&version=2.3.0`);
242
228
  if (response.ok) {
243
229
  const data = await response.json();
244
230
  if (this.config.hmacSecret) {
@@ -258,9 +244,6 @@ var ConfigManager = class {
258
244
  } else {
259
245
  throw new Error(`Server responded with ${response.status}`);
260
246
  }
261
- if (!instanceConfig2?.firstUseDate) {
262
- this.setInstanceConfig(instanceId, { firstUseDate: now.toISOString() });
263
- }
264
247
  } catch (error) {
265
248
  console.error(`[IK_CONFIG] Remote sync failed (${error.message}). Falling open (Fail-Open mode).`);
266
249
  this.setInstanceConfig(instanceId, {
@@ -268,17 +251,6 @@ var ConfigManager = class {
268
251
  lastSyncDate: now.toISOString()
269
252
  });
270
253
  }
271
- try {
272
- const registry = this.registry;
273
- if (registry.exists(registry.getUsageFilePath())) {
274
- const aggregatedUsage = registry.loadUsage();
275
- if (aggregatedUsage.length > 0) {
276
- this.hooks?.onStatus?.(`\u{1F4CA} IK_SYNC: Found ${aggregatedUsage.length} pending usage reports. Preparation for Daily Flush...`);
277
- this._pendingUsage = aggregatedUsage;
278
- }
279
- }
280
- } catch (e) {
281
- }
282
254
  }
283
255
  getConfig() {
284
256
  return { ...this.config };
@@ -1436,9 +1408,13 @@ var Orchestrator = class {
1436
1408
 
1437
1409
  // src/UsageTracker.ts
1438
1410
  var UsageTracker = class {
1439
- buffer = [];
1440
- MAX_BUFFER_SIZE = 1;
1441
- // Immediate reporting for Edge/Stateless environments
1411
+ aggregationMap = /* @__PURE__ */ new Map();
1412
+ retryQueue = [];
1413
+ lastFlushTime = Date.now();
1414
+ flushInProgress = false;
1415
+ BATCH_SIZE_THRESHOLD = 50;
1416
+ TIME_THRESHOLD_MS = 2e3;
1417
+ MAX_RETRIES = 5;
1442
1418
  hooks;
1443
1419
  config;
1444
1420
  constructor(config, hooks) {
@@ -1446,113 +1422,116 @@ var UsageTracker = class {
1446
1422
  this.hooks = hooks;
1447
1423
  }
1448
1424
  /**
1449
- * Record a single AI interaction
1425
+ * Record a single AI interaction and aggregate in memory
1450
1426
  */
1451
1427
  async logInteraction(params) {
1452
- const { instanceId, model, inputTokens, outputTokens, optimizedTokens = 0, cqScore, routingPath, clientOrigin, trace } = params;
1428
+ const { instanceId, model, inputTokens, outputTokens, optimizedTokens = 0 } = params;
1453
1429
  const tokensSaved = optimizedTokens > 0 ? inputTokens - optimizedTokens : 0;
1454
- const instanceConfig = this.config.getConfig().instanceConfigs?.[instanceId];
1455
- const remotePricing = instanceConfig?.pricing?.[model] || { input: 5e-3, output: 0.015 };
1456
- const costSaved = tokensSaved / 1e3 * remotePricing.input;
1457
- const commissionDue = costSaved * 0.2;
1458
- const data = {
1430
+ const totalTokens = inputTokens + outputTokens;
1431
+ const day = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1432
+ const aggKey = `${instanceId}:${model}:${day}`;
1433
+ const existing = this.aggregationMap.get(aggKey) || {
1434
+ modelName: model,
1435
+ date: day,
1436
+ tokensIn: 0,
1437
+ tokensOut: 0,
1438
+ tokensSaved: 0
1439
+ };
1440
+ existing.tokensIn += inputTokens;
1441
+ existing.tokensOut += outputTokens;
1442
+ existing.tokensSaved += tokensSaved;
1443
+ this.aggregationMap.set(aggKey, existing);
1444
+ this.hooks?.onUsage?.({
1459
1445
  instanceId,
1460
1446
  model_used: model,
1461
- routingPath,
1447
+ routingPath: params.routingPath,
1462
1448
  input_tokens: inputTokens,
1463
1449
  output_tokens: outputTokens,
1464
1450
  optimized_tokens: optimizedTokens,
1465
1451
  tokens_saved: tokensSaved,
1466
- cost_saved: Number(costSaved.toFixed(6)),
1467
- commission_due: Number(commissionDue.toFixed(6)),
1452
+ cost_saved: 0,
1453
+ commission_due: 0,
1468
1454
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1469
1455
  is_local: model === "local",
1470
- cq_score: cqScore,
1471
- clientOrigin,
1472
- trace
1473
- };
1474
- this.buffer.push(data);
1475
- this.hooks?.onUsageReported?.(data);
1476
- await this.persistToLocalBuffer(data);
1456
+ cq_score: params.cqScore,
1457
+ clientOrigin: params.clientOrigin,
1458
+ trace: params.trace
1459
+ });
1460
+ this.hooks?.onUsageAggregate?.(existing);
1461
+ await this.checkFlushConditions(instanceId);
1462
+ }
1463
+ async checkFlushConditions(instanceId) {
1464
+ const now = Date.now();
1465
+ const timeSinceFlush = now - this.lastFlushTime;
1466
+ if (this.aggregationMap.size >= this.BATCH_SIZE_THRESHOLD || timeSinceFlush >= this.TIME_THRESHOLD_MS) {
1467
+ this.performAsyncFlush(instanceId).catch((err) => {
1468
+ console.error("[IK_TRACKER] Background flush failed:", err);
1469
+ });
1470
+ }
1477
1471
  }
1478
- /**
1479
- * Appends usage data to the local .ik-adapter/usage.json file
1480
- */
1481
- async persistToLocalBuffer(data) {
1472
+ async performAsyncFlush(instanceId) {
1473
+ if (this.flushInProgress || this.aggregationMap.size === 0) return;
1474
+ this.flushInProgress = true;
1475
+ const reports = Array.from(this.aggregationMap.values());
1476
+ this.aggregationMap.clear();
1477
+ this.lastFlushTime = Date.now();
1478
+ const licenseKey = this.config.getConfig().instanceConfigs?.[instanceId]?.licenseKey || this.config.get("licenseKey");
1479
+ const payload = {
1480
+ licenseKey: licenseKey || "TRIAL-KEY",
1481
+ instanceId,
1482
+ timestamp: Date.now(),
1483
+ reports
1484
+ };
1482
1485
  try {
1483
- const registry = this.config.getRegistry();
1484
- const usageFile = registry.getUsageFilePath();
1485
- let existingUsage = [];
1486
- if (registry.exists(usageFile)) {
1487
- existingUsage = registry.loadUsage();
1488
- }
1489
- existingUsage.push(data);
1490
- registry.saveUsage(existingUsage);
1491
- } catch (e) {
1492
- console.error("[IK_TRACKER] Failed to persist usage to local buffer:", e);
1486
+ await this.sendBatch(payload);
1487
+ } catch (error) {
1488
+ console.error("[IK_TRACKER] Batch send failed, queuing for retry:", error);
1489
+ this.handleFailure(payload);
1490
+ } finally {
1491
+ this.flushInProgress = false;
1493
1492
  }
1494
1493
  }
1495
- /**
1496
- * Send buffered usage data to the central IK billing service
1497
- */
1498
- async flush(aggregatedData) {
1499
- const usageToFlush = aggregatedData || this.buffer;
1500
- if (usageToFlush.length === 0) return;
1494
+ async sendBatch(payload) {
1501
1495
  const baseUrl = this.config.get("centralReportEndpoint") || "https://ik-firewall.vercel.app";
1502
1496
  const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/report`;
1503
- const hmacSecret = this.config.get("hmacSecret");
1504
- try {
1505
- this.hooks?.onStatus?.(`\u{1F4E4} IK_TRACKER: Flushing ${usageToFlush.length} interactions to ${endpoint}...`);
1506
- const reportsByInstance = {};
1507
- for (const report of usageToFlush) {
1508
- if (!reportsByInstance[report.instanceId]) reportsByInstance[report.instanceId] = [];
1509
- reportsByInstance[report.instanceId].push(report);
1510
- }
1511
- for (const [instanceId, reports] of Object.entries(reportsByInstance)) {
1512
- const instanceConfig = this.config.getConfig().instanceConfigs?.[instanceId];
1513
- const payload = {
1514
- licenseKey: instanceConfig?.licenseKey || "",
1515
- instanceId,
1516
- reports: reports.map((r) => ({
1517
- modelName: r.model_used,
1518
- tokensIn: r.input_tokens,
1519
- tokensOut: r.output_tokens,
1520
- tokensSaved: r.tokens_saved,
1521
- date: r.timestamp
1522
- }))
1523
- };
1524
- if (hmacSecret) {
1525
- const jsonStr = JSON.stringify(payload);
1526
- try {
1527
- const crypto = typeof window === "undefined" ? (await import(
1528
- /* webpackIgnore: true */
1529
- "crypto"
1530
- )).default : null;
1531
- if (crypto && crypto.createHmac) {
1532
- payload.signature = crypto.createHmac("sha256", hmacSecret).update(jsonStr).digest("hex");
1533
- }
1534
- } catch (e) {
1535
- }
1536
- }
1537
- const response = await fetch(endpoint, {
1538
- method: "POST",
1539
- headers: { "Content-Type": "application/json" },
1540
- body: JSON.stringify(payload)
1541
- });
1542
- if (!response.ok) {
1543
- console.error(`[IK_TRACKER] Failed to send aggregate for ${instanceId}: ${response.statusText}`);
1497
+ const hmacSecret = this.config.get("hmacSecret") || process.env.IK_FIREWALL_SECRET;
1498
+ if (hmacSecret) {
1499
+ try {
1500
+ const crypto = typeof window === "undefined" ? (await import("crypto")).default : null;
1501
+ if (crypto && crypto.createHmac) {
1502
+ payload.signature = crypto.createHmac("sha256", hmacSecret).update(JSON.stringify(payload)).digest("hex");
1544
1503
  }
1504
+ } catch (e) {
1545
1505
  }
1546
- this.buffer = [];
1547
- if (aggregatedData) {
1548
- this.config.getRegistry().clearUsage();
1506
+ }
1507
+ const response = await fetch(endpoint, {
1508
+ method: "POST",
1509
+ headers: {
1510
+ "Content-Type": "application/json",
1511
+ "x-ik-signature": payload.signature || ""
1512
+ },
1513
+ body: JSON.stringify(payload)
1514
+ });
1515
+ if (!response.ok) {
1516
+ throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
1517
+ }
1518
+ this.hooks?.onFlushSuccess?.(payload);
1519
+ }
1520
+ async handleFailure(payload) {
1521
+ this.retryQueue.push(payload);
1522
+ if (this.config.getRegistry() && typeof window === "undefined") {
1523
+ try {
1524
+ const registry = this.config.getRegistry();
1525
+ const retryFile = registry.getUsageFilePath().replace("usage.json", "usage.retry.jsonl");
1526
+ } catch (e) {
1549
1527
  }
1550
- } catch (error) {
1551
- console.error("[IK_TRACKER] Failed to flush usage data:", error);
1528
+ }
1529
+ if (this.hooks?.persistState) {
1530
+ await this.hooks.persistState(`ik_retry_${payload.timestamp}`, JSON.stringify(payload));
1552
1531
  }
1553
1532
  }
1554
1533
  getBuffer() {
1555
- return [...this.buffer];
1534
+ return Array.from(this.aggregationMap.values());
1556
1535
  }
1557
1536
  };
1558
1537
 
@@ -1827,20 +1806,20 @@ var IKFirewallCore = class _IKFirewallCore {
1827
1806
  async logConsumption(params) {
1828
1807
  return this.usageTracker.logInteraction(params);
1829
1808
  }
1809
+ hydrated = false;
1830
1810
  /**
1831
1811
  * Primary Analysis Entry Point
1832
1812
  */
1833
1813
  async analyze(input, provider, personaName = "professional", locale, instanceId) {
1814
+ if (!this.hydrated) {
1815
+ await this.configManager.hydrateFromHooks();
1816
+ this.hydrated = true;
1817
+ }
1834
1818
  if (instanceId) {
1835
1819
  this.configManager.ensureInstanceConfig(instanceId);
1836
- const syncPromise = this.configManager.syncRemoteConfig(instanceId).then(async () => {
1837
- const pendingUsage = this.configManager._pendingUsage;
1838
- if (pendingUsage && pendingUsage.length > 0) {
1839
- await this.usageTracker.flush(pendingUsage);
1840
- this.configManager._pendingUsage = null;
1841
- }
1820
+ this.configManager.syncRemoteConfig(instanceId).catch((err) => {
1821
+ console.warn("[IK_CORE] Heartbeat sync failed:", err);
1842
1822
  });
1843
- await syncPromise;
1844
1823
  }
1845
1824
  const mergedConfig = this.configManager.getMergedConfig(instanceId);
1846
1825
  if (instanceId && mergedConfig?.isVerified === false) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ik-firewall/core",
3
- "version": "2.3.3",
3
+ "version": "2.3.4",
4
4
  "type": "module",
5
5
  "description": "The core IK Firewall engine for semantic-driven AI optimization.",
6
6
  "main": "./dist/index.js",