@chromahq/core 1.0.46 → 1.0.48

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.
@@ -2,6 +2,311 @@
2
2
 
3
3
  var container$1 = require('@inversifyjs/container');
4
4
 
5
+ const NONCE_STORE_STORAGE_KEY = "__CHROMA_NONCE_STORE__";
6
+ class NonceService {
7
+ constructor() {
8
+ /** In-memory nonce store */
9
+ this.nonceStore = /* @__PURE__ */ new Map();
10
+ /** Default TTL for completed/failed entries (24 hours) */
11
+ this.DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
12
+ /** TTL for pending entries (5 minutes) */
13
+ this.PENDING_TTL_MS = 5 * 60 * 1e3;
14
+ /** Maximum entries to prevent unbounded growth */
15
+ this.MAX_ENTRIES = 500;
16
+ /** Interval handle for periodic cleanup */
17
+ this.cleanupInterval = null;
18
+ /** Whether store has been loaded from persistence */
19
+ this.loaded = false;
20
+ /** Promise for initial load operation */
21
+ this.loadPromise = null;
22
+ /** Debounce timer for persistence */
23
+ this.persistTimer = null;
24
+ /** In-flight persistence promise */
25
+ this.persistInFlight = null;
26
+ this.startCleanup();
27
+ void this.ensureLoaded();
28
+ }
29
+ /**
30
+ * Ensure nonce store has been loaded from storage.
31
+ * Safe to call multiple times.
32
+ */
33
+ async ensureLoaded() {
34
+ if (this.loaded) return;
35
+ if (this.loadPromise) return this.loadPromise;
36
+ this.loadPromise = (async () => {
37
+ this.loaded = true;
38
+ if (typeof chrome === "undefined" || !chrome.storage?.local?.get) {
39
+ return;
40
+ }
41
+ try {
42
+ const result = await chrome.storage.local.get(NONCE_STORE_STORAGE_KEY);
43
+ const raw = result?.[NONCE_STORE_STORAGE_KEY];
44
+ if (!raw || typeof raw !== "object") {
45
+ return;
46
+ }
47
+ const now = Date.now();
48
+ const entries = raw;
49
+ Object.entries(entries).forEach(([nonce, entry]) => {
50
+ if (!entry || typeof entry !== "object") return;
51
+ if (typeof entry.expiresAt !== "number" || now > entry.expiresAt) return;
52
+ if (entry.status !== "pending" && entry.status !== "completed" && entry.status !== "failed")
53
+ return;
54
+ this.nonceStore.set(nonce, entry);
55
+ });
56
+ } catch {
57
+ }
58
+ })();
59
+ return this.loadPromise;
60
+ }
61
+ /**
62
+ * Check if a nonce has already been processed
63
+ * @returns NonceCheckResult with cached data if exists
64
+ */
65
+ async checkNonce(nonce) {
66
+ await this.ensureLoaded();
67
+ const entry = this.nonceStore.get(nonce);
68
+ if (!entry) {
69
+ return { exists: false };
70
+ }
71
+ if (Date.now() > entry.expiresAt) {
72
+ this.nonceStore.delete(nonce);
73
+ return { exists: false };
74
+ }
75
+ return {
76
+ exists: true,
77
+ status: entry.status,
78
+ result: entry.result,
79
+ error: entry.error
80
+ };
81
+ }
82
+ /**
83
+ * Mark a nonce as pending (operation started but not completed)
84
+ * This prevents duplicate submissions while operation is in progress
85
+ */
86
+ async markPending(nonce, timestamp) {
87
+ await this.ensureLoaded();
88
+ this.nonceStore.set(nonce, {
89
+ status: "pending",
90
+ timestamp,
91
+ expiresAt: Date.now() + this.PENDING_TTL_MS
92
+ });
93
+ this.enforceLimits();
94
+ this.schedulePersist();
95
+ }
96
+ /**
97
+ * Store the result of a completed operation
98
+ */
99
+ async storeResult(nonce, result, ttlMs) {
100
+ await this.ensureLoaded();
101
+ const existing = this.nonceStore.get(nonce);
102
+ const timestamp = existing?.timestamp || Date.now();
103
+ this.nonceStore.set(nonce, {
104
+ result,
105
+ status: "completed",
106
+ timestamp,
107
+ expiresAt: Date.now() + (ttlMs || this.DEFAULT_TTL_MS)
108
+ });
109
+ this.enforceLimits();
110
+ this.schedulePersist();
111
+ }
112
+ /**
113
+ * Store an error result (operation failed)
114
+ * We store failures too so retries get the same error
115
+ */
116
+ async storeError(nonce, error, ttlMs) {
117
+ await this.ensureLoaded();
118
+ const existing = this.nonceStore.get(nonce);
119
+ const timestamp = existing?.timestamp || Date.now();
120
+ this.nonceStore.set(nonce, {
121
+ error,
122
+ status: "failed",
123
+ timestamp,
124
+ expiresAt: Date.now() + (ttlMs || this.DEFAULT_TTL_MS)
125
+ });
126
+ this.enforceLimits();
127
+ this.schedulePersist();
128
+ }
129
+ /**
130
+ * Remove a nonce (e.g., if operation was cancelled before starting)
131
+ */
132
+ async removeNonce(nonce) {
133
+ await this.ensureLoaded();
134
+ this.nonceStore.delete(nonce);
135
+ this.schedulePersist();
136
+ }
137
+ /**
138
+ * Check if a payload is a critical operation
139
+ */
140
+ isCriticalPayload(payload) {
141
+ if (!payload || typeof payload !== "object") {
142
+ return false;
143
+ }
144
+ const p = payload;
145
+ return p.__critical__ === true && typeof p.__nonce__ === "string";
146
+ }
147
+ /**
148
+ * Get statistics about the nonce store (for debugging)
149
+ */
150
+ getStats() {
151
+ let pending = 0;
152
+ let completed = 0;
153
+ let failed = 0;
154
+ this.nonceStore.forEach((entry) => {
155
+ switch (entry.status) {
156
+ case "pending":
157
+ pending++;
158
+ break;
159
+ case "completed":
160
+ completed++;
161
+ break;
162
+ case "failed":
163
+ failed++;
164
+ break;
165
+ }
166
+ });
167
+ return { total: this.nonceStore.size, pending, completed, failed };
168
+ }
169
+ /**
170
+ * Start periodic cleanup of expired nonces
171
+ */
172
+ startCleanup() {
173
+ if (this.cleanupInterval) return;
174
+ this.cleanupInterval = setInterval(
175
+ () => {
176
+ this.cleanup();
177
+ },
178
+ 5 * 60 * 1e3
179
+ );
180
+ }
181
+ /**
182
+ * Remove expired nonces
183
+ */
184
+ cleanup() {
185
+ const now = Date.now();
186
+ let removed = 0;
187
+ this.nonceStore.forEach((entry, nonce) => {
188
+ if (now > entry.expiresAt) {
189
+ this.nonceStore.delete(nonce);
190
+ removed++;
191
+ }
192
+ });
193
+ if (removed > 0) {
194
+ console.log(`[NonceService] Cleaned up ${removed} expired nonces`);
195
+ }
196
+ }
197
+ /**
198
+ * Destroy the service (stop cleanup interval)
199
+ */
200
+ destroy() {
201
+ if (this.cleanupInterval) {
202
+ clearInterval(this.cleanupInterval);
203
+ this.cleanupInterval = null;
204
+ }
205
+ if (this.persistTimer) {
206
+ clearTimeout(this.persistTimer);
207
+ this.persistTimer = null;
208
+ }
209
+ this.nonceStore.clear();
210
+ }
211
+ enforceLimits() {
212
+ if (this.nonceStore.size <= this.MAX_ENTRIES) return;
213
+ const sorted = Array.from(this.nonceStore.entries()).sort(
214
+ (a, b) => (a[1].timestamp ?? 0) - (b[1].timestamp ?? 0)
215
+ );
216
+ const toRemove = this.nonceStore.size - this.MAX_ENTRIES;
217
+ for (let index = 0; index < toRemove; index++) {
218
+ this.nonceStore.delete(sorted[index][0]);
219
+ }
220
+ }
221
+ schedulePersist() {
222
+ if (typeof chrome === "undefined" || !chrome.storage?.local?.set) {
223
+ return;
224
+ }
225
+ if (this.persistTimer) {
226
+ clearTimeout(this.persistTimer);
227
+ }
228
+ this.persistTimer = setTimeout(() => {
229
+ this.persistTimer = null;
230
+ void this.persist();
231
+ }, 250);
232
+ }
233
+ async persist() {
234
+ if (this.persistInFlight) {
235
+ return this.persistInFlight;
236
+ }
237
+ if (typeof chrome === "undefined" || !chrome.storage?.local?.set) {
238
+ return;
239
+ }
240
+ this.persistInFlight = (async () => {
241
+ try {
242
+ const now = Date.now();
243
+ const obj = {};
244
+ this.nonceStore.forEach((entry, nonce) => {
245
+ if (now <= entry.expiresAt) {
246
+ obj[nonce] = entry;
247
+ }
248
+ });
249
+ await chrome.storage.local.set({ [NONCE_STORE_STORAGE_KEY]: obj });
250
+ } catch {
251
+ } finally {
252
+ this.persistInFlight = null;
253
+ }
254
+ })();
255
+ return this.persistInFlight;
256
+ }
257
+ }
258
+ let nonceServiceInstance = null;
259
+ function getNonceService() {
260
+ if (!nonceServiceInstance) {
261
+ nonceServiceInstance = new NonceService();
262
+ }
263
+ return nonceServiceInstance;
264
+ }
265
+
266
+ const DEFAULT_PORT_NAME$1 = "chroma-bridge";
267
+ const earlyPorts = [];
268
+ let listenerSetup = false;
269
+ let portsClaimed = false;
270
+ let onPortConnectCallback = null;
271
+ function setupEarlyListener(portName = DEFAULT_PORT_NAME$1) {
272
+ if (listenerSetup) {
273
+ return;
274
+ }
275
+ listenerSetup = true;
276
+ chrome.runtime.onConnect.addListener((port) => {
277
+ if (port.name !== portName) {
278
+ return;
279
+ }
280
+ if (portsClaimed && onPortConnectCallback) {
281
+ onPortConnectCallback(port);
282
+ } else {
283
+ console.log(`[EarlyListener] Captured early port connection: ${port.name}`);
284
+ earlyPorts.push(port);
285
+ }
286
+ });
287
+ console.log(`[EarlyListener] Early connection listener registered for port: ${portName}`);
288
+ }
289
+ function claimEarlyPorts(onConnect) {
290
+ if (portsClaimed) {
291
+ console.warn("[EarlyListener] Ports already claimed, returning empty array");
292
+ return [];
293
+ }
294
+ portsClaimed = true;
295
+ onPortConnectCallback = onConnect;
296
+ const captured = [...earlyPorts];
297
+ earlyPorts.length = 0;
298
+ if (captured.length > 0) {
299
+ console.log(`[EarlyListener] Claimed ${captured.length} early port(s)`);
300
+ }
301
+ return captured;
302
+ }
303
+ function isEarlyListenerSetup() {
304
+ return listenerSetup;
305
+ }
306
+ function arePortsClaimed() {
307
+ return portsClaimed;
308
+ }
309
+
5
310
  const container = new container$1.Container({
6
311
  defaultScope: "Singleton"
7
312
  });
@@ -96,10 +401,13 @@ const _BridgeRuntimeManager = class _BridgeRuntimeManager {
96
401
  this.isInitialized = true;
97
402
  }
98
403
  /**
99
- * Setup Chrome runtime port listener
404
+ * Setup Chrome runtime port listener.
405
+ *
406
+ * If early listener was set up (via setupEarlyListener), this will claim
407
+ * any ports captured during bootstrap and wire them up.
100
408
  */
101
409
  setupPortListener() {
102
- chrome.runtime.onConnect.addListener((port) => {
410
+ const handlePort = (port) => {
103
411
  try {
104
412
  if (!this.isValidPort(port)) {
105
413
  return;
@@ -117,7 +425,18 @@ const _BridgeRuntimeManager = class _BridgeRuntimeManager {
117
425
  chrome.runtime.lastError;
118
426
  }
119
427
  }
120
- });
428
+ };
429
+ if (isEarlyListenerSetup()) {
430
+ const earlyPorts = claimEarlyPorts(handlePort);
431
+ if (earlyPorts.length > 0) {
432
+ this.logger.info(
433
+ `\u{1F4E1} Processing ${earlyPorts.length} early port(s) captured during bootstrap`
434
+ );
435
+ earlyPorts.forEach(handlePort);
436
+ }
437
+ } else {
438
+ chrome.runtime.onConnect.addListener(handlePort);
439
+ }
121
440
  }
122
441
  setupDirectMessageListener() {
123
442
  if (!chrome?.runtime?.onMessage?.addListener) {
@@ -218,7 +537,7 @@ const _BridgeRuntimeManager = class _BridgeRuntimeManager {
218
537
  id: request.id,
219
538
  hasPayload: !!request.payload
220
539
  });
221
- const response = await this.processMessage(context);
540
+ const response = await this.processMessage(context, port);
222
541
  this.sendResponse(port, response);
223
542
  } catch (error) {
224
543
  if (!("type" in message)) {
@@ -261,7 +580,7 @@ const _BridgeRuntimeManager = class _BridgeRuntimeManager {
261
580
  /**
262
581
  * Process message through middleware pipeline and handler
263
582
  */
264
- async processMessage(context) {
583
+ async processMessage(context, senderPort) {
265
584
  const { request } = context;
266
585
  if (request.key === "__ping__") {
267
586
  return {
@@ -278,23 +597,83 @@ const _BridgeRuntimeManager = class _BridgeRuntimeManager {
278
597
  };
279
598
  }
280
599
  this.logger.debug(`Processing message: ${request.key} (id: ${request.id})`);
600
+ const nonceService = getNonceService();
601
+ const isCritical = nonceService.isCriticalPayload(request.payload);
602
+ let nonce;
603
+ let actualPayload = request.payload;
604
+ if (isCritical) {
605
+ const criticalPayload = request.payload;
606
+ nonce = criticalPayload.__nonce__;
607
+ actualPayload = criticalPayload.data;
608
+ this.logger.debug(`Critical operation detected: ${request.key} (nonce: ${nonce})`);
609
+ const nonceCheck = await nonceService.checkNonce(nonce);
610
+ if (nonceCheck.exists) {
611
+ this.logger.debug(`Duplicate nonce detected: ${nonce} (status: ${nonceCheck.status})`);
612
+ if (nonceCheck.status === "completed") {
613
+ return {
614
+ id: request.id,
615
+ data: nonceCheck.result,
616
+ timestamp: Date.now()
617
+ };
618
+ } else if (nonceCheck.status === "failed") {
619
+ return {
620
+ id: request.id,
621
+ error: nonceCheck.error || "Operation previously failed",
622
+ timestamp: Date.now()
623
+ };
624
+ } else if (nonceCheck.status === "pending") {
625
+ return {
626
+ id: request.id,
627
+ error: "Operation already in progress",
628
+ timestamp: Date.now()
629
+ };
630
+ }
631
+ }
632
+ await nonceService.markPending(nonce, criticalPayload.__timestamp__);
633
+ if (senderPort) {
634
+ try {
635
+ senderPort.postMessage({
636
+ type: "broadcast",
637
+ key: `__ack__:${nonce}`,
638
+ payload: { received: true, timestamp: Date.now() }
639
+ });
640
+ this.logger.debug(`Sent acknowledgment for nonce: ${nonce}`);
641
+ } catch (e) {
642
+ this.logger.warn(`Failed to send acknowledgment for nonce: ${nonce} - ${e}`);
643
+ }
644
+ }
645
+ }
281
646
  const handler = this.resolveHandler(request.key);
282
647
  const middlewares = MiddlewareRegistry.pipeline(request.key);
283
648
  this.logger.debug(
284
649
  `Running pipeline for: ${request.key} with ${middlewares.length} middlewares`
285
650
  );
286
- const data = await this.runPipeline(middlewares, context, async () => {
287
- this.logger.debug(`Executing handler for: ${request.key}`);
288
- const result = await handler.handle(request.payload);
289
- this.logger.debug(`Handler completed for: ${request.key}`, { resultType: typeof result });
290
- return result;
291
- });
292
- this.logger.debug(`Message processed: ${request.key} (id: ${request.id})`);
293
- return {
294
- id: request.id,
295
- data,
296
- timestamp: Date.now()
297
- };
651
+ const handlerContext = isCritical ? { ...context, request: { ...request, payload: actualPayload } } : context;
652
+ try {
653
+ const data = await this.runPipeline(middlewares, handlerContext, async () => {
654
+ this.logger.debug(`Executing handler for: ${request.key}`);
655
+ const result = await handler.handle(actualPayload);
656
+ this.logger.debug(`Handler completed for: ${request.key}`, { resultType: typeof result });
657
+ return result;
658
+ });
659
+ if (isCritical && nonce) {
660
+ await nonceService.storeResult(nonce, data);
661
+ this.logger.debug(`Stored result for nonce: ${nonce}`);
662
+ }
663
+ this.logger.debug(`Message processed: ${request.key} (id: ${request.id})`);
664
+ return {
665
+ id: request.id,
666
+ data,
667
+ timestamp: Date.now()
668
+ };
669
+ } catch (error) {
670
+ if (isCritical && nonce) {
671
+ const errorMessage = error instanceof Error ? error.message : String(error);
672
+ await nonceService.storeError(nonce, errorMessage);
673
+ this.logger.debug(`Stored error for nonce: ${nonce}`);
674
+ }
675
+ throw error;
676
+ }
298
677
  }
299
678
  /**
300
679
  * Resolve message handler from dependency injection container
@@ -2072,7 +2451,13 @@ class BootstrapBuilder {
2072
2451
  }
2073
2452
 
2074
2453
  exports.JobState = JobState;
2454
+ exports.NonceService = NonceService;
2455
+ exports.arePortsClaimed = arePortsClaimed;
2075
2456
  exports.bootstrap = bootstrap;
2457
+ exports.claimEarlyPorts = claimEarlyPorts;
2076
2458
  exports.container = container;
2077
2459
  exports.create = create;
2078
- //# sourceMappingURL=boot-PA1aK9w-.js.map
2460
+ exports.getNonceService = getNonceService;
2461
+ exports.isEarlyListenerSetup = isEarlyListenerSetup;
2462
+ exports.setupEarlyListener = setupEarlyListener;
2463
+ //# sourceMappingURL=boot-CgF0JVQl.js.map