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