@ik-firewall/core 2.3.2 → 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
  }
@@ -211,7 +181,13 @@ var IKHmac = class {
211
181
  const { signature, ...rest } = data;
212
182
  const payload = JSON.stringify(rest);
213
183
  try {
214
- const crypto = await import("crypto");
184
+ const crypto = typeof window === "undefined" ? (await import(
185
+ /* webpackIgnore: true */
186
+ "crypto"
187
+ )).default : null;
188
+ if (!crypto || !crypto.createHmac) {
189
+ return true;
190
+ }
215
191
  const expectedSignature = crypto.createHmac("sha256", this.secret).update(payload).digest("hex");
216
192
  return signature === expectedSignature;
217
193
  } catch (e) {
@@ -230,6 +206,9 @@ var ConfigManager = class {
230
206
  this.config = initialConfig;
231
207
  this.hooks = hooks;
232
208
  this.registry = Registry.getInstance();
209
+ if (hooks) {
210
+ this.registry.setHooks(hooks);
211
+ }
233
212
  this.loadFromRegistry();
234
213
  }
235
214
  getRegistry() {
@@ -237,6 +216,22 @@ var ConfigManager = class {
237
216
  }
238
217
  setHooks(hooks) {
239
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
+ }
240
235
  }
241
236
  loadFromRegistry() {
242
237
  const instanceConfigs = this.registry.load();
@@ -262,19 +257,16 @@ var ConfigManager = class {
262
257
  try {
263
258
  const baseUrl = this.config.centralReportEndpoint || "https://ik-firewall.vercel.app";
264
259
  const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/sync`;
265
- const instanceConfig2 = this.config.instanceConfigs?.[instanceId];
266
- const registrationEmail = instanceConfig2?.ownerEmail || this.config.ownerEmail;
267
- 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) {
268
262
  const success = await this.registerInstance(instanceId, registrationEmail);
269
263
  if (success) {
270
264
  this.hooks?.onStatus?.("\u2728 IK_SYNC: Zero-Config registration successful. License issued.");
271
265
  return;
272
- } else {
273
- this.hooks?.onStatus?.("\u26A0\uFE0F IK_SYNC: Auto-registration failed. Continuing with local trial.");
274
266
  }
275
267
  }
276
268
  this.hooks?.onStatus?.(`\u{1F4E1} IK_SYNC: Heartbeat to ${endpoint}...`);
277
- 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`);
278
270
  if (response.ok) {
279
271
  const data = await response.json();
280
272
  if (this.config.hmacSecret) {
@@ -294,9 +286,6 @@ var ConfigManager = class {
294
286
  } else {
295
287
  throw new Error(`Server responded with ${response.status}`);
296
288
  }
297
- if (!instanceConfig2?.firstUseDate) {
298
- this.setInstanceConfig(instanceId, { firstUseDate: now.toISOString() });
299
- }
300
289
  } catch (error) {
301
290
  console.error(`[IK_CONFIG] Remote sync failed (${error.message}). Falling open (Fail-Open mode).`);
302
291
  this.setInstanceConfig(instanceId, {
@@ -304,17 +293,6 @@ var ConfigManager = class {
304
293
  lastSyncDate: now.toISOString()
305
294
  });
306
295
  }
307
- try {
308
- const registry = this.registry;
309
- if (registry.exists(registry.getUsageFilePath())) {
310
- const aggregatedUsage = registry.loadUsage();
311
- if (aggregatedUsage.length > 0) {
312
- this.hooks?.onStatus?.(`\u{1F4CA} IK_SYNC: Found ${aggregatedUsage.length} pending usage reports. Preparation for Daily Flush...`);
313
- this._pendingUsage = aggregatedUsage;
314
- }
315
- }
316
- } catch (e) {
317
- }
318
296
  }
319
297
  getConfig() {
320
298
  return { ...this.config };
@@ -1472,9 +1450,13 @@ var Orchestrator = class {
1472
1450
 
1473
1451
  // src/UsageTracker.ts
1474
1452
  var UsageTracker = class {
1475
- buffer = [];
1476
- MAX_BUFFER_SIZE = 1;
1477
- // 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;
1478
1460
  hooks;
1479
1461
  config;
1480
1462
  constructor(config, hooks) {
@@ -1482,105 +1464,116 @@ var UsageTracker = class {
1482
1464
  this.hooks = hooks;
1483
1465
  }
1484
1466
  /**
1485
- * Record a single AI interaction
1467
+ * Record a single AI interaction and aggregate in memory
1486
1468
  */
1487
1469
  async logInteraction(params) {
1488
- const { instanceId, model, inputTokens, outputTokens, optimizedTokens = 0, cqScore, routingPath, clientOrigin, trace } = params;
1470
+ const { instanceId, model, inputTokens, outputTokens, optimizedTokens = 0 } = params;
1489
1471
  const tokensSaved = optimizedTokens > 0 ? inputTokens - optimizedTokens : 0;
1490
- const instanceConfig = this.config.getConfig().instanceConfigs?.[instanceId];
1491
- const remotePricing = instanceConfig?.pricing?.[model] || { input: 5e-3, output: 0.015 };
1492
- const costSaved = tokensSaved / 1e3 * remotePricing.input;
1493
- const commissionDue = costSaved * 0.2;
1494
- 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?.({
1495
1487
  instanceId,
1496
1488
  model_used: model,
1497
- routingPath,
1489
+ routingPath: params.routingPath,
1498
1490
  input_tokens: inputTokens,
1499
1491
  output_tokens: outputTokens,
1500
1492
  optimized_tokens: optimizedTokens,
1501
1493
  tokens_saved: tokensSaved,
1502
- cost_saved: Number(costSaved.toFixed(6)),
1503
- commission_due: Number(commissionDue.toFixed(6)),
1494
+ cost_saved: 0,
1495
+ commission_due: 0,
1504
1496
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1505
1497
  is_local: model === "local",
1506
- cq_score: cqScore,
1507
- clientOrigin,
1508
- trace
1509
- };
1510
- this.buffer.push(data);
1511
- this.hooks?.onUsageReported?.(data);
1512
- 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
+ }
1513
1513
  }
1514
- /**
1515
- * Appends usage data to the local .ik-adapter/usage.json file
1516
- */
1517
- 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
+ };
1518
1527
  try {
1519
- const registry = this.config.getRegistry();
1520
- const usageFile = registry.getUsageFilePath();
1521
- let existingUsage = [];
1522
- if (registry.exists(usageFile)) {
1523
- existingUsage = registry.loadUsage();
1524
- }
1525
- existingUsage.push(data);
1526
- registry.saveUsage(existingUsage);
1527
- } catch (e) {
1528
- 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;
1529
1534
  }
1530
1535
  }
1531
- /**
1532
- * Send buffered usage data to the central IK billing service
1533
- */
1534
- async flush(aggregatedData) {
1535
- const usageToFlush = aggregatedData || this.buffer;
1536
- if (usageToFlush.length === 0) return;
1536
+ async sendBatch(payload) {
1537
1537
  const baseUrl = this.config.get("centralReportEndpoint") || "https://ik-firewall.vercel.app";
1538
1538
  const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/report`;
1539
- const hmacSecret = this.config.get("hmacSecret");
1540
- try {
1541
- this.hooks?.onStatus?.(`\u{1F4E4} IK_TRACKER: Flushing ${usageToFlush.length} interactions to ${endpoint}...`);
1542
- const reportsByInstance = {};
1543
- for (const report of usageToFlush) {
1544
- if (!reportsByInstance[report.instanceId]) reportsByInstance[report.instanceId] = [];
1545
- reportsByInstance[report.instanceId].push(report);
1546
- }
1547
- for (const [instanceId, reports] of Object.entries(reportsByInstance)) {
1548
- const instanceConfig = this.config.getConfig().instanceConfigs?.[instanceId];
1549
- const payload = {
1550
- licenseKey: instanceConfig?.licenseKey || "",
1551
- instanceId,
1552
- reports: reports.map((r) => ({
1553
- modelName: r.model_used,
1554
- tokensIn: r.input_tokens,
1555
- tokensOut: r.output_tokens,
1556
- tokensSaved: r.tokens_saved,
1557
- date: r.timestamp
1558
- }))
1559
- };
1560
- if (hmacSecret) {
1561
- const jsonStr = JSON.stringify(payload);
1562
- const crypto = await import("crypto");
1563
- payload.signature = crypto.createHmac("sha256", hmacSecret).update(jsonStr).digest("hex");
1564
- }
1565
- const response = await fetch(endpoint, {
1566
- method: "POST",
1567
- headers: { "Content-Type": "application/json" },
1568
- body: JSON.stringify(payload)
1569
- });
1570
- if (!response.ok) {
1571
- 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");
1572
1545
  }
1546
+ } catch (e) {
1573
1547
  }
1574
- this.buffer = [];
1575
- if (aggregatedData) {
1576
- 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) {
1577
1569
  }
1578
- } catch (error) {
1579
- 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));
1580
1573
  }
1581
1574
  }
1582
1575
  getBuffer() {
1583
- return [...this.buffer];
1576
+ return Array.from(this.aggregationMap.values());
1584
1577
  }
1585
1578
  };
1586
1579
 
@@ -1855,20 +1848,20 @@ var IKFirewallCore = class _IKFirewallCore {
1855
1848
  async logConsumption(params) {
1856
1849
  return this.usageTracker.logInteraction(params);
1857
1850
  }
1851
+ hydrated = false;
1858
1852
  /**
1859
1853
  * Primary Analysis Entry Point
1860
1854
  */
1861
1855
  async analyze(input, provider, personaName = "professional", locale, instanceId) {
1856
+ if (!this.hydrated) {
1857
+ await this.configManager.hydrateFromHooks();
1858
+ this.hydrated = true;
1859
+ }
1862
1860
  if (instanceId) {
1863
1861
  this.configManager.ensureInstanceConfig(instanceId);
1864
- const syncPromise = this.configManager.syncRemoteConfig(instanceId).then(async () => {
1865
- const pendingUsage = this.configManager._pendingUsage;
1866
- if (pendingUsage && pendingUsage.length > 0) {
1867
- await this.usageTracker.flush(pendingUsage);
1868
- this.configManager._pendingUsage = null;
1869
- }
1862
+ this.configManager.syncRemoteConfig(instanceId).catch((err) => {
1863
+ console.warn("[IK_CORE] Heartbeat sync failed:", err);
1870
1864
  });
1871
- await syncPromise;
1872
1865
  }
1873
1866
  const mergedConfig = this.configManager.getMergedConfig(instanceId);
1874
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
  }
@@ -169,7 +139,13 @@ var IKHmac = class {
169
139
  const { signature, ...rest } = data;
170
140
  const payload = JSON.stringify(rest);
171
141
  try {
172
- const crypto = await import("crypto");
142
+ const crypto = typeof window === "undefined" ? (await import(
143
+ /* webpackIgnore: true */
144
+ "crypto"
145
+ )).default : null;
146
+ if (!crypto || !crypto.createHmac) {
147
+ return true;
148
+ }
173
149
  const expectedSignature = crypto.createHmac("sha256", this.secret).update(payload).digest("hex");
174
150
  return signature === expectedSignature;
175
151
  } catch (e) {
@@ -188,6 +164,9 @@ var ConfigManager = class {
188
164
  this.config = initialConfig;
189
165
  this.hooks = hooks;
190
166
  this.registry = Registry.getInstance();
167
+ if (hooks) {
168
+ this.registry.setHooks(hooks);
169
+ }
191
170
  this.loadFromRegistry();
192
171
  }
193
172
  getRegistry() {
@@ -195,6 +174,22 @@ var ConfigManager = class {
195
174
  }
196
175
  setHooks(hooks) {
197
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
+ }
198
193
  }
199
194
  loadFromRegistry() {
200
195
  const instanceConfigs = this.registry.load();
@@ -220,19 +215,16 @@ var ConfigManager = class {
220
215
  try {
221
216
  const baseUrl = this.config.centralReportEndpoint || "https://ik-firewall.vercel.app";
222
217
  const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/sync`;
223
- const instanceConfig2 = this.config.instanceConfigs?.[instanceId];
224
- const registrationEmail = instanceConfig2?.ownerEmail || this.config.ownerEmail;
225
- 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) {
226
220
  const success = await this.registerInstance(instanceId, registrationEmail);
227
221
  if (success) {
228
222
  this.hooks?.onStatus?.("\u2728 IK_SYNC: Zero-Config registration successful. License issued.");
229
223
  return;
230
- } else {
231
- this.hooks?.onStatus?.("\u26A0\uFE0F IK_SYNC: Auto-registration failed. Continuing with local trial.");
232
224
  }
233
225
  }
234
226
  this.hooks?.onStatus?.(`\u{1F4E1} IK_SYNC: Heartbeat to ${endpoint}...`);
235
- 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`);
236
228
  if (response.ok) {
237
229
  const data = await response.json();
238
230
  if (this.config.hmacSecret) {
@@ -252,9 +244,6 @@ var ConfigManager = class {
252
244
  } else {
253
245
  throw new Error(`Server responded with ${response.status}`);
254
246
  }
255
- if (!instanceConfig2?.firstUseDate) {
256
- this.setInstanceConfig(instanceId, { firstUseDate: now.toISOString() });
257
- }
258
247
  } catch (error) {
259
248
  console.error(`[IK_CONFIG] Remote sync failed (${error.message}). Falling open (Fail-Open mode).`);
260
249
  this.setInstanceConfig(instanceId, {
@@ -262,17 +251,6 @@ var ConfigManager = class {
262
251
  lastSyncDate: now.toISOString()
263
252
  });
264
253
  }
265
- try {
266
- const registry = this.registry;
267
- if (registry.exists(registry.getUsageFilePath())) {
268
- const aggregatedUsage = registry.loadUsage();
269
- if (aggregatedUsage.length > 0) {
270
- this.hooks?.onStatus?.(`\u{1F4CA} IK_SYNC: Found ${aggregatedUsage.length} pending usage reports. Preparation for Daily Flush...`);
271
- this._pendingUsage = aggregatedUsage;
272
- }
273
- }
274
- } catch (e) {
275
- }
276
254
  }
277
255
  getConfig() {
278
256
  return { ...this.config };
@@ -1430,9 +1408,13 @@ var Orchestrator = class {
1430
1408
 
1431
1409
  // src/UsageTracker.ts
1432
1410
  var UsageTracker = class {
1433
- buffer = [];
1434
- MAX_BUFFER_SIZE = 1;
1435
- // 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;
1436
1418
  hooks;
1437
1419
  config;
1438
1420
  constructor(config, hooks) {
@@ -1440,105 +1422,116 @@ var UsageTracker = class {
1440
1422
  this.hooks = hooks;
1441
1423
  }
1442
1424
  /**
1443
- * Record a single AI interaction
1425
+ * Record a single AI interaction and aggregate in memory
1444
1426
  */
1445
1427
  async logInteraction(params) {
1446
- const { instanceId, model, inputTokens, outputTokens, optimizedTokens = 0, cqScore, routingPath, clientOrigin, trace } = params;
1428
+ const { instanceId, model, inputTokens, outputTokens, optimizedTokens = 0 } = params;
1447
1429
  const tokensSaved = optimizedTokens > 0 ? inputTokens - optimizedTokens : 0;
1448
- const instanceConfig = this.config.getConfig().instanceConfigs?.[instanceId];
1449
- const remotePricing = instanceConfig?.pricing?.[model] || { input: 5e-3, output: 0.015 };
1450
- const costSaved = tokensSaved / 1e3 * remotePricing.input;
1451
- const commissionDue = costSaved * 0.2;
1452
- 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?.({
1453
1445
  instanceId,
1454
1446
  model_used: model,
1455
- routingPath,
1447
+ routingPath: params.routingPath,
1456
1448
  input_tokens: inputTokens,
1457
1449
  output_tokens: outputTokens,
1458
1450
  optimized_tokens: optimizedTokens,
1459
1451
  tokens_saved: tokensSaved,
1460
- cost_saved: Number(costSaved.toFixed(6)),
1461
- commission_due: Number(commissionDue.toFixed(6)),
1452
+ cost_saved: 0,
1453
+ commission_due: 0,
1462
1454
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1463
1455
  is_local: model === "local",
1464
- cq_score: cqScore,
1465
- clientOrigin,
1466
- trace
1467
- };
1468
- this.buffer.push(data);
1469
- this.hooks?.onUsageReported?.(data);
1470
- 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
+ }
1471
1471
  }
1472
- /**
1473
- * Appends usage data to the local .ik-adapter/usage.json file
1474
- */
1475
- 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
+ };
1476
1485
  try {
1477
- const registry = this.config.getRegistry();
1478
- const usageFile = registry.getUsageFilePath();
1479
- let existingUsage = [];
1480
- if (registry.exists(usageFile)) {
1481
- existingUsage = registry.loadUsage();
1482
- }
1483
- existingUsage.push(data);
1484
- registry.saveUsage(existingUsage);
1485
- } catch (e) {
1486
- 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;
1487
1492
  }
1488
1493
  }
1489
- /**
1490
- * Send buffered usage data to the central IK billing service
1491
- */
1492
- async flush(aggregatedData) {
1493
- const usageToFlush = aggregatedData || this.buffer;
1494
- if (usageToFlush.length === 0) return;
1494
+ async sendBatch(payload) {
1495
1495
  const baseUrl = this.config.get("centralReportEndpoint") || "https://ik-firewall.vercel.app";
1496
1496
  const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/report`;
1497
- const hmacSecret = this.config.get("hmacSecret");
1498
- try {
1499
- this.hooks?.onStatus?.(`\u{1F4E4} IK_TRACKER: Flushing ${usageToFlush.length} interactions to ${endpoint}...`);
1500
- const reportsByInstance = {};
1501
- for (const report of usageToFlush) {
1502
- if (!reportsByInstance[report.instanceId]) reportsByInstance[report.instanceId] = [];
1503
- reportsByInstance[report.instanceId].push(report);
1504
- }
1505
- for (const [instanceId, reports] of Object.entries(reportsByInstance)) {
1506
- const instanceConfig = this.config.getConfig().instanceConfigs?.[instanceId];
1507
- const payload = {
1508
- licenseKey: instanceConfig?.licenseKey || "",
1509
- instanceId,
1510
- reports: reports.map((r) => ({
1511
- modelName: r.model_used,
1512
- tokensIn: r.input_tokens,
1513
- tokensOut: r.output_tokens,
1514
- tokensSaved: r.tokens_saved,
1515
- date: r.timestamp
1516
- }))
1517
- };
1518
- if (hmacSecret) {
1519
- const jsonStr = JSON.stringify(payload);
1520
- const crypto = await import("crypto");
1521
- payload.signature = crypto.createHmac("sha256", hmacSecret).update(jsonStr).digest("hex");
1522
- }
1523
- const response = await fetch(endpoint, {
1524
- method: "POST",
1525
- headers: { "Content-Type": "application/json" },
1526
- body: JSON.stringify(payload)
1527
- });
1528
- if (!response.ok) {
1529
- 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");
1530
1503
  }
1504
+ } catch (e) {
1531
1505
  }
1532
- this.buffer = [];
1533
- if (aggregatedData) {
1534
- 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) {
1535
1527
  }
1536
- } catch (error) {
1537
- 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));
1538
1531
  }
1539
1532
  }
1540
1533
  getBuffer() {
1541
- return [...this.buffer];
1534
+ return Array.from(this.aggregationMap.values());
1542
1535
  }
1543
1536
  };
1544
1537
 
@@ -1813,20 +1806,20 @@ var IKFirewallCore = class _IKFirewallCore {
1813
1806
  async logConsumption(params) {
1814
1807
  return this.usageTracker.logInteraction(params);
1815
1808
  }
1809
+ hydrated = false;
1816
1810
  /**
1817
1811
  * Primary Analysis Entry Point
1818
1812
  */
1819
1813
  async analyze(input, provider, personaName = "professional", locale, instanceId) {
1814
+ if (!this.hydrated) {
1815
+ await this.configManager.hydrateFromHooks();
1816
+ this.hydrated = true;
1817
+ }
1820
1818
  if (instanceId) {
1821
1819
  this.configManager.ensureInstanceConfig(instanceId);
1822
- const syncPromise = this.configManager.syncRemoteConfig(instanceId).then(async () => {
1823
- const pendingUsage = this.configManager._pendingUsage;
1824
- if (pendingUsage && pendingUsage.length > 0) {
1825
- await this.usageTracker.flush(pendingUsage);
1826
- this.configManager._pendingUsage = null;
1827
- }
1820
+ this.configManager.syncRemoteConfig(instanceId).catch((err) => {
1821
+ console.warn("[IK_CORE] Heartbeat sync failed:", err);
1828
1822
  });
1829
- await syncPromise;
1830
1823
  }
1831
1824
  const mergedConfig = this.configManager.getMergedConfig(instanceId);
1832
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.2",
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",