@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.
- package/LICENSE +3 -0
- package/README.md +23 -0
- package/dist/cli.js +327 -0
- package/dist/extension/background.js +1008 -0
- package/dist/extension/content/coupang.js +914 -0
- package/dist/extension/manifest.json +36 -0
- package/dist/extension/popup.html +132 -0
- package/dist/extension/popup.js +119 -0
- package/dist/native-host.js +4 -0
- package/package.json +32 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// extension/protocol.ts
|
|
3
|
+
var BUYGENT_PROTOCOL_VERSION = 1;
|
|
4
|
+
var BUYGENT_NATIVE_HOST_NAME = "com.buygent.host";
|
|
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/selector-config.ts
|
|
65
|
+
var BUNDLED_COUPANG_SELECTOR_CONFIG = {
|
|
66
|
+
version: "coupang.v2",
|
|
67
|
+
platform: "coupang",
|
|
68
|
+
generatedAt: "2026-05-02T00:00:00.000Z",
|
|
69
|
+
selectors: {
|
|
70
|
+
loginIndicators: [
|
|
71
|
+
"a[href*='mycoupang']",
|
|
72
|
+
".my-coupang",
|
|
73
|
+
".header-my-coupang",
|
|
74
|
+
"[data-testid='header-my-coupang']"
|
|
75
|
+
],
|
|
76
|
+
loggedOutIndicators: [
|
|
77
|
+
"a[href*='login.coupang.com']",
|
|
78
|
+
"a[href*='/login/login.pang']",
|
|
79
|
+
"button[aria-label*='로그인']"
|
|
80
|
+
],
|
|
81
|
+
productTitle: [
|
|
82
|
+
"h1.prod-buy-header__title",
|
|
83
|
+
".prod-buy-header__title",
|
|
84
|
+
"h1[class*='title']",
|
|
85
|
+
"[data-testid='product-title']",
|
|
86
|
+
"meta[property='og:title']"
|
|
87
|
+
],
|
|
88
|
+
productPrice: [
|
|
89
|
+
".total-price strong",
|
|
90
|
+
".prod-sale-price .total-price strong",
|
|
91
|
+
".price-value",
|
|
92
|
+
"[class*='price'] strong"
|
|
93
|
+
],
|
|
94
|
+
optionGroup: [
|
|
95
|
+
{
|
|
96
|
+
container: "[data-testid='product-option-selector']",
|
|
97
|
+
items: "button, [role='button'], option",
|
|
98
|
+
label: "[data-testid='option-label'], .title, label, strong"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
container: ".prod-option",
|
|
102
|
+
items: "button, [role='button'], option",
|
|
103
|
+
label: ".title, label, strong"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
container: "select[class*='option'], .prod-option select",
|
|
107
|
+
items: "option",
|
|
108
|
+
label: "label, strong, .title"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
container: "select[name*='quantity'], select[id*='quantity']",
|
|
112
|
+
items: "option",
|
|
113
|
+
label: "label, strong, .title"
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
buyNowButton: [
|
|
117
|
+
"button.prod-buy-btn",
|
|
118
|
+
"button[aria-label*='바로구매']",
|
|
119
|
+
"[data-testid='buy-now-button']",
|
|
120
|
+
"[data-cy='buy-now-button']",
|
|
121
|
+
"[data-automation-id='buy-now-button']"
|
|
122
|
+
],
|
|
123
|
+
addToCartButton: [
|
|
124
|
+
"button.prod-cart-btn",
|
|
125
|
+
"button[aria-label*='장바구니']",
|
|
126
|
+
"[data-testid='add-to-cart-button']",
|
|
127
|
+
"[data-cy='add-to-cart-button']"
|
|
128
|
+
],
|
|
129
|
+
cartCheckoutButton: [
|
|
130
|
+
"[data-testid='cart-order-button']",
|
|
131
|
+
"[data-cy='cart-order-button']",
|
|
132
|
+
"[data-automation-id='cart-order-button']"
|
|
133
|
+
],
|
|
134
|
+
checkoutTotal: [
|
|
135
|
+
".order-sheet .total-price strong",
|
|
136
|
+
"[data-testid='payment-total'] strong",
|
|
137
|
+
".final-payment-price strong",
|
|
138
|
+
".price-area strong"
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// extension/content/coupang-probe.ts
|
|
144
|
+
var PRODUCT_URL_PATTERN = /\/vp\/products\/(\d+)/u;
|
|
145
|
+
var LOGIN_URL_PATTERN = /login\.coupang\.com|\/login\//u;
|
|
146
|
+
var CART_URL_PATTERN = /cart\.coupang\.com|\/cartView/u;
|
|
147
|
+
var CHECKOUT_URL_PATTERN = /checkout\.coupang\.com|\/order-form|\/payment/u;
|
|
148
|
+
var normalizeText = (value) => value?.replace(/\s+/gu, " ").trim() ?? "";
|
|
149
|
+
var extractKrwAmount = (value) => {
|
|
150
|
+
const digits = value?.replace(/[^\d]/gu, "") ?? "";
|
|
151
|
+
return digits ? Number.parseInt(digits, 10) : undefined;
|
|
152
|
+
};
|
|
153
|
+
var toSelectorGroup = (selectors) => ({ anyOf: selectors });
|
|
154
|
+
var getSelectorConfig = (selectorConfig) => selectorConfig ?? BUNDLED_COUPANG_SELECTOR_CONFIG;
|
|
155
|
+
var detectCoupangPageKind = (url, dom, selectorConfig) => {
|
|
156
|
+
const config = getSelectorConfig(selectorConfig);
|
|
157
|
+
if (LOGIN_URL_PATTERN.test(url)) {
|
|
158
|
+
return "login";
|
|
159
|
+
}
|
|
160
|
+
if (PRODUCT_URL_PATTERN.test(url)) {
|
|
161
|
+
return "product";
|
|
162
|
+
}
|
|
163
|
+
if (CART_URL_PATTERN.test(url) || dom.hasAny(toSelectorGroup(config.selectors.cartCheckoutButton))) {
|
|
164
|
+
return "cart";
|
|
165
|
+
}
|
|
166
|
+
if (CHECKOUT_URL_PATTERN.test(url) || dom.hasAny(toSelectorGroup(config.selectors.checkoutTotal))) {
|
|
167
|
+
return "checkout";
|
|
168
|
+
}
|
|
169
|
+
if (/www\.coupang\.com\/?$/u.test(url)) {
|
|
170
|
+
return "home";
|
|
171
|
+
}
|
|
172
|
+
return "unknown";
|
|
173
|
+
};
|
|
174
|
+
var detectCoupangSessionState = (url, dom, selectorConfig) => {
|
|
175
|
+
const config = getSelectorConfig(selectorConfig);
|
|
176
|
+
if (dom.hasAny(toSelectorGroup(config.selectors.loginIndicators))) {
|
|
177
|
+
return "logged-in";
|
|
178
|
+
}
|
|
179
|
+
const bodyText = normalizeText(dom.bodyText);
|
|
180
|
+
if (/마이쿠팡|주문목록|쿠페이/u.test(bodyText)) {
|
|
181
|
+
return "logged-in";
|
|
182
|
+
}
|
|
183
|
+
if (LOGIN_URL_PATTERN.test(url) || dom.hasAny(toSelectorGroup(config.selectors.loggedOutIndicators))) {
|
|
184
|
+
return "logged-out";
|
|
185
|
+
}
|
|
186
|
+
if (/로그인|회원가입/u.test(bodyText)) {
|
|
187
|
+
return "logged-out";
|
|
188
|
+
}
|
|
189
|
+
return "unknown";
|
|
190
|
+
};
|
|
191
|
+
var buildCoupangProbe = (url, dom, observedAtOrOptions) => {
|
|
192
|
+
const observedAt = typeof observedAtOrOptions === "string" ? observedAtOrOptions : observedAtOrOptions?.observedAt ?? new Date().toISOString();
|
|
193
|
+
const selectorConfig = typeof observedAtOrOptions === "string" ? undefined : observedAtOrOptions?.selectorConfig;
|
|
194
|
+
const config = getSelectorConfig(selectorConfig);
|
|
195
|
+
const pageKind = detectCoupangPageKind(url, dom, config);
|
|
196
|
+
const sessionState = detectCoupangSessionState(url, dom, config);
|
|
197
|
+
const title = normalizeText(dom.title) || "Coupang";
|
|
198
|
+
const productId = PRODUCT_URL_PATTERN.exec(url)?.[1];
|
|
199
|
+
const productTitle = normalizeText(dom.queryText(toSelectorGroup(config.selectors.productTitle))) || undefined;
|
|
200
|
+
const priceText = normalizeText(dom.queryText(toSelectorGroup(config.selectors.productPrice))) || undefined;
|
|
201
|
+
const priceKrw = extractKrwAmount(priceText);
|
|
202
|
+
const buyNowAvailable = dom.hasAny(toSelectorGroup(config.selectors.buyNowButton)) || /바로구매/u.test(normalizeText(dom.bodyText));
|
|
203
|
+
const cartCheckoutAvailable = dom.hasAny(toSelectorGroup(config.selectors.cartCheckoutButton));
|
|
204
|
+
const checkoutTotalText = normalizeText(dom.queryText(toSelectorGroup(config.selectors.checkoutTotal))) || undefined;
|
|
205
|
+
const checkoutTotalKrw = extractKrwAmount(checkoutTotalText);
|
|
206
|
+
return {
|
|
207
|
+
platform: "coupang",
|
|
208
|
+
url,
|
|
209
|
+
title,
|
|
210
|
+
pageKind,
|
|
211
|
+
sessionState,
|
|
212
|
+
loggedIn: sessionState === "logged-in",
|
|
213
|
+
selectorsVersion: config.version,
|
|
214
|
+
observedAt,
|
|
215
|
+
...pageKind === "product" || productTitle || priceText || productId ? {
|
|
216
|
+
product: {
|
|
217
|
+
...productId ? { productId } : {},
|
|
218
|
+
...productTitle ? { title: productTitle } : {},
|
|
219
|
+
...priceText ? { priceText } : {},
|
|
220
|
+
...typeof priceKrw === "number" ? { priceKrw } : {},
|
|
221
|
+
buyNowAvailable,
|
|
222
|
+
cartCheckoutAvailable
|
|
223
|
+
}
|
|
224
|
+
} : {},
|
|
225
|
+
...pageKind === "checkout" || checkoutTotalText || typeof checkoutTotalKrw === "number" ? {
|
|
226
|
+
checkout: {
|
|
227
|
+
...checkoutTotalText ? { totalPriceText: checkoutTotalText } : {},
|
|
228
|
+
...typeof checkoutTotalKrw === "number" ? { totalPriceKrw: checkoutTotalKrw } : {}
|
|
229
|
+
}
|
|
230
|
+
} : {}
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
var createDocumentProbeAdapter = (document2) => ({
|
|
234
|
+
title: document2.title,
|
|
235
|
+
bodyText: document2.body?.innerText ?? document2.body?.textContent ?? "",
|
|
236
|
+
hasAny(group) {
|
|
237
|
+
return group.anyOf.some((selector) => {
|
|
238
|
+
const element = document2.querySelector(selector);
|
|
239
|
+
if (!element) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
if (element instanceof HTMLMetaElement) {
|
|
243
|
+
return Boolean(normalizeText(element.content));
|
|
244
|
+
}
|
|
245
|
+
return Boolean(normalizeText(element.textContent) || element instanceof HTMLElement);
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
queryText(group) {
|
|
249
|
+
for (const selector of group.anyOf) {
|
|
250
|
+
const element = document2.querySelector(selector);
|
|
251
|
+
if (!element) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (element instanceof HTMLMetaElement) {
|
|
255
|
+
const content = normalizeText(element.content);
|
|
256
|
+
if (content) {
|
|
257
|
+
return content;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const text = normalizeText(element.textContent);
|
|
261
|
+
if (text) {
|
|
262
|
+
return text;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// extension/content/coupang-option-selection.ts
|
|
270
|
+
var SOLD_OUT_PATTERN = /품절|sold\s*out|out\s*of\s*stock/u;
|
|
271
|
+
var SIZE_PATTERN = /사이즈|size/u;
|
|
272
|
+
var COLOR_PATTERN = /색상|컬러|color/u;
|
|
273
|
+
var QUANTITY_PATTERN = /수량|개수|qty|quantity/u;
|
|
274
|
+
var DISABLED_CLASS_PATTERN = /disabled|soldout|sold-out|unavailable|gray|grey/u;
|
|
275
|
+
var wait = async (ms) => {
|
|
276
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
277
|
+
};
|
|
278
|
+
var toArray = (value) => value ? [...value] : [];
|
|
279
|
+
var getText = (element) => normalizeText(element?.textContent ?? (typeof element?.value === "string" ? element.value : ""));
|
|
280
|
+
var getAttribute = (element, name) => element.getAttribute(name) ?? undefined;
|
|
281
|
+
var isElementAvailable = (element) => {
|
|
282
|
+
const text = getText(element);
|
|
283
|
+
const className = typeof element.className === "string" ? element.className : "";
|
|
284
|
+
const ariaDisabled = getAttribute(element, "aria-disabled");
|
|
285
|
+
return !element.hidden && !element.disabled && ariaDisabled !== "true" && !DISABLED_CLASS_PATTERN.test(className) && !SOLD_OUT_PATTERN.test(text);
|
|
286
|
+
};
|
|
287
|
+
var isElementSelected = (element) => {
|
|
288
|
+
if (element.selected) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
const ariaSelected = getAttribute(element, "aria-selected");
|
|
292
|
+
const ariaPressed = getAttribute(element, "aria-pressed");
|
|
293
|
+
const ariaChecked = getAttribute(element, "aria-checked");
|
|
294
|
+
const className = typeof element.className === "string" ? element.className : "";
|
|
295
|
+
return ariaSelected === "true" || ariaPressed === "true" || ariaChecked === "true" || /selected|active|checked|current|on/u.test(className);
|
|
296
|
+
};
|
|
297
|
+
var inferGroupKind = (label, choices) => {
|
|
298
|
+
const normalizedLabel = normalizeText(label);
|
|
299
|
+
if (QUANTITY_PATTERN.test(normalizedLabel)) {
|
|
300
|
+
return "quantity";
|
|
301
|
+
}
|
|
302
|
+
if (COLOR_PATTERN.test(normalizedLabel)) {
|
|
303
|
+
return "color";
|
|
304
|
+
}
|
|
305
|
+
if (SIZE_PATTERN.test(normalizedLabel)) {
|
|
306
|
+
return "size";
|
|
307
|
+
}
|
|
308
|
+
if (choices.every((choice) => /^\d+$/u.test(choice.value))) {
|
|
309
|
+
return "quantity";
|
|
310
|
+
}
|
|
311
|
+
return "unknown";
|
|
312
|
+
};
|
|
313
|
+
var parseMinimumQuantity = (choices) => {
|
|
314
|
+
const numericValues = choices.map((choice) => Number.parseInt(choice.value.replace(/[^\d]/gu, ""), 10)).filter((value) => Number.isInteger(value) && value > 0);
|
|
315
|
+
if (numericValues.length === 0) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
return Math.min(...numericValues);
|
|
319
|
+
};
|
|
320
|
+
var toGroupKey = (kind, label, index) => `${kind}:${normalizeText(label)?.toLowerCase() || `group-${index}`}`;
|
|
321
|
+
var activateElement = async (element) => {
|
|
322
|
+
const tagName = element.tagName?.toUpperCase();
|
|
323
|
+
if (tagName === "OPTION") {
|
|
324
|
+
const parent = element.parentElement;
|
|
325
|
+
if (parent && typeof element.value === "string") {
|
|
326
|
+
parent.value = element.value;
|
|
327
|
+
if (typeof parent.dispatchEvent === "function") {
|
|
328
|
+
parent.dispatchEvent(new Event("change", { bubbles: true }));
|
|
329
|
+
}
|
|
330
|
+
await wait(50);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (typeof element.click === "function") {
|
|
335
|
+
element.click();
|
|
336
|
+
await wait(50);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
throw new Error("options_dom_unrecognized");
|
|
340
|
+
};
|
|
341
|
+
var createChoice = (element) => ({
|
|
342
|
+
value: getText(element),
|
|
343
|
+
available: isElementAvailable(element),
|
|
344
|
+
selected: isElementSelected(element),
|
|
345
|
+
async activate() {
|
|
346
|
+
await activateElement(element);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
var detectGroups = (root, selectorConfig) => {
|
|
350
|
+
const seen = new Set;
|
|
351
|
+
const groups = [];
|
|
352
|
+
selectorConfig.selectors.optionGroup.forEach((selectorGroup, configIndex) => {
|
|
353
|
+
const containers = toArray(root.querySelectorAll(selectorGroup.container));
|
|
354
|
+
containers.forEach((container, containerIndex) => {
|
|
355
|
+
if (seen.has(container)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
seen.add(container);
|
|
359
|
+
const choices = toArray(container.querySelectorAll(selectorGroup.items)).map((element) => createChoice(element)).filter((choice) => choice.value.length > 0);
|
|
360
|
+
if (choices.length === 0) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const label = selectorGroup.label ? getText(container.querySelector(selectorGroup.label)) || undefined : undefined;
|
|
364
|
+
const kind = inferGroupKind(label, choices);
|
|
365
|
+
groups.push({
|
|
366
|
+
key: toGroupKey(kind, label, configIndex * 100 + containerIndex),
|
|
367
|
+
kind,
|
|
368
|
+
label,
|
|
369
|
+
required: true,
|
|
370
|
+
minimumQuantity: kind === "quantity" ? parseMinimumQuantity(choices) : undefined,
|
|
371
|
+
choices
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
return groups;
|
|
376
|
+
};
|
|
377
|
+
var findPreferredValue = (group, preferences, existingPlan) => {
|
|
378
|
+
const fromExisting = existingPlan?.groups.find((candidate) => candidate.key === group.key)?.desiredValue;
|
|
379
|
+
if (fromExisting) {
|
|
380
|
+
return fromExisting;
|
|
381
|
+
}
|
|
382
|
+
if (!preferences) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const candidates = [group.key, group.kind, normalizeText(group.label).toLowerCase()].filter((value) => value.length > 0);
|
|
386
|
+
for (const candidate of candidates) {
|
|
387
|
+
const preferred = preferences[candidate];
|
|
388
|
+
if (typeof preferred === "string" && preferred.trim().length > 0) {
|
|
389
|
+
return preferred.trim();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
};
|
|
394
|
+
var chooseDesiredValue = (group, preferences, existingPlan) => {
|
|
395
|
+
const availableChoices = group.choices.filter((choice) => choice.available);
|
|
396
|
+
if (availableChoices.length === 0) {
|
|
397
|
+
throw new Error("options_unavailable");
|
|
398
|
+
}
|
|
399
|
+
if (group.kind === "quantity") {
|
|
400
|
+
const minimumQuantity = group.minimumQuantity ?? 1;
|
|
401
|
+
const matchingQuantity = availableChoices.find((choice) => {
|
|
402
|
+
const numericValue = Number.parseInt(choice.value.replace(/[^\d]/gu, ""), 10);
|
|
403
|
+
return Number.isInteger(numericValue) && numericValue >= minimumQuantity;
|
|
404
|
+
});
|
|
405
|
+
return matchingQuantity?.value ?? availableChoices[0].value;
|
|
406
|
+
}
|
|
407
|
+
const preferredValue = findPreferredValue(group, preferences, existingPlan);
|
|
408
|
+
if (preferredValue) {
|
|
409
|
+
const preferred = availableChoices.find((choice) => normalizeText(choice.value).toLowerCase() === normalizeText(preferredValue).toLowerCase());
|
|
410
|
+
if (preferred) {
|
|
411
|
+
return preferred.value;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return availableChoices[0].value;
|
|
415
|
+
};
|
|
416
|
+
var toPlanGroup = (group, preferences, existingPlan) => {
|
|
417
|
+
const desiredValue = chooseDesiredValue(group, preferences, existingPlan);
|
|
418
|
+
const selected = group.choices.find((choice) => choice.selected && normalizeText(choice.value).toLowerCase() === normalizeText(desiredValue).toLowerCase());
|
|
419
|
+
return {
|
|
420
|
+
key: group.key,
|
|
421
|
+
kind: group.kind,
|
|
422
|
+
...group.label ? { label: group.label } : {},
|
|
423
|
+
required: group.required,
|
|
424
|
+
desiredValue,
|
|
425
|
+
...selected ? { selectedValue: selected.value } : {},
|
|
426
|
+
availableValues: group.choices.filter((choice) => choice.available).map((choice) => choice.value),
|
|
427
|
+
status: selected ? "selected" : "pending",
|
|
428
|
+
...typeof group.minimumQuantity === "number" ? { minimumQuantity: group.minimumQuantity } : {}
|
|
429
|
+
};
|
|
430
|
+
};
|
|
431
|
+
var applyPlanGroup = async (detectedGroup, planGroup) => {
|
|
432
|
+
const desiredValue = normalizeText(planGroup.desiredValue).toLowerCase();
|
|
433
|
+
const matchingChoice = detectedGroup.choices.find((choice) => normalizeText(choice.value).toLowerCase() === desiredValue);
|
|
434
|
+
if (!matchingChoice || !matchingChoice.available) {
|
|
435
|
+
throw new Error("options_unavailable");
|
|
436
|
+
}
|
|
437
|
+
if (!matchingChoice.selected) {
|
|
438
|
+
await matchingChoice.activate();
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
...planGroup,
|
|
442
|
+
selectedValue: matchingChoice.value,
|
|
443
|
+
status: "selected"
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
var buildSelectionRequired = (root) => /옵션을\s*선택|필수\s*옵션|선택해\s*주세요/u.test(getText(root));
|
|
447
|
+
var createOptionSelectionRuntime = (root, selectorConfig) => ({
|
|
448
|
+
requiresSelection() {
|
|
449
|
+
if (buildSelectionRequired(root)) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
return detectGroups(root, selectorConfig).length > 0;
|
|
453
|
+
},
|
|
454
|
+
buildSelectionPlan(input) {
|
|
455
|
+
const detectedGroups = detectGroups(root, selectorConfig);
|
|
456
|
+
if (detectedGroups.length === 0) {
|
|
457
|
+
if (buildSelectionRequired(root)) {
|
|
458
|
+
throw new Error("options_dom_unrecognized");
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
generatedAt: new Date().toISOString(),
|
|
462
|
+
...input?.preferences ? { preferences: input.preferences } : {},
|
|
463
|
+
groups: []
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
generatedAt: new Date().toISOString(),
|
|
468
|
+
...input?.preferences ? { preferences: input.preferences } : {},
|
|
469
|
+
groups: detectedGroups.map((group) => toPlanGroup(group, input?.preferences, input?.existingPlan))
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
async applySelectionPlan(plan) {
|
|
473
|
+
if (plan.groups.length === 0) {
|
|
474
|
+
return plan;
|
|
475
|
+
}
|
|
476
|
+
const liveGroups = detectGroups(root, selectorConfig);
|
|
477
|
+
if (liveGroups.length === 0) {
|
|
478
|
+
throw new Error("options_dom_unrecognized");
|
|
479
|
+
}
|
|
480
|
+
const updatedGroups = [];
|
|
481
|
+
for (const planGroup of plan.groups) {
|
|
482
|
+
const detectedGroup = liveGroups.find((candidate) => candidate.key === planGroup.key) ?? liveGroups.find((candidate) => candidate.kind === planGroup.kind && normalizeText(candidate.label) === normalizeText(planGroup.label));
|
|
483
|
+
if (!detectedGroup) {
|
|
484
|
+
throw new Error("options_dom_unrecognized");
|
|
485
|
+
}
|
|
486
|
+
updatedGroups.push(await applyPlanGroup(detectedGroup, planGroup));
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
...plan,
|
|
490
|
+
groups: updatedGroups
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
var createDocumentOptionSelectionRuntime = (document2, selectorConfig) => createOptionSelectionRuntime({
|
|
495
|
+
querySelector(selector) {
|
|
496
|
+
return document2.querySelector(selector);
|
|
497
|
+
},
|
|
498
|
+
querySelectorAll(selector) {
|
|
499
|
+
return document2.querySelectorAll(selector);
|
|
500
|
+
},
|
|
501
|
+
textContent: document2.body?.innerText ?? document2.body?.textContent ?? ""
|
|
502
|
+
}, selectorConfig);
|
|
503
|
+
|
|
504
|
+
// extension/content/order-state.ts
|
|
505
|
+
var chromeApi = globalThis.chrome;
|
|
506
|
+
var storageArea = chromeApi?.storage?.session ?? chromeApi?.storage?.local;
|
|
507
|
+
var ORDER_TRANSITIONS = {
|
|
508
|
+
idle: ["navigating", "aborted"],
|
|
509
|
+
navigating: ["product_loaded", "aborted"],
|
|
510
|
+
product_loaded: ["selecting_options", "aborted"],
|
|
511
|
+
selecting_options: ["option_selected", "aborted"],
|
|
512
|
+
option_selected: ["cart_added", "aborted"],
|
|
513
|
+
cart_added: ["checkout_loaded", "aborted"],
|
|
514
|
+
checkout_loaded: ["awaiting_user_confirm", "completed_dry_run", "aborted"],
|
|
515
|
+
awaiting_user_confirm: ["completed_dry_run", "aborted"],
|
|
516
|
+
aborted: [],
|
|
517
|
+
completed_dry_run: []
|
|
518
|
+
};
|
|
519
|
+
var createInitialCheckpoint = (request, observedAt = new Date().toISOString()) => ({
|
|
520
|
+
orderId: request.orderId,
|
|
521
|
+
platform: request.platform,
|
|
522
|
+
state: "idle",
|
|
523
|
+
productUrl: request.productUrl,
|
|
524
|
+
...typeof request.maxPriceKrw === "number" ? { maxPriceKrw: request.maxPriceKrw } : {},
|
|
525
|
+
...typeof request.tabId === "number" ? { tabId: request.tabId } : {},
|
|
526
|
+
dryRun: request.dryRun,
|
|
527
|
+
observedAt
|
|
528
|
+
});
|
|
529
|
+
var transitionOrderCheckpoint = (current, nextState, patch = {}, observedAt = new Date().toISOString()) => {
|
|
530
|
+
if (current.state !== nextState && !ORDER_TRANSITIONS[current.state].includes(nextState)) {
|
|
531
|
+
throw new Error(`Invalid order transition: ${current.state} -> ${nextState}`);
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
...current,
|
|
535
|
+
...patch,
|
|
536
|
+
state: nextState,
|
|
537
|
+
observedAt
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
var createStorageArea = () => storageArea;
|
|
541
|
+
var readOrderCheckpoint = async (storage, orderId) => {
|
|
542
|
+
const key = getOrderCheckpointStorageKey(orderId);
|
|
543
|
+
const stored = await storage.get(key);
|
|
544
|
+
return stored[key];
|
|
545
|
+
};
|
|
546
|
+
var writeOrderCheckpoint = async (storage, checkpoint) => {
|
|
547
|
+
const { resumeState: _resumeState, ...persisted } = checkpoint;
|
|
548
|
+
await storage.set({
|
|
549
|
+
[getOrderCheckpointStorageKey(checkpoint.orderId)]: persisted
|
|
550
|
+
});
|
|
551
|
+
return persisted;
|
|
552
|
+
};
|
|
553
|
+
var resumeOrderCheckpoint = async (storage, request, observedAt = new Date().toISOString()) => {
|
|
554
|
+
const existing = await readOrderCheckpoint(storage, request.orderId);
|
|
555
|
+
if (existing) {
|
|
556
|
+
return existing;
|
|
557
|
+
}
|
|
558
|
+
const created = createInitialCheckpoint(request, observedAt);
|
|
559
|
+
await writeOrderCheckpoint(storage, created);
|
|
560
|
+
return created;
|
|
561
|
+
};
|
|
562
|
+
var toRuntimeFailureReason = (error) => error instanceof Error ? error.message : typeof error === "string" ? error : "The Coupang dry-run flow failed unexpectedly.";
|
|
563
|
+
var createAbortedOrderCheckpoint = (checkpoint, reason, observedAt = new Date().toISOString()) => transitionOrderCheckpoint(checkpoint, "aborted", {
|
|
564
|
+
reason,
|
|
565
|
+
detail: reason,
|
|
566
|
+
lastError: reason
|
|
567
|
+
}, observedAt);
|
|
568
|
+
|
|
569
|
+
// extension/content/coupang-order.ts
|
|
570
|
+
var COUPANG_CART_URL = "https://cart.coupang.com/cartView.pang";
|
|
571
|
+
var BUTTON_LIKE_SELECTOR = 'button, a, [role="button"], label';
|
|
572
|
+
var wait2 = async (ms) => {
|
|
573
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
574
|
+
};
|
|
575
|
+
var checkpointPatchFromProbe = (request, checkpoint, probe) => ({
|
|
576
|
+
productUrl: request.productUrl,
|
|
577
|
+
pageUrl: probe.url,
|
|
578
|
+
...probe.product?.title ? { productTitle: probe.product.title } : {},
|
|
579
|
+
...typeof probe.product?.priceKrw === "number" ? { priceKrw: probe.product.priceKrw } : {},
|
|
580
|
+
...probe.checkout?.totalPriceText ? { totalPriceText: probe.checkout.totalPriceText } : {},
|
|
581
|
+
...typeof probe.checkout?.totalPriceKrw === "number" ? { totalPriceKrw: probe.checkout.totalPriceKrw } : {},
|
|
582
|
+
...probe.selectorsVersion ? { selectorsVersion: probe.selectorsVersion } : {},
|
|
583
|
+
...typeof request.maxPriceKrw === "number" ? { maxPriceKrw: request.maxPriceKrw } : {},
|
|
584
|
+
...typeof checkpoint.tabId === "number" ? { tabId: checkpoint.tabId } : {},
|
|
585
|
+
dryRun: request.dryRun
|
|
586
|
+
});
|
|
587
|
+
var persistAndEmitStatus = async (dependencies, checkpoint) => {
|
|
588
|
+
const persisted = await dependencies.persistCheckpoint(checkpoint);
|
|
589
|
+
await dependencies.emitStatus(persisted);
|
|
590
|
+
return persisted;
|
|
591
|
+
};
|
|
592
|
+
var abortOrder = async (dependencies, checkpoint, message) => {
|
|
593
|
+
const aborted = createAbortedOrderCheckpoint(checkpoint, message);
|
|
594
|
+
return await persistAndEmitStatus(dependencies, aborted);
|
|
595
|
+
};
|
|
596
|
+
var runCoupangDryRunFlow = async (dependencies) => {
|
|
597
|
+
const { adapter, request } = dependencies;
|
|
598
|
+
let checkpoint = dependencies.initialCheckpoint;
|
|
599
|
+
try {
|
|
600
|
+
while (true) {
|
|
601
|
+
const probe = adapter.probe();
|
|
602
|
+
const patch = checkpointPatchFromProbe(request, checkpoint, probe);
|
|
603
|
+
switch (checkpoint.state) {
|
|
604
|
+
case "idle": {
|
|
605
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "navigating", {
|
|
606
|
+
...patch,
|
|
607
|
+
detail: "Navigating to Coupang product page."
|
|
608
|
+
}));
|
|
609
|
+
if (adapter.getUrl() !== request.productUrl) {
|
|
610
|
+
await adapter.navigate(request.productUrl);
|
|
611
|
+
return checkpoint;
|
|
612
|
+
}
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
case "navigating": {
|
|
616
|
+
if (!probe.loggedIn) {
|
|
617
|
+
return await abortOrder(dependencies, checkpoint, "Coupang login is required before starting a dry-run order.");
|
|
618
|
+
}
|
|
619
|
+
if (probe.pageKind !== "product") {
|
|
620
|
+
if (adapter.getUrl() !== request.productUrl) {
|
|
621
|
+
await adapter.navigate(request.productUrl);
|
|
622
|
+
}
|
|
623
|
+
return checkpoint;
|
|
624
|
+
}
|
|
625
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "product_loaded", {
|
|
626
|
+
...patch,
|
|
627
|
+
detail: "Loaded product context from the Coupang product page."
|
|
628
|
+
}));
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
case "product_loaded": {
|
|
632
|
+
if (!probe.loggedIn) {
|
|
633
|
+
return await abortOrder(dependencies, checkpoint, "Coupang login is required before continuing.");
|
|
634
|
+
}
|
|
635
|
+
if (typeof request.maxPriceKrw === "number" && typeof probe.product?.priceKrw === "number" && probe.product.priceKrw > request.maxPriceKrw) {
|
|
636
|
+
return await abortOrder(dependencies, checkpoint, `Product price ${probe.product.priceKrw} KRW exceeds the maxPriceKrw guardrail of ${request.maxPriceKrw}.`);
|
|
637
|
+
}
|
|
638
|
+
const selectionPlan = adapter.buildSelectionPlan({
|
|
639
|
+
existingPlan: checkpoint.selectionPlan,
|
|
640
|
+
preferences: request.selectionPreferences
|
|
641
|
+
});
|
|
642
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "selecting_options", {
|
|
643
|
+
...patch,
|
|
644
|
+
selectionPlan,
|
|
645
|
+
detail: adapter.requiresOptionSelection() ? "Preparing required product option selections." : "Checking whether product options need to be selected."
|
|
646
|
+
}));
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
case "selecting_options": {
|
|
650
|
+
const selectionPlan = adapter.buildSelectionPlan({
|
|
651
|
+
existingPlan: checkpoint.selectionPlan,
|
|
652
|
+
preferences: request.selectionPreferences
|
|
653
|
+
});
|
|
654
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "selecting_options", {
|
|
655
|
+
...patch,
|
|
656
|
+
selectionPlan,
|
|
657
|
+
detail: "Selecting product options for the dry-run."
|
|
658
|
+
}));
|
|
659
|
+
const appliedPlan = await adapter.applySelectionPlan(selectionPlan);
|
|
660
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "option_selected", {
|
|
661
|
+
...patch,
|
|
662
|
+
selectionPlan: appliedPlan,
|
|
663
|
+
detail: appliedPlan.groups.length > 0 ? "Selected product options for the dry-run." : "No product option selection was required for this dry-run."
|
|
664
|
+
}));
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
case "option_selected": {
|
|
668
|
+
await adapter.addToCart();
|
|
669
|
+
await wait2(400);
|
|
670
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "cart_added", {
|
|
671
|
+
...patch,
|
|
672
|
+
detail: "Added the product to the cart."
|
|
673
|
+
}));
|
|
674
|
+
await adapter.navigate(COUPANG_CART_URL);
|
|
675
|
+
return checkpoint;
|
|
676
|
+
}
|
|
677
|
+
case "cart_added": {
|
|
678
|
+
if (probe.pageKind !== "cart" && probe.pageKind !== "checkout") {
|
|
679
|
+
await adapter.navigate(COUPANG_CART_URL);
|
|
680
|
+
return checkpoint;
|
|
681
|
+
}
|
|
682
|
+
if (probe.pageKind === "cart") {
|
|
683
|
+
await adapter.selectCartItems();
|
|
684
|
+
await adapter.startCheckout();
|
|
685
|
+
return checkpoint;
|
|
686
|
+
}
|
|
687
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "checkout_loaded", {
|
|
688
|
+
...patch,
|
|
689
|
+
detail: "Reached the Coupang checkout page."
|
|
690
|
+
}));
|
|
691
|
+
await dependencies.emitCheckpoint(checkpoint);
|
|
692
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "awaiting_user_confirm", {
|
|
693
|
+
...patch,
|
|
694
|
+
detail: "Dry-run reached checkout and is awaiting future approval wiring."
|
|
695
|
+
}));
|
|
696
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "completed_dry_run", {
|
|
697
|
+
...patch,
|
|
698
|
+
detail: "Dry-run completed at the checkout checkpoint without submitting payment."
|
|
699
|
+
}));
|
|
700
|
+
return checkpoint;
|
|
701
|
+
}
|
|
702
|
+
case "checkout_loaded": {
|
|
703
|
+
await dependencies.emitCheckpoint(checkpoint);
|
|
704
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "awaiting_user_confirm", {
|
|
705
|
+
detail: checkpoint.detail ?? "Checkout checkpoint reached."
|
|
706
|
+
}));
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
case "awaiting_user_confirm": {
|
|
710
|
+
checkpoint = await persistAndEmitStatus(dependencies, transitionOrderCheckpoint(checkpoint, "completed_dry_run", {
|
|
711
|
+
detail: checkpoint.detail ?? "Dry-run complete."
|
|
712
|
+
}));
|
|
713
|
+
return checkpoint;
|
|
714
|
+
}
|
|
715
|
+
case "aborted":
|
|
716
|
+
case "completed_dry_run":
|
|
717
|
+
return checkpoint;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
} catch (error) {
|
|
721
|
+
if (!isTerminalOrderState(checkpoint.state)) {
|
|
722
|
+
await abortOrder(dependencies, checkpoint, toRuntimeFailureReason(error));
|
|
723
|
+
}
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
var isVisible = (element) => {
|
|
728
|
+
const candidate = element;
|
|
729
|
+
if (candidate.hidden) {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
const style = typeof window !== "undefined" && typeof window.getComputedStyle === "function" ? window.getComputedStyle(candidate) : undefined;
|
|
733
|
+
const rect = typeof candidate.getBoundingClientRect === "function" ? candidate.getBoundingClientRect() : undefined;
|
|
734
|
+
return !candidate.hasAttribute?.("hidden") && style?.display !== "none" && style?.visibility !== "hidden" && style?.opacity !== "0" && (rect ? rect.width > 0 && rect.height > 0 : true);
|
|
735
|
+
};
|
|
736
|
+
var queryFirstVisibleBySelectorGroup = (document2, selectors) => {
|
|
737
|
+
for (const selector of selectors) {
|
|
738
|
+
const match = document2.querySelector(selector);
|
|
739
|
+
if (match && isVisible(match)) {
|
|
740
|
+
return match;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
};
|
|
745
|
+
var queryFirstVisibleByText = (document2, pattern, selector = BUTTON_LIKE_SELECTOR) => {
|
|
746
|
+
const elements = document2.querySelectorAll(selector);
|
|
747
|
+
for (const element of elements) {
|
|
748
|
+
if (!isVisible(element)) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const text = normalizeText(element.textContent ?? "");
|
|
752
|
+
if (text && pattern.test(text)) {
|
|
753
|
+
return element;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return;
|
|
757
|
+
};
|
|
758
|
+
var clickElement = async (element, errorMessage) => {
|
|
759
|
+
if (!element) {
|
|
760
|
+
throw new Error(errorMessage);
|
|
761
|
+
}
|
|
762
|
+
element.click();
|
|
763
|
+
await wait2(250);
|
|
764
|
+
};
|
|
765
|
+
var createDocumentOrderRuntimeAdapter = (document2, location, selectorConfig = BUNDLED_COUPANG_SELECTOR_CONFIG) => {
|
|
766
|
+
const optionRuntime = createDocumentOptionSelectionRuntime(document2, selectorConfig);
|
|
767
|
+
return {
|
|
768
|
+
getUrl: () => location.href,
|
|
769
|
+
async navigate(url) {
|
|
770
|
+
if (location.href !== url) {
|
|
771
|
+
location.href = url;
|
|
772
|
+
await wait2(50);
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
probe() {
|
|
776
|
+
return buildCoupangProbe(location.href, createDocumentProbeAdapter(document2), { selectorConfig });
|
|
777
|
+
},
|
|
778
|
+
requiresOptionSelection() {
|
|
779
|
+
return optionRuntime.requiresSelection();
|
|
780
|
+
},
|
|
781
|
+
buildSelectionPlan(input) {
|
|
782
|
+
return optionRuntime.buildSelectionPlan(input);
|
|
783
|
+
},
|
|
784
|
+
async applySelectionPlan(plan) {
|
|
785
|
+
return await optionRuntime.applySelectionPlan(plan);
|
|
786
|
+
},
|
|
787
|
+
async addToCart() {
|
|
788
|
+
const button = queryFirstVisibleBySelectorGroup(document2, selectorConfig.selectors.addToCartButton) ?? queryFirstVisibleByText(document2, /장바구니\s*담기|카트\s*담기/u);
|
|
789
|
+
await clickElement(button, "Unable to find the Coupang add-to-cart button.");
|
|
790
|
+
},
|
|
791
|
+
async selectCartItems() {
|
|
792
|
+
const selectAll = queryFirstVisibleByText(document2, /전체\s*선택/u);
|
|
793
|
+
if (!selectAll) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const ariaChecked = selectAll.getAttribute("aria-checked");
|
|
797
|
+
if (selectAll instanceof HTMLInputElement && selectAll.checked) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (ariaChecked === "true") {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
await clickElement(selectAll, "Unable to toggle the Coupang cart selection state.");
|
|
804
|
+
},
|
|
805
|
+
async startCheckout() {
|
|
806
|
+
const button = queryFirstVisibleBySelectorGroup(document2, selectorConfig.selectors.cartCheckoutButton) ?? queryFirstVisibleByText(document2, /^주문하기$/u);
|
|
807
|
+
await clickElement(button, "Unable to find the Coupang cart order button.");
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// extension/content/coupang.ts
|
|
813
|
+
var createRequestId = () => globalThis.crypto?.randomUUID?.() ?? `req_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
814
|
+
var sendBackgroundEnvelope = async (envelope) => {
|
|
815
|
+
try {
|
|
816
|
+
await chrome.runtime.sendMessage(envelope);
|
|
817
|
+
} catch {}
|
|
818
|
+
};
|
|
819
|
+
var resolveSelectorConfig = (selectorConfig) => selectorConfig?.config ?? BUNDLED_COUPANG_SELECTOR_CONFIG;
|
|
820
|
+
var createProbeResponse = (message) => {
|
|
821
|
+
try {
|
|
822
|
+
return createEnvelope("coupang:probe-result", {
|
|
823
|
+
probe: buildCoupangProbe(window.location.href, createDocumentProbeAdapter(document), {
|
|
824
|
+
selectorConfig: resolveSelectorConfig(message.payload.selectorConfig)
|
|
825
|
+
})
|
|
826
|
+
}, {
|
|
827
|
+
source: "extension:content",
|
|
828
|
+
messageId: createRequestId(),
|
|
829
|
+
requestId: message.messageId
|
|
830
|
+
});
|
|
831
|
+
} catch (error) {
|
|
832
|
+
return createErrorEnvelope({
|
|
833
|
+
code: "internal_error",
|
|
834
|
+
message: error instanceof Error ? error.message : "Failed to inspect the Coupang page."
|
|
835
|
+
}, {
|
|
836
|
+
source: "extension:content",
|
|
837
|
+
messageId: createRequestId(),
|
|
838
|
+
requestId: message.messageId
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
var runOrder = async (message) => {
|
|
843
|
+
const storage = createStorageArea();
|
|
844
|
+
if (!storage) {
|
|
845
|
+
throw new Error("chrome.storage.session is unavailable in the Coupang content script.");
|
|
846
|
+
}
|
|
847
|
+
const checkpoint = await resumeOrderCheckpoint(storage, message.payload, message.sentAt);
|
|
848
|
+
const emit = async (type, nextCheckpoint) => {
|
|
849
|
+
await sendBackgroundEnvelope(createEnvelope(type, nextCheckpoint, {
|
|
850
|
+
source: "extension:content",
|
|
851
|
+
messageId: createRequestId(),
|
|
852
|
+
requestId: message.messageId
|
|
853
|
+
}));
|
|
854
|
+
};
|
|
855
|
+
await runCoupangDryRunFlow({
|
|
856
|
+
request: message.payload,
|
|
857
|
+
initialCheckpoint: checkpoint,
|
|
858
|
+
adapter: createDocumentOrderRuntimeAdapter(document, window.location, resolveSelectorConfig(message.payload.selectorConfig)),
|
|
859
|
+
persistCheckpoint: async (nextCheckpoint) => await writeOrderCheckpoint(storage, nextCheckpoint),
|
|
860
|
+
emitStatus: async (nextCheckpoint) => await emit("order:status", nextCheckpoint),
|
|
861
|
+
emitCheckpoint: async (nextCheckpoint) => await emit("order:checkpoint", nextCheckpoint)
|
|
862
|
+
});
|
|
863
|
+
};
|
|
864
|
+
var createAck = (message) => createEnvelope("content:ack", {
|
|
865
|
+
orderId: message.payload.orderId,
|
|
866
|
+
ok: true
|
|
867
|
+
}, {
|
|
868
|
+
source: "extension:content",
|
|
869
|
+
messageId: createRequestId(),
|
|
870
|
+
requestId: message.messageId
|
|
871
|
+
});
|
|
872
|
+
chrome.runtime.onMessage.addListener((rawMessage, _sender, sendResponse) => {
|
|
873
|
+
const decoded = decodeEnvelope(rawMessage, {
|
|
874
|
+
source: "extension:content",
|
|
875
|
+
messageId: createRequestId()
|
|
876
|
+
});
|
|
877
|
+
if (!decoded.ok) {
|
|
878
|
+
sendResponse(decoded.error);
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
const message = decoded.envelope;
|
|
882
|
+
if (message.type === "coupang:probe") {
|
|
883
|
+
sendResponse(createProbeResponse(message));
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
if (message.type === "order:start" || message.type === "order:resume") {
|
|
887
|
+
runOrder(message).catch(async (error) => {
|
|
888
|
+
const reason = error instanceof Error ? error.message : "Failed to run the Coupang dry-run order flow.";
|
|
889
|
+
await sendBackgroundEnvelope(createErrorEnvelope({
|
|
890
|
+
code: "internal_error",
|
|
891
|
+
message: reason,
|
|
892
|
+
details: {
|
|
893
|
+
orderId: message.payload.orderId,
|
|
894
|
+
reason
|
|
895
|
+
}
|
|
896
|
+
}, {
|
|
897
|
+
source: "extension:content",
|
|
898
|
+
messageId: createRequestId(),
|
|
899
|
+
requestId: message.requestId ?? message.messageId
|
|
900
|
+
}));
|
|
901
|
+
});
|
|
902
|
+
sendResponse(createAck(message));
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
return false;
|
|
906
|
+
});
|
|
907
|
+
sendBackgroundEnvelope(createEnvelope("content:ready", {
|
|
908
|
+
platform: "coupang",
|
|
909
|
+
url: window.location.href
|
|
910
|
+
}, {
|
|
911
|
+
source: "extension:content",
|
|
912
|
+
messageId: createRequestId()
|
|
913
|
+
}));
|
|
914
|
+
})();
|