@buygent/cli 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1008 @@
1
+ // extension/protocol.ts
2
+ var BUYGENT_PROTOCOL_VERSION = 1;
3
+ var BUYGENT_NATIVE_HOST_NAME = "com.buygent.host";
4
+ var EXTENSION_RUNTIME_STATE_KEY = "buygentRuntimeState";
5
+ var EXTENSION_ORDER_CHECKPOINT_PREFIX = "buygentOrderCheckpoint:";
6
+ var NATIVE_HOST_SOCKET_FILENAME = `${BUYGENT_NATIVE_HOST_NAME}.sock`;
7
+ var createEnvelope = (type, payload, options) => ({
8
+ protocolVersion: BUYGENT_PROTOCOL_VERSION,
9
+ type,
10
+ messageId: options.messageId,
11
+ ...options.requestId ? { requestId: options.requestId } : {},
12
+ sentAt: options.sentAt ?? new Date().toISOString(),
13
+ source: options.source,
14
+ payload
15
+ });
16
+ var createErrorEnvelope = (payload, options) => createEnvelope("error", payload, options);
17
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
18
+ var decodeEnvelope = (raw, options = {}) => {
19
+ const fallback = {
20
+ source: options.source ?? "native-host",
21
+ messageId: options.messageId ?? `protocol_${Date.now()}`,
22
+ requestId: options.requestId,
23
+ sentAt: options.sentAt
24
+ };
25
+ if (!isRecord(raw)) {
26
+ return {
27
+ ok: false,
28
+ error: createErrorEnvelope({
29
+ code: "invalid_envelope",
30
+ message: "Expected a JSON object envelope."
31
+ }, fallback)
32
+ };
33
+ }
34
+ if (raw.protocolVersion !== BUYGENT_PROTOCOL_VERSION) {
35
+ return {
36
+ ok: false,
37
+ error: createErrorEnvelope({
38
+ code: "unsupported_protocol_version",
39
+ message: `Unsupported protocolVersion: ${String(raw.protocolVersion ?? "missing")}`,
40
+ details: {
41
+ supportedProtocolVersion: BUYGENT_PROTOCOL_VERSION
42
+ }
43
+ }, fallback)
44
+ };
45
+ }
46
+ if (typeof raw.type !== "string" || typeof raw.messageId !== "string" || typeof raw.sentAt !== "string" || typeof raw.source !== "string") {
47
+ return {
48
+ ok: false,
49
+ error: createErrorEnvelope({
50
+ code: "invalid_envelope",
51
+ message: "Envelope must include type, messageId, sentAt, and source fields."
52
+ }, fallback)
53
+ };
54
+ }
55
+ return {
56
+ ok: true,
57
+ envelope: raw
58
+ };
59
+ };
60
+ var TERMINAL_ORDER_STATES = new Set(["aborted", "completed_dry_run"]);
61
+ var isTerminalOrderState = (state) => TERMINAL_ORDER_STATES.has(state);
62
+ var getOrderCheckpointStorageKey = (orderId) => `${EXTENSION_ORDER_CHECKPOINT_PREFIX}${orderId}`;
63
+
64
+ // extension/order-error.ts
65
+ var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
66
+ var getOrderIdFromErrorEnvelope = (message) => {
67
+ const details = message.payload.details;
68
+ if (!isRecord2(details) || typeof details.orderId !== "string") {
69
+ return;
70
+ }
71
+ return details.orderId;
72
+ };
73
+ var isOrderScopedErrorEnvelope = (message) => typeof getOrderIdFromErrorEnvelope(message) === "string" || typeof message.requestId === "string";
74
+ var relayOrderScopedErrorEnvelope = async (message, dependencies) => {
75
+ if (!isOrderScopedErrorEnvelope(message)) {
76
+ return false;
77
+ }
78
+ await dependencies.ensureNativePort();
79
+ dependencies.postToNativeHost(message);
80
+ return true;
81
+ };
82
+
83
+ // extension/selector-config.ts
84
+ var DEFAULT_SELECTOR_CONFIG_REFRESH_WINDOW_SECONDS = 600;
85
+ var DEFAULT_SELECTOR_CONFIG_TIMEOUT_MS = 3000;
86
+ var SELECTOR_CONFIG_SETTINGS_STORAGE_KEY = "buygentSelectorConfigSettings";
87
+ var SELECTOR_CONFIG_CACHE_META_STORAGE_KEY = "buygentSelectorConfigCacheMeta:coupang";
88
+ var getSelectorConfigCacheStorageKey = (platform, version) => `buygentSelectorConfigCache:${platform}:${version}`;
89
+ var BUNDLED_COUPANG_SELECTOR_CONFIG = {
90
+ version: "coupang.v2",
91
+ platform: "coupang",
92
+ generatedAt: "2026-05-02T00:00:00.000Z",
93
+ selectors: {
94
+ loginIndicators: [
95
+ "a[href*='mycoupang']",
96
+ ".my-coupang",
97
+ ".header-my-coupang",
98
+ "[data-testid='header-my-coupang']"
99
+ ],
100
+ loggedOutIndicators: [
101
+ "a[href*='login.coupang.com']",
102
+ "a[href*='/login/login.pang']",
103
+ "button[aria-label*='로그인']"
104
+ ],
105
+ productTitle: [
106
+ "h1.prod-buy-header__title",
107
+ ".prod-buy-header__title",
108
+ "h1[class*='title']",
109
+ "[data-testid='product-title']",
110
+ "meta[property='og:title']"
111
+ ],
112
+ productPrice: [
113
+ ".total-price strong",
114
+ ".prod-sale-price .total-price strong",
115
+ ".price-value",
116
+ "[class*='price'] strong"
117
+ ],
118
+ optionGroup: [
119
+ {
120
+ container: "[data-testid='product-option-selector']",
121
+ items: "button, [role='button'], option",
122
+ label: "[data-testid='option-label'], .title, label, strong"
123
+ },
124
+ {
125
+ container: ".prod-option",
126
+ items: "button, [role='button'], option",
127
+ label: ".title, label, strong"
128
+ },
129
+ {
130
+ container: "select[class*='option'], .prod-option select",
131
+ items: "option",
132
+ label: "label, strong, .title"
133
+ },
134
+ {
135
+ container: "select[name*='quantity'], select[id*='quantity']",
136
+ items: "option",
137
+ label: "label, strong, .title"
138
+ }
139
+ ],
140
+ buyNowButton: [
141
+ "button.prod-buy-btn",
142
+ "button[aria-label*='바로구매']",
143
+ "[data-testid='buy-now-button']",
144
+ "[data-cy='buy-now-button']",
145
+ "[data-automation-id='buy-now-button']"
146
+ ],
147
+ addToCartButton: [
148
+ "button.prod-cart-btn",
149
+ "button[aria-label*='장바구니']",
150
+ "[data-testid='add-to-cart-button']",
151
+ "[data-cy='add-to-cart-button']"
152
+ ],
153
+ cartCheckoutButton: [
154
+ "[data-testid='cart-order-button']",
155
+ "[data-cy='cart-order-button']",
156
+ "[data-automation-id='cart-order-button']"
157
+ ],
158
+ checkoutTotal: [
159
+ ".order-sheet .total-price strong",
160
+ "[data-testid='payment-total'] strong",
161
+ ".final-payment-price strong",
162
+ ".price-area strong"
163
+ ]
164
+ }
165
+ };
166
+
167
+ // extension/content/coupang-probe.ts
168
+ var CHECKOUT_URL_PATTERN = /checkout\.coupang\.com|\/order-form|\/payment/u;
169
+
170
+ // extension/content/order-state.ts
171
+ var chromeApi = globalThis.chrome;
172
+ var storageArea = chromeApi?.storage?.session ?? chromeApi?.storage?.local;
173
+
174
+ // extension/content/coupang-order.ts
175
+ var COUPANG_CART_URL = "https://cart.coupang.com/cartView.pang";
176
+
177
+ // extension/order-resume.ts
178
+ var inFlightResumeDispatches = new Map;
179
+ var COUPANG_CHECKOUT_RESUME_STATES = new Set([
180
+ "cart_added",
181
+ "checkout_loaded",
182
+ "awaiting_user_confirm"
183
+ ]);
184
+ var toOrderStartPayload = (checkpoint) => ({
185
+ orderId: checkpoint.orderId,
186
+ platform: checkpoint.platform,
187
+ productUrl: checkpoint.productUrl,
188
+ ...typeof checkpoint.maxPriceKrw === "number" ? { maxPriceKrw: checkpoint.maxPriceKrw } : {},
189
+ dryRun: true,
190
+ ...typeof checkpoint.tabId === "number" ? { tabId: checkpoint.tabId } : {}
191
+ });
192
+ var withResumeState = (checkpoint, resumeState) => {
193
+ if (!resumeState) {
194
+ const { resumeState: _resumeState, ...rest } = checkpoint;
195
+ return rest;
196
+ }
197
+ return {
198
+ ...checkpoint,
199
+ resumeState
200
+ };
201
+ };
202
+ var isKnownOrderResumeUrl = (checkpoint, url) => {
203
+ if (url === checkpoint.productUrl) {
204
+ return true;
205
+ }
206
+ if (url === COUPANG_CART_URL) {
207
+ return true;
208
+ }
209
+ if (checkpoint.pageUrl && url === checkpoint.pageUrl) {
210
+ return true;
211
+ }
212
+ return COUPANG_CHECKOUT_RESUME_STATES.has(checkpoint.state) && CHECKOUT_URL_PATTERN.test(url);
213
+ };
214
+ var shouldResumeStoredOrder = (checkpoint, tabId, url) => checkpoint.platform === "coupang" && typeof checkpoint.tabId === "number" && checkpoint.tabId === tabId && !checkpoint.resumeState && !isTerminalOrderState(checkpoint.state) && isKnownOrderResumeUrl(checkpoint, url);
215
+ var dispatchStoredOrderResume = async (options) => {
216
+ const candidate = options.checkpoints.find((checkpoint) => shouldResumeStoredOrder(checkpoint, options.tabId, options.url));
217
+ if (!candidate) {
218
+ return false;
219
+ }
220
+ if (inFlightResumeDispatches.has(candidate.orderId)) {
221
+ return false;
222
+ }
223
+ const dispatch = (async () => {
224
+ const resuming = withResumeState(candidate, "resuming");
225
+ await options.writeCheckpoint(resuming);
226
+ try {
227
+ await options.sendResume(createEnvelope("order:resume", toOrderStartPayload(candidate), {
228
+ source: "extension:background",
229
+ messageId: options.createMessageId()
230
+ }));
231
+ await options.writeCheckpoint(withResumeState(resuming, "resumed"));
232
+ return true;
233
+ } catch (error) {
234
+ await options.writeCheckpoint(withResumeState(candidate, undefined));
235
+ throw error;
236
+ }
237
+ })();
238
+ inFlightResumeDispatches.set(candidate.orderId, dispatch);
239
+ try {
240
+ return await dispatch;
241
+ } finally {
242
+ if (inFlightResumeDispatches.get(candidate.orderId) === dispatch) {
243
+ inFlightResumeDispatches.delete(candidate.orderId);
244
+ }
245
+ }
246
+ };
247
+
248
+ // extension/selector-config-service.ts
249
+ var isRecord3 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
250
+ var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
251
+ var isOptionGroupArray = (value) => Array.isArray(value) && value.every((entry) => isRecord3(entry) && typeof entry.container === "string" && typeof entry.items === "string" && (entry.label === undefined || typeof entry.label === "string"));
252
+ var COUPANG_SELECTOR_CONFIG_CACHE_KEY_PREFIX = getSelectorConfigCacheStorageKey("coupang", "");
253
+ var parseStoredValue = (value) => {
254
+ if (typeof value !== "string") {
255
+ return value;
256
+ }
257
+ return JSON.parse(value);
258
+ };
259
+ var logCacheInvalidationFailure = (keys, error) => {
260
+ console.warn("Failed to invalidate malformed selector-config cache entries.", {
261
+ keys,
262
+ error: error instanceof Error ? error.message : String(error)
263
+ });
264
+ };
265
+ var invalidateMalformedCacheEntries = async (storage, keys) => {
266
+ const entries = [...new Set(Array.isArray(keys) ? keys : [keys])];
267
+ try {
268
+ await storage.remove(entries.length === 1 ? entries[0] : entries);
269
+ } catch (error) {
270
+ logCacheInvalidationFailure(entries, error);
271
+ }
272
+ };
273
+ var compareRecoveredCachePayloads = (left, right) => {
274
+ const leftGeneratedAt = Date.parse(left.generatedAt);
275
+ const rightGeneratedAt = Date.parse(right.generatedAt);
276
+ const leftHasGeneratedAt = Number.isFinite(leftGeneratedAt);
277
+ const rightHasGeneratedAt = Number.isFinite(rightGeneratedAt);
278
+ if (leftHasGeneratedAt && rightHasGeneratedAt && leftGeneratedAt !== rightGeneratedAt) {
279
+ return leftGeneratedAt - rightGeneratedAt;
280
+ }
281
+ if (leftHasGeneratedAt !== rightHasGeneratedAt) {
282
+ return leftHasGeneratedAt ? 1 : -1;
283
+ }
284
+ return left.version.localeCompare(right.version);
285
+ };
286
+ var parseCachedConfigEntry = async (storage, cacheKey, raw) => {
287
+ let parsed;
288
+ try {
289
+ parsed = parseStoredValue(raw);
290
+ } catch {
291
+ await invalidateMalformedCacheEntries(storage, cacheKey);
292
+ return;
293
+ }
294
+ const cached = parseCoupangSelectorConfigResponse(parsed);
295
+ if (!cached) {
296
+ await invalidateMalformedCacheEntries(storage, cacheKey);
297
+ return;
298
+ }
299
+ return cached;
300
+ };
301
+ var readRecoveredCachedConfig = async (storage, refreshWindowSeconds, resolvedAt, endpoint) => {
302
+ const stored = await storage.get(null);
303
+ let recovered;
304
+ for (const [key, raw] of Object.entries(stored)) {
305
+ if (!key.startsWith(COUPANG_SELECTOR_CONFIG_CACHE_KEY_PREFIX)) {
306
+ continue;
307
+ }
308
+ const cached = await parseCachedConfigEntry(storage, key, raw);
309
+ if (!cached) {
310
+ continue;
311
+ }
312
+ if (!recovered || compareRecoveredCachePayloads(cached, recovered) > 0) {
313
+ recovered = cached;
314
+ }
315
+ }
316
+ if (!recovered) {
317
+ return;
318
+ }
319
+ return withSourceState(recovered, {
320
+ platform: "coupang",
321
+ source: "cache",
322
+ resolvedAt,
323
+ endpoint,
324
+ refreshWindowSeconds
325
+ });
326
+ };
327
+ var parseCoupangSelectorConfigResponse = (value) => {
328
+ if (!isRecord3(value)) {
329
+ return;
330
+ }
331
+ const selectors = value.selectors;
332
+ if (typeof value.version !== "string" || value.platform !== "coupang" || typeof value.generatedAt !== "string" || !isRecord3(selectors) || !isStringArray(selectors.loginIndicators) || !isStringArray(selectors.loggedOutIndicators) || !isStringArray(selectors.productTitle) || !isStringArray(selectors.productPrice) || !isOptionGroupArray(selectors.optionGroup) || !isStringArray(selectors.buyNowButton) || !isStringArray(selectors.addToCartButton) || !isStringArray(selectors.cartCheckoutButton) || !isStringArray(selectors.checkoutTotal)) {
333
+ return;
334
+ }
335
+ return {
336
+ version: value.version,
337
+ platform: "coupang",
338
+ generatedAt: value.generatedAt,
339
+ selectors: {
340
+ loginIndicators: [...selectors.loginIndicators],
341
+ loggedOutIndicators: [...selectors.loggedOutIndicators],
342
+ productTitle: [...selectors.productTitle],
343
+ productPrice: [...selectors.productPrice],
344
+ optionGroup: selectors.optionGroup.map((entry) => ({
345
+ container: entry.container,
346
+ items: entry.items,
347
+ ...entry.label ? { label: entry.label } : {}
348
+ })),
349
+ buyNowButton: [...selectors.buyNowButton],
350
+ addToCartButton: [...selectors.addToCartButton],
351
+ cartCheckoutButton: [...selectors.cartCheckoutButton],
352
+ checkoutTotal: [...selectors.checkoutTotal]
353
+ }
354
+ };
355
+ };
356
+ var normalizeBaseUrl = (value) => {
357
+ if (!value) {
358
+ return;
359
+ }
360
+ const trimmed = value.trim().replace(/\/+$/u, "");
361
+ return trimmed.length > 0 ? trimmed : undefined;
362
+ };
363
+ var withSourceState = (config, state) => ({
364
+ config,
365
+ source: {
366
+ ...state,
367
+ version: config.version,
368
+ generatedAt: config.generatedAt
369
+ }
370
+ });
371
+ var readSettings = async (storage, fallback) => {
372
+ const stored = await storage.get(SELECTOR_CONFIG_SETTINGS_STORAGE_KEY);
373
+ const raw = stored[SELECTOR_CONFIG_SETTINGS_STORAGE_KEY];
374
+ const value = isRecord3(raw) ? raw : {};
375
+ return {
376
+ baseUrl: typeof value.baseUrl === "string" ? value.baseUrl : fallback?.baseUrl,
377
+ refreshWindowSeconds: typeof value.refreshWindowSeconds === "number" ? value.refreshWindowSeconds : fallback?.refreshWindowSeconds,
378
+ timeoutMs: typeof value.timeoutMs === "number" ? value.timeoutMs : fallback?.timeoutMs
379
+ };
380
+ };
381
+ var readCacheMeta = async (storage) => {
382
+ const stored = await storage.get(SELECTOR_CONFIG_CACHE_META_STORAGE_KEY);
383
+ const raw = stored[SELECTOR_CONFIG_CACHE_META_STORAGE_KEY];
384
+ if (raw === undefined) {
385
+ return;
386
+ }
387
+ let parsed;
388
+ try {
389
+ parsed = parseStoredValue(raw);
390
+ } catch {
391
+ await invalidateMalformedCacheEntries(storage, SELECTOR_CONFIG_CACHE_META_STORAGE_KEY);
392
+ return;
393
+ }
394
+ if (!isRecord3(parsed) || typeof parsed.version !== "string" || typeof parsed.fetchedAt !== "string" || typeof parsed.endpoint !== "string") {
395
+ await invalidateMalformedCacheEntries(storage, SELECTOR_CONFIG_CACHE_META_STORAGE_KEY);
396
+ return;
397
+ }
398
+ return {
399
+ version: parsed.version,
400
+ fetchedAt: parsed.fetchedAt,
401
+ endpoint: parsed.endpoint
402
+ };
403
+ };
404
+ var readCachedConfig = async (storage, meta, refreshWindowSeconds, resolvedAt) => {
405
+ if (!meta) {
406
+ return;
407
+ }
408
+ const cacheKey = getSelectorConfigCacheStorageKey("coupang", meta.version);
409
+ const stored = await storage.get(cacheKey);
410
+ const raw = stored[cacheKey];
411
+ if (raw === undefined) {
412
+ return;
413
+ }
414
+ const cached = await parseCachedConfigEntry(storage, cacheKey, raw);
415
+ if (!cached) {
416
+ return;
417
+ }
418
+ return withSourceState(cached, {
419
+ platform: "coupang",
420
+ source: "cache",
421
+ resolvedAt,
422
+ endpoint: meta.endpoint,
423
+ refreshWindowSeconds
424
+ });
425
+ };
426
+ var fetchWithTimeout = async (input, fetchImpl, timeoutMs) => {
427
+ return await Promise.race([
428
+ fetchImpl(input, {
429
+ method: "GET",
430
+ headers: {
431
+ accept: "application/json"
432
+ }
433
+ }),
434
+ new Promise((_, reject) => {
435
+ setTimeout(() => reject(new Error(`selector_config_timeout:${timeoutMs}`)), timeoutMs);
436
+ })
437
+ ]);
438
+ };
439
+ var resolveCoupangSelectorConfig = async (options) => {
440
+ const now = options.now ?? (() => Date.now());
441
+ const resolvedAt = new Date(now()).toISOString();
442
+ const settings = await readSettings(options.storage, options.settings);
443
+ const refreshWindowSeconds = settings.refreshWindowSeconds ?? DEFAULT_SELECTOR_CONFIG_REFRESH_WINDOW_SECONDS;
444
+ const timeoutMs = settings.timeoutMs ?? DEFAULT_SELECTOR_CONFIG_TIMEOUT_MS;
445
+ const baseUrl = normalizeBaseUrl(settings.baseUrl);
446
+ if (!baseUrl) {
447
+ return withSourceState(BUNDLED_COUPANG_SELECTOR_CONFIG, {
448
+ platform: "coupang",
449
+ source: "bundled",
450
+ resolvedAt,
451
+ refreshWindowSeconds
452
+ });
453
+ }
454
+ const endpoint = `${baseUrl}/coupang/v1`;
455
+ const meta = await readCacheMeta(options.storage);
456
+ const cacheAgeMs = meta ? now() - new Date(meta.fetchedAt).getTime() : Number.POSITIVE_INFINITY;
457
+ const cached = meta ? await readCachedConfig(options.storage, meta, refreshWindowSeconds, resolvedAt) : await readRecoveredCachedConfig(options.storage, refreshWindowSeconds, resolvedAt, endpoint);
458
+ if (cached && Number.isFinite(cacheAgeMs) && cacheAgeMs < refreshWindowSeconds * 1000) {
459
+ return cached;
460
+ }
461
+ const fetchImpl = options.fetchImpl ?? fetch;
462
+ try {
463
+ const response = await fetchWithTimeout(endpoint, fetchImpl, timeoutMs);
464
+ if (!response.ok) {
465
+ throw new Error(`selector_config_http_${response.status}`);
466
+ }
467
+ const parsed = parseCoupangSelectorConfigResponse(await response.json());
468
+ if (!parsed) {
469
+ throw new Error("selector_config_invalid_shape");
470
+ }
471
+ await options.storage.set({
472
+ [getSelectorConfigCacheStorageKey(parsed.platform, parsed.version)]: parsed,
473
+ [SELECTOR_CONFIG_CACHE_META_STORAGE_KEY]: {
474
+ version: parsed.version,
475
+ fetchedAt: resolvedAt,
476
+ endpoint
477
+ }
478
+ });
479
+ return withSourceState(parsed, {
480
+ platform: "coupang",
481
+ source: "remote",
482
+ resolvedAt,
483
+ endpoint,
484
+ refreshWindowSeconds
485
+ });
486
+ } catch {
487
+ if (cached) {
488
+ return cached;
489
+ }
490
+ return withSourceState(BUNDLED_COUPANG_SELECTOR_CONFIG, {
491
+ platform: "coupang",
492
+ source: "bundled",
493
+ resolvedAt,
494
+ endpoint,
495
+ refreshWindowSeconds
496
+ });
497
+ }
498
+ };
499
+
500
+ // extension/background.ts
501
+ var storageArea2 = chrome.storage?.session ?? chrome.storage?.local;
502
+ var localStorageArea = chrome.storage?.local ?? chrome.storage?.session;
503
+ var COUPANG_URL_FILTER = ["*://*.coupang.com/*"];
504
+ var NATIVE_REQUEST_TIMEOUT_MS = 5000;
505
+ var createRequestId = () => globalThis.crypto?.randomUUID?.() ?? `req_${Date.now()}_${Math.random().toString(16).slice(2)}`;
506
+ var toErrorMessage = (error) => error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown extension error";
507
+ var readStoredState = async () => {
508
+ if (!storageArea2) {
509
+ return {};
510
+ }
511
+ const stored = await storageArea2.get(EXTENSION_RUNTIME_STATE_KEY);
512
+ return stored[EXTENSION_RUNTIME_STATE_KEY] ?? {};
513
+ };
514
+ var writeStoredState = async (patch) => {
515
+ const next = {
516
+ ...await readStoredState(),
517
+ ...patch,
518
+ lastUpdatedAt: new Date().toISOString()
519
+ };
520
+ if (storageArea2) {
521
+ await storageArea2.set({
522
+ [EXTENSION_RUNTIME_STATE_KEY]: next
523
+ });
524
+ }
525
+ return next;
526
+ };
527
+ var writeOrderCheckpointToStorage = async (checkpoint) => {
528
+ if (!storageArea2) {
529
+ return checkpoint;
530
+ }
531
+ await storageArea2.set({
532
+ [getOrderCheckpointStorageKey(checkpoint.orderId)]: checkpoint
533
+ });
534
+ return checkpoint;
535
+ };
536
+ var listActiveOrderCheckpoints = async () => {
537
+ if (!storageArea2) {
538
+ return [];
539
+ }
540
+ const stored = await storageArea2.get(null);
541
+ return Object.entries(stored).filter(([key, value]) => key.startsWith("buygentOrderCheckpoint:") && Boolean(value)).map(([, value]) => value).filter((checkpoint) => !isTerminalOrderState(checkpoint.state));
542
+ };
543
+ var selectorConfigPromise;
544
+ var ensureSelectorConfig = async () => {
545
+ if (!localStorageArea) {
546
+ return await resolveCoupangSelectorConfig({
547
+ storage: {
548
+ async get() {
549
+ return {};
550
+ },
551
+ async set() {},
552
+ async remove() {}
553
+ }
554
+ });
555
+ }
556
+ if (!selectorConfigPromise) {
557
+ selectorConfigPromise = resolveCoupangSelectorConfig({
558
+ storage: localStorageArea
559
+ }).finally(() => {
560
+ selectorConfigPromise = undefined;
561
+ });
562
+ }
563
+ const resolved = await selectorConfigPromise;
564
+ await writeStoredState({
565
+ selectorConfig: resolved.source
566
+ });
567
+ return resolved;
568
+ };
569
+ var getActiveTab = async () => {
570
+ const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
571
+ if (!tab?.id) {
572
+ throw new Error("No active tab found.");
573
+ }
574
+ return tab;
575
+ };
576
+ var wait = async (ms) => {
577
+ await new Promise((resolve) => setTimeout(resolve, ms));
578
+ };
579
+ var waitForTabReady = async (tabId, timeoutMs = 1e4) => {
580
+ const current = await chrome.tabs.get(tabId);
581
+ if (current?.status === "complete") {
582
+ return;
583
+ }
584
+ await new Promise((resolve, reject) => {
585
+ const timeout = setTimeout(() => {
586
+ chrome.tabs.onUpdated.removeListener(listener);
587
+ reject(new Error(`Timed out waiting for Coupang tab ${tabId} to finish loading.`));
588
+ }, timeoutMs);
589
+ const listener = (updatedTabId, changeInfo) => {
590
+ if (updatedTabId !== tabId || changeInfo.status !== "complete") {
591
+ return;
592
+ }
593
+ clearTimeout(timeout);
594
+ chrome.tabs.onUpdated.removeListener(listener);
595
+ resolve();
596
+ };
597
+ chrome.tabs.onUpdated.addListener(listener);
598
+ });
599
+ };
600
+ var ensureCoupangTab = async (productUrl) => {
601
+ const existingTabs = await chrome.tabs.query({ url: COUPANG_URL_FILTER });
602
+ const exactMatch = existingTabs.find((tab) => typeof tab.id === "number" && tab.url === productUrl);
603
+ if (exactMatch?.id) {
604
+ await chrome.tabs.update(exactMatch.id, { active: true });
605
+ if (typeof exactMatch.windowId === "number") {
606
+ await chrome.windows.update(exactMatch.windowId, { focused: true });
607
+ }
608
+ return exactMatch;
609
+ }
610
+ const anyCoupang = existingTabs.find((tab) => typeof tab.id === "number");
611
+ if (anyCoupang?.id) {
612
+ const updated = await chrome.tabs.update(anyCoupang.id, { active: true, url: productUrl });
613
+ if (typeof updated?.windowId === "number") {
614
+ await chrome.windows.update(updated.windowId, { focused: true });
615
+ }
616
+ return updated;
617
+ }
618
+ return await chrome.tabs.create({ url: productUrl, active: true });
619
+ };
620
+ var sendMessageToTab = async (tabId, envelope, attempts = 4) => {
621
+ let lastError;
622
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
623
+ try {
624
+ return await chrome.tabs.sendMessage(tabId, envelope);
625
+ } catch (error) {
626
+ lastError = error;
627
+ await wait(attempt * 250);
628
+ }
629
+ }
630
+ throw lastError instanceof Error ? lastError : new Error("The Coupang content script did not respond.");
631
+ };
632
+ var nativePort;
633
+ var pendingNativeResponses = new Map;
634
+ var postToNativeHost = (envelope) => {
635
+ if (!nativePort) {
636
+ throw new Error("The Buygent native host is not connected.");
637
+ }
638
+ nativePort.postMessage(envelope);
639
+ };
640
+ var routeOrderStartFromNativeHost = async (message) => {
641
+ const tab = await ensureCoupangTab(message.payload.productUrl);
642
+ if (!tab?.id) {
643
+ throw new Error("Unable to open or focus a Coupang tab for the dry-run order.");
644
+ }
645
+ await waitForTabReady(tab.id);
646
+ const selectorConfig = await ensureSelectorConfig();
647
+ const startPayload = {
648
+ ...message.payload,
649
+ tabId: tab.id
650
+ };
651
+ const response = await sendMessageToTab(tab.id, createEnvelope("order:start", {
652
+ ...startPayload,
653
+ selectorConfig
654
+ }, {
655
+ source: "extension:background",
656
+ messageId: createRequestId(),
657
+ requestId: message.messageId
658
+ }));
659
+ const decoded = decodeEnvelope(response, {
660
+ source: "extension:background",
661
+ messageId: createRequestId(),
662
+ requestId: message.messageId
663
+ });
664
+ if (!decoded.ok) {
665
+ throw new Error(decoded.error.payload.message);
666
+ }
667
+ await writeStoredState({
668
+ activeTabUrl: typeof tab.url === "string" ? tab.url : message.payload.productUrl,
669
+ lastError: undefined,
670
+ selectorConfig: selectorConfig.source
671
+ });
672
+ return createEnvelope("order:ack", {
673
+ orderId: message.payload.orderId,
674
+ accepted: true,
675
+ tabId: tab.id
676
+ }, {
677
+ source: "extension:background",
678
+ messageId: createRequestId(),
679
+ requestId: message.messageId
680
+ });
681
+ };
682
+ var routeRuntimeStateRequestFromNativeHost = async (message) => {
683
+ const selectorConfig = await ensureSelectorConfig();
684
+ const state = await writeStoredState({
685
+ selectorConfig: selectorConfig.source
686
+ });
687
+ return createEnvelope("runtime:state", { state }, {
688
+ source: "extension:background",
689
+ messageId: createRequestId(),
690
+ requestId: message.messageId
691
+ });
692
+ };
693
+ var handleNativeHostMessage = async (rawMessage) => {
694
+ const decoded = decodeEnvelope(rawMessage, {
695
+ source: "extension:background",
696
+ messageId: createRequestId()
697
+ });
698
+ if (!decoded.ok) {
699
+ return;
700
+ }
701
+ const message = decoded.envelope;
702
+ if (message.requestId && pendingNativeResponses.has(message.requestId)) {
703
+ const pending = pendingNativeResponses.get(message.requestId);
704
+ if (pending) {
705
+ clearTimeout(pending.timeout);
706
+ pendingNativeResponses.delete(message.requestId);
707
+ pending.resolve(message);
708
+ return;
709
+ }
710
+ }
711
+ if (message.type === "order:start") {
712
+ try {
713
+ postToNativeHost(await routeOrderStartFromNativeHost(message));
714
+ } catch (error) {
715
+ postToNativeHost(createErrorEnvelope({
716
+ code: "internal_error",
717
+ message: toErrorMessage(error),
718
+ details: {
719
+ orderId: message.payload.orderId
720
+ }
721
+ }, {
722
+ source: "extension:background",
723
+ messageId: createRequestId(),
724
+ requestId: message.messageId
725
+ }));
726
+ await writeStoredState({
727
+ lastError: toErrorMessage(error)
728
+ });
729
+ }
730
+ return;
731
+ }
732
+ if (message.type === "runtime:get-state") {
733
+ try {
734
+ postToNativeHost(await routeRuntimeStateRequestFromNativeHost(message));
735
+ } catch (error) {
736
+ postToNativeHost(createErrorEnvelope({
737
+ code: "internal_error",
738
+ message: toErrorMessage(error)
739
+ }, {
740
+ source: "extension:background",
741
+ messageId: createRequestId(),
742
+ requestId: message.messageId
743
+ }));
744
+ await writeStoredState({
745
+ lastError: toErrorMessage(error)
746
+ });
747
+ }
748
+ }
749
+ };
750
+ var ensureNativePort = async () => {
751
+ if (nativePort) {
752
+ return nativePort;
753
+ }
754
+ nativePort = chrome.runtime.connectNative(BUYGENT_NATIVE_HOST_NAME);
755
+ nativePort.onMessage.addListener((message) => {
756
+ handleNativeHostMessage(message);
757
+ });
758
+ nativePort.onDisconnect.addListener(() => {
759
+ nativePort = undefined;
760
+ for (const pending of pendingNativeResponses.values()) {
761
+ clearTimeout(pending.timeout);
762
+ pending.reject(new Error(chrome.runtime.lastError?.message ?? "The Buygent native host disconnected."));
763
+ }
764
+ pendingNativeResponses.clear();
765
+ });
766
+ return nativePort;
767
+ };
768
+ var sendNativePingRequest = async (payload) => {
769
+ await ensureNativePort();
770
+ const request = createEnvelope("native:ping", payload, {
771
+ source: "extension:background",
772
+ messageId: createRequestId()
773
+ });
774
+ const responsePromise = new Promise((resolve, reject) => {
775
+ const timeout = setTimeout(() => {
776
+ pendingNativeResponses.delete(request.messageId);
777
+ reject(new Error("Timed out waiting for native host response to native:ping."));
778
+ }, NATIVE_REQUEST_TIMEOUT_MS);
779
+ pendingNativeResponses.set(request.messageId, { resolve, reject, timeout });
780
+ });
781
+ postToNativeHost(request);
782
+ return await responsePromise;
783
+ };
784
+ var getStateResponse = async (request) => {
785
+ const current = await readStoredState();
786
+ const selectorConfig = await ensureSelectorConfig();
787
+ try {
788
+ const activeTab = await getActiveTab();
789
+ const state = await writeStoredState({
790
+ ...current,
791
+ activeTabUrl: activeTab.url,
792
+ selectorConfig: selectorConfig.source
793
+ });
794
+ return createEnvelope("popup:state", { state }, {
795
+ source: "extension:background",
796
+ messageId: createRequestId(),
797
+ requestId: request.messageId
798
+ });
799
+ } catch {
800
+ const state = await writeStoredState({
801
+ ...current,
802
+ selectorConfig: selectorConfig.source
803
+ });
804
+ return createEnvelope("popup:state", { state }, {
805
+ source: "extension:background",
806
+ messageId: createRequestId(),
807
+ requestId: request.messageId
808
+ });
809
+ }
810
+ };
811
+ var requestCoupangProbe = async (request) => {
812
+ const activeTab = await getActiveTab();
813
+ const activeTabUrl = typeof activeTab.url === "string" ? activeTab.url : undefined;
814
+ if (!activeTabUrl?.includes("coupang.com")) {
815
+ throw new Error("Open a Coupang tab before probing the active page.");
816
+ }
817
+ const selectorConfig = await ensureSelectorConfig();
818
+ const response = await sendMessageToTab(activeTab.id, createEnvelope("coupang:probe", { selectorConfig }, {
819
+ source: "extension:background",
820
+ messageId: createRequestId(),
821
+ requestId: request.messageId
822
+ }));
823
+ const decoded = decodeEnvelope(response, {
824
+ source: "extension:background",
825
+ messageId: createRequestId(),
826
+ requestId: request.messageId
827
+ });
828
+ if (!decoded.ok) {
829
+ throw new Error(decoded.error.payload.message);
830
+ }
831
+ if (decoded.envelope.type === "error") {
832
+ throw new Error(decoded.envelope.payload.message);
833
+ }
834
+ const probeEnvelope = decoded.envelope;
835
+ const probe = probeEnvelope.payload.probe;
836
+ const state = await writeStoredState({
837
+ lastProbe: probe,
838
+ activeTabUrl,
839
+ lastError: undefined,
840
+ selectorConfig: selectorConfig.source
841
+ });
842
+ return createEnvelope("popup:probe-result", {
843
+ probe,
844
+ state
845
+ }, {
846
+ source: "extension:background",
847
+ messageId: createRequestId(),
848
+ requestId: request.messageId
849
+ });
850
+ };
851
+ var pingNativeHost = async (request) => {
852
+ const response = await sendNativePingRequest({
853
+ extensionVersion: chrome.runtime.getManifest().version
854
+ });
855
+ const nativeHostResponse = response;
856
+ const state = await writeStoredState({
857
+ lastNativeHostResponse: nativeHostResponse,
858
+ lastError: response.type === "error" ? response.payload.message : undefined
859
+ });
860
+ return createEnvelope("popup:native-host-response", {
861
+ response: nativeHostResponse,
862
+ state
863
+ }, {
864
+ source: "extension:background",
865
+ messageId: createRequestId(),
866
+ requestId: request.messageId
867
+ });
868
+ };
869
+ var toPopupError = async (requestId, error) => {
870
+ await writeStoredState({
871
+ lastError: toErrorMessage(error)
872
+ });
873
+ return createErrorEnvelope({
874
+ code: "internal_error",
875
+ message: toErrorMessage(error)
876
+ }, {
877
+ source: "extension:background",
878
+ messageId: createRequestId(),
879
+ requestId
880
+ });
881
+ };
882
+ var relayOrderUpdateToNativeHost = async (message) => {
883
+ await ensureNativePort();
884
+ postToNativeHost(createEnvelope(message.type, message.payload, {
885
+ source: "extension:background",
886
+ messageId: createRequestId(),
887
+ requestId: message.requestId
888
+ }));
889
+ await writeStoredState({
890
+ ...message.type === "order:status" ? {
891
+ lastOrderStatus: message.payload
892
+ } : {
893
+ lastOrderCheckpoint: message.payload
894
+ },
895
+ activeTabUrl: message.payload.pageUrl,
896
+ lastError: message.payload.lastError
897
+ });
898
+ };
899
+ var maybeResumeStoredOrder = async (tabId, url) => {
900
+ const activeCheckpoints = await listActiveOrderCheckpoints();
901
+ const selectorConfig = await ensureSelectorConfig();
902
+ await dispatchStoredOrderResume({
903
+ checkpoints: activeCheckpoints,
904
+ tabId,
905
+ url,
906
+ writeCheckpoint: writeOrderCheckpointToStorage,
907
+ sendResume: async (envelope) => {
908
+ await sendMessageToTab(tabId, {
909
+ ...envelope,
910
+ payload: {
911
+ ...envelope.payload,
912
+ selectorConfig
913
+ }
914
+ });
915
+ },
916
+ createMessageId: createRequestId
917
+ }).catch(() => {
918
+ return;
919
+ });
920
+ };
921
+ var handleRuntimeMessage = async (rawMessage, sender) => {
922
+ const decoded = decodeEnvelope(rawMessage, {
923
+ source: "extension:background",
924
+ messageId: createRequestId()
925
+ });
926
+ if (!decoded.ok) {
927
+ return decoded.error;
928
+ }
929
+ const message = decoded.envelope;
930
+ switch (message.type) {
931
+ case "popup:get-state":
932
+ return await getStateResponse(message);
933
+ case "popup:probe-active-tab":
934
+ return await requestCoupangProbe(message);
935
+ case "popup:ping-native-host":
936
+ return await pingNativeHost(message);
937
+ case "content:ready":
938
+ if (typeof sender.tab?.id === "number") {
939
+ maybeResumeStoredOrder(sender.tab.id, message.payload.url);
940
+ }
941
+ return createEnvelope("content:ack", { ok: true }, {
942
+ source: "extension:background",
943
+ messageId: createRequestId(),
944
+ requestId: message.messageId
945
+ });
946
+ case "order:status":
947
+ case "order:checkpoint":
948
+ await relayOrderUpdateToNativeHost(message);
949
+ return createEnvelope("content:ack", { orderId: message.payload.orderId, ok: true }, {
950
+ source: "extension:background",
951
+ messageId: createRequestId(),
952
+ requestId: message.messageId
953
+ });
954
+ case "error":
955
+ await writeStoredState({
956
+ lastError: message.payload.message
957
+ });
958
+ await relayOrderScopedErrorEnvelope(message, {
959
+ ensureNativePort,
960
+ postToNativeHost
961
+ }).catch(() => {
962
+ return;
963
+ });
964
+ return createEnvelope("content:ack", {
965
+ ...getOrderIdFromErrorEnvelope(message) ? { orderId: getOrderIdFromErrorEnvelope(message) } : {},
966
+ ok: true
967
+ }, {
968
+ source: "extension:background",
969
+ messageId: createRequestId(),
970
+ requestId: message.messageId
971
+ });
972
+ default:
973
+ return createErrorEnvelope({
974
+ code: "unsupported_message",
975
+ message: `Unsupported extension runtime message: ${message.type ?? "unknown"}`
976
+ }, {
977
+ source: "extension:background",
978
+ messageId: createRequestId(),
979
+ requestId: message.messageId
980
+ });
981
+ }
982
+ };
983
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
984
+ handleRuntimeMessage(message, sender).then(sendResponse).catch(async (error) => sendResponse(await toPopupError(message?.messageId, error)));
985
+ return true;
986
+ });
987
+ chrome.runtime.onStartup?.addListener(() => {
988
+ ensureNativePort().catch(() => {
989
+ return;
990
+ });
991
+ ensureSelectorConfig().catch(() => {
992
+ return;
993
+ });
994
+ });
995
+ chrome.runtime.onInstalled?.addListener(() => {
996
+ ensureNativePort().catch(() => {
997
+ return;
998
+ });
999
+ ensureSelectorConfig().catch(() => {
1000
+ return;
1001
+ });
1002
+ });
1003
+ ensureNativePort().catch(() => {
1004
+ return;
1005
+ });
1006
+ ensureSelectorConfig().catch(() => {
1007
+ return;
1008
+ });