@drip-apex/sdk 0.1.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.
Files changed (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/_headers +3 -0
  4. package/dist/bundle-report.json +564 -0
  5. package/dist/drip.debug.js +2 -0
  6. package/dist/drip.debug.js.map +1 -0
  7. package/dist/drip.heatmap.js +2 -0
  8. package/dist/drip.heatmap.js.map +1 -0
  9. package/dist/drip.js +2 -0
  10. package/dist/drip.js.map +1 -0
  11. package/dist/drip.live.js +2 -0
  12. package/dist/drip.live.js.map +1 -0
  13. package/dist/editor.js +480 -0
  14. package/dist/editor.js.map +7 -0
  15. package/dist/index.mjs +645 -0
  16. package/dist/index.mjs.map +7 -0
  17. package/dist/metafiles/drip.debug.meta.json +973 -0
  18. package/dist/metafiles/drip.heatmap.meta.json +918 -0
  19. package/dist/metafiles/drip.live.meta.json +918 -0
  20. package/dist/metafiles/drip.meta.json +973 -0
  21. package/dist/metafiles/editor.meta.json +321 -0
  22. package/dist/metafiles/index.meta.json +1039 -0
  23. package/dist/metafiles/index.mjs.meta.json +78 -0
  24. package/dist/react/index.js +16973 -0
  25. package/dist/react/index.js.map +7 -0
  26. package/dist/sri.json +27 -0
  27. package/dist/types/analytics.d.ts +15 -0
  28. package/dist/types/antiflicker.d.ts +14 -0
  29. package/dist/types/assignment-store.d.ts +30 -0
  30. package/dist/types/audit.d.ts +9 -0
  31. package/dist/types/bucket.d.ts +2 -0
  32. package/dist/types/bucketing-cleanup.d.ts +5 -0
  33. package/dist/types/commerce-gift.d.ts +41 -0
  34. package/dist/types/commerce-price.d.ts +22 -0
  35. package/dist/types/commerce-shipping.d.ts +28 -0
  36. package/dist/types/css.d.ts +5 -0
  37. package/dist/types/cwv.d.ts +10 -0
  38. package/dist/types/data-layer-normalize.d.ts +1 -0
  39. package/dist/types/debug.d.ts +23 -0
  40. package/dist/types/define.d.ts +87 -0
  41. package/dist/types/devtools.d.ts +32 -0
  42. package/dist/types/dom-trigger.d.ts +3 -0
  43. package/dist/types/editor/api.d.ts +37 -0
  44. package/dist/types/editor/artifacts.d.ts +15 -0
  45. package/dist/types/editor/command-runtime.d.ts +48 -0
  46. package/dist/types/editor/controller.d.ts +97 -0
  47. package/dist/types/editor/device-preview.d.ts +23 -0
  48. package/dist/types/editor/index.d.ts +9 -0
  49. package/dist/types/editor/operator.d.ts +39 -0
  50. package/dist/types/editor/picker.d.ts +23 -0
  51. package/dist/types/editor/selector.d.ts +2 -0
  52. package/dist/types/editor/types.d.ts +55 -0
  53. package/dist/types/editor/view-kit.d.ts +33 -0
  54. package/dist/types/editor/view.d.ts +5 -0
  55. package/dist/types/editor-bootstrap.d.ts +37 -0
  56. package/dist/types/exclusion-groups.d.ts +59 -0
  57. package/dist/types/exposure-signals.d.ts +7 -0
  58. package/dist/types/flags.d.ts +65 -0
  59. package/dist/types/goals.d.ts +16 -0
  60. package/dist/types/guards.d.ts +39 -0
  61. package/dist/types/handover.d.ts +10 -0
  62. package/dist/types/hash.d.ts +19 -0
  63. package/dist/types/heatmap.d.ts +8 -0
  64. package/dist/types/hideflicker.d.ts +15 -0
  65. package/dist/types/hydration.d.ts +11 -0
  66. package/dist/types/index.d.ts +144 -0
  67. package/dist/types/js.d.ts +6 -0
  68. package/dist/types/module.d.ts +4 -0
  69. package/dist/types/money.d.ts +14 -0
  70. package/dist/types/mutate.d.ts +34 -0
  71. package/dist/types/observer-mute.d.ts +3 -0
  72. package/dist/types/pii-keys.d.ts +1 -0
  73. package/dist/types/prerequisites.d.ts +12 -0
  74. package/dist/types/qa-assistant-state.d.ts +68 -0
  75. package/dist/types/qa-assistant-view.d.ts +6 -0
  76. package/dist/types/qa-assistant.d.ts +9 -0
  77. package/dist/types/qa-toolbar.d.ts +19 -0
  78. package/dist/types/react/index.d.ts +84 -0
  79. package/dist/types/reassert-sentinel.d.ts +6 -0
  80. package/dist/types/router.d.ts +9 -0
  81. package/dist/types/runtime-tools.d.ts +50 -0
  82. package/dist/types/runtime.d.ts +22 -0
  83. package/dist/types/safe-regex.d.ts +6 -0
  84. package/dist/types/sanitize.d.ts +5 -0
  85. package/dist/types/semantic.d.ts +7 -0
  86. package/dist/types/shopify-cart-events.d.ts +7 -0
  87. package/dist/types/shopify-cart.d.ts +62 -0
  88. package/dist/types/signals.d.ts +31 -0
  89. package/dist/types/surface-signals.d.ts +6 -0
  90. package/dist/types/targeting-diagnostics.d.ts +5 -0
  91. package/dist/types/targeting.d.ts +50 -0
  92. package/dist/types/test-fixtures/commerce.d.ts +12 -0
  93. package/dist/types/track.d.ts +79 -0
  94. package/dist/types/types.d.ts +859 -0
  95. package/dist/types/wait.d.ts +7 -0
  96. package/package.json +74 -0
  97. package/src/analytics.ts +146 -0
  98. package/src/antiflicker.test.ts +127 -0
  99. package/src/antiflicker.ts +147 -0
  100. package/src/assignment-store.test.ts +171 -0
  101. package/src/assignment-store.ts +208 -0
  102. package/src/audit.test.ts +17 -0
  103. package/src/audit.ts +362 -0
  104. package/src/bucket.test.ts +65 -0
  105. package/src/bucket.ts +18 -0
  106. package/src/bucketing-cleanup.test.ts +30 -0
  107. package/src/bucketing-cleanup.ts +72 -0
  108. package/src/bucketing-parity.test.ts +41 -0
  109. package/src/commerce-analytics-lifecycle.test.ts +1416 -0
  110. package/src/commerce-config-lifecycle.test.ts +274 -0
  111. package/src/commerce-epoch-lifecycle.test.ts +62 -0
  112. package/src/commerce-gift.test.ts +1293 -0
  113. package/src/commerce-gift.ts +979 -0
  114. package/src/commerce-offers-display.test.ts +154 -0
  115. package/src/commerce-price.test.ts +1145 -0
  116. package/src/commerce-price.ts +941 -0
  117. package/src/commerce-revocation-lifecycle.test.ts +78 -0
  118. package/src/commerce-shipping.test.ts +630 -0
  119. package/src/commerce-shipping.ts +447 -0
  120. package/src/config-fetch.test.ts +325 -0
  121. package/src/css.ts +54 -0
  122. package/src/cwv.test.ts +95 -0
  123. package/src/cwv.ts +129 -0
  124. package/src/data-layer-normalize.ts +21 -0
  125. package/src/debug.test.ts +83 -0
  126. package/src/debug.ts +138 -0
  127. package/src/define.test.ts +327 -0
  128. package/src/define.ts +208 -0
  129. package/src/devtools.ts +582 -0
  130. package/src/dom-trigger.test.ts +70 -0
  131. package/src/dom-trigger.ts +55 -0
  132. package/src/editor/api.test.ts +154 -0
  133. package/src/editor/api.ts +185 -0
  134. package/src/editor/artifacts.test.ts +57 -0
  135. package/src/editor/artifacts.ts +115 -0
  136. package/src/editor/command-runtime.test.ts +225 -0
  137. package/src/editor/command-runtime.ts +572 -0
  138. package/src/editor/controller.test.ts +371 -0
  139. package/src/editor/controller.ts +596 -0
  140. package/src/editor/device-preview.test.ts +60 -0
  141. package/src/editor/device-preview.ts +120 -0
  142. package/src/editor/index.test.ts +161 -0
  143. package/src/editor/index.ts +169 -0
  144. package/src/editor/operator.test.ts +148 -0
  145. package/src/editor/operator.ts +256 -0
  146. package/src/editor/picker.test.ts +58 -0
  147. package/src/editor/picker.ts +152 -0
  148. package/src/editor/selector.test.ts +44 -0
  149. package/src/editor/selector.ts +110 -0
  150. package/src/editor/types.ts +92 -0
  151. package/src/editor/view-kit.ts +162 -0
  152. package/src/editor/view.test.ts +535 -0
  153. package/src/editor/view.ts +1395 -0
  154. package/src/editor-bootstrap.test.ts +116 -0
  155. package/src/editor-bootstrap.ts +213 -0
  156. package/src/event-schema-contract.test.ts +35 -0
  157. package/src/exclusion-groups.test.ts +383 -0
  158. package/src/exclusion-groups.ts +270 -0
  159. package/src/exposure-signals.test.ts +182 -0
  160. package/src/exposure-signals.ts +110 -0
  161. package/src/flags.test.ts +322 -0
  162. package/src/flags.ts +201 -0
  163. package/src/goals.test.ts +42 -0
  164. package/src/goals.ts +200 -0
  165. package/src/guards.test.ts +249 -0
  166. package/src/guards.ts +250 -0
  167. package/src/handover-cache-order.test.ts +32 -0
  168. package/src/handover.test.ts +349 -0
  169. package/src/handover.ts +309 -0
  170. package/src/hash.ts +214 -0
  171. package/src/heatmap.test.ts +305 -0
  172. package/src/heatmap.ts +373 -0
  173. package/src/hideflicker.ts +21 -0
  174. package/src/hydration.test.ts +252 -0
  175. package/src/hydration.ts +203 -0
  176. package/src/index.ts +7513 -0
  177. package/src/init-empty.test.ts +5572 -0
  178. package/src/js.test.ts +26 -0
  179. package/src/js.ts +61 -0
  180. package/src/module.ts +50 -0
  181. package/src/money.test.ts +35 -0
  182. package/src/money.ts +53 -0
  183. package/src/mutate.test.ts +304 -0
  184. package/src/mutate.ts +469 -0
  185. package/src/observer-mute.test.ts +88 -0
  186. package/src/observer-mute.ts +45 -0
  187. package/src/page-trigger.test.ts +84 -0
  188. package/src/personalization.test.ts +68 -0
  189. package/src/pii-keys.ts +17 -0
  190. package/src/prerequisites.test.ts +39 -0
  191. package/src/prerequisites.ts +94 -0
  192. package/src/qa-assistant-state.test.ts +471 -0
  193. package/src/qa-assistant-state.ts +505 -0
  194. package/src/qa-assistant-view.test.ts +185 -0
  195. package/src/qa-assistant-view.ts +768 -0
  196. package/src/qa-assistant.ts +40 -0
  197. package/src/qa-toolbar.ts +444 -0
  198. package/src/react/index.test.tsx +177 -0
  199. package/src/react/index.tsx +252 -0
  200. package/src/reassert-sentinel.test.ts +314 -0
  201. package/src/reassert-sentinel.ts +191 -0
  202. package/src/router.test.ts +78 -0
  203. package/src/router.ts +92 -0
  204. package/src/runtime-tools.test.ts +112 -0
  205. package/src/runtime-tools.ts +403 -0
  206. package/src/runtime.test.ts +217 -0
  207. package/src/runtime.ts +268 -0
  208. package/src/safe-regex.ts +35 -0
  209. package/src/sanitize.ts +34 -0
  210. package/src/semantic.ts +245 -0
  211. package/src/shopify-cart-events.test.ts +76 -0
  212. package/src/shopify-cart-events.ts +115 -0
  213. package/src/shopify-cart.test.ts +2823 -0
  214. package/src/shopify-cart.ts +1256 -0
  215. package/src/signals.ts +226 -0
  216. package/src/surface-signals.test.ts +83 -0
  217. package/src/surface-signals.ts +165 -0
  218. package/src/targeting-diagnostics.ts +560 -0
  219. package/src/targeting.detailed.test.ts +443 -0
  220. package/src/targeting.test.ts +1054 -0
  221. package/src/targeting.ts +1024 -0
  222. package/src/test-fixtures/commerce.ts +65 -0
  223. package/src/track.test.ts +817 -0
  224. package/src/track.ts +519 -0
  225. package/src/types.ts +991 -0
  226. package/src/wait.test.ts +96 -0
  227. package/src/wait.ts +238 -0
@@ -0,0 +1,572 @@
1
+ import type { Mutation, MutationAction } from "../types";
2
+ import { EditorApi, EditorApiError } from "./api";
3
+ import type { ArtifactCommandAudit, EditorController } from "./controller";
4
+ import { generateDeterministicSelector } from "./selector";
5
+ import type {
6
+ ClaimedEditorCommand,
7
+ EditorArtifacts,
8
+ EditorBrowserCommand,
9
+ } from "./types";
10
+
11
+ const POLL_INTERVAL_MS = 900;
12
+ const MAX_LOGS = 80;
13
+ const REDACTED = "[redacted]";
14
+ const SENSITIVE_KEY = /(?:account|address|auth(?:orization)?|bearer|card|cookie|credential|customer|cvv|email|name|pass(?:word|wd)?|payment|phone|postal|secret|session|ssn|token|user|value|zip)/i;
15
+ const EMAIL = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
16
+ const JWT = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
17
+ const AUTH_VALUE = /\b(?:bearer|basic)\s+[A-Za-z0-9._~+\/-]+=*/gi;
18
+ const LONG_NUMBER = /\b(?:\+?\d[\d .()-]{6,}\d)\b/g;
19
+ const MUTATION_ACTIONS = new Set<MutationAction>([
20
+ "html", "text", "setText", "attribute", "style", "setStyle", "remove",
21
+ "insertBefore", "insertAfter",
22
+ ]);
23
+
24
+ export type EditorCommandResult = { ok: boolean; [key: string]: unknown };
25
+ type CommandExecution = {
26
+ result: EditorCommandResult;
27
+ afterAcknowledge?: () => void;
28
+ };
29
+
30
+ type NavigationActions = {
31
+ navigate(url: string): void;
32
+ reload(): void;
33
+ back(): void;
34
+ forward(): void;
35
+ };
36
+
37
+ function record(value: unknown): Record<string, unknown> | null {
38
+ return value && typeof value === "object" && !Array.isArray(value)
39
+ ? value as Record<string, unknown>
40
+ : null;
41
+ }
42
+
43
+ function string(value: unknown): string {
44
+ return typeof value === "string" ? value.trim() : "";
45
+ }
46
+
47
+ function isLoopbackOrigin(origin: string): boolean {
48
+ try {
49
+ const hostname = new URL(origin).hostname.toLowerCase();
50
+ return hostname === "localhost" || hostname.endsWith(".localhost") ||
51
+ hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function sanitizedUrl(value: string, base?: string): string {
58
+ try {
59
+ const url = new URL(value, base);
60
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
61
+ return `${url.protocol}${REDACTED}`;
62
+ }
63
+ url.username = "";
64
+ url.password = "";
65
+ url.search = "";
66
+ url.hash = "";
67
+ return url.toString();
68
+ } catch {
69
+ return REDACTED;
70
+ }
71
+ }
72
+
73
+ function sanitizeDiagnosticText(value: string): string {
74
+ return value
75
+ .replace(/https?:\/\/[^\s"'<>]+/gi, (url) => sanitizedUrl(url))
76
+ .replace(EMAIL, REDACTED)
77
+ .replace(JWT, REDACTED)
78
+ .replace(AUTH_VALUE, REDACTED)
79
+ .replace(LONG_NUMBER, REDACTED)
80
+ .replace(/\b(?:api[-_]?key|authorization|cookie|password|secret|token)\s*[:=]\s*[^\s,;]+/gi, (match) => {
81
+ const separator = match.includes(":") ? ":" : "=";
82
+ return `${match.split(separator, 1)[0]}${separator}${REDACTED}`;
83
+ });
84
+ }
85
+
86
+ function cloneSafe(value: unknown, depth = 0, key = ""): unknown {
87
+ if (key && SENSITIVE_KEY.test(key)) return REDACTED;
88
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
89
+ return typeof value === "string" ? sanitizeDiagnosticText(value).slice(0, 500) : value;
90
+ }
91
+ if (typeof value === "undefined") return null;
92
+ if (depth > 4) return "[truncated]";
93
+ if (value instanceof Element) return elementSummary(value);
94
+ if (Array.isArray(value)) return value.slice(0, 120).map((item) => cloneSafe(item, depth + 1));
95
+ if (typeof value === "object") {
96
+ const output: Record<string, unknown> = {};
97
+ for (const [entryKey, entry] of Object.entries(value as Record<string, unknown>).slice(0, 80)) {
98
+ try { output[entryKey] = cloneSafe(entry, depth + 1, entryKey); } catch {}
99
+ }
100
+ return output;
101
+ }
102
+ return `[${typeof value}]`;
103
+ }
104
+
105
+ function visibleText(element: Element | null): string {
106
+ if (!element) return "";
107
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement ||
108
+ element instanceof HTMLSelectElement || element instanceof HTMLFormElement ||
109
+ (element instanceof HTMLElement && element.isContentEditable)) {
110
+ return "";
111
+ }
112
+ return (element.textContent ?? "").trim().replace(/\s+/g, " ");
113
+ }
114
+
115
+ function elementSummary(element: Element | null): Record<string, unknown> | null {
116
+ if (!element) return null;
117
+ let rect: Record<string, number> | null = null;
118
+ try {
119
+ const box = element.getBoundingClientRect();
120
+ rect = {
121
+ x: Math.round(box.x), y: Math.round(box.y),
122
+ width: Math.round(box.width), height: Math.round(box.height),
123
+ };
124
+ } catch {}
125
+ return {
126
+ selector: generateDeterministicSelector(element),
127
+ tag: element.tagName.toLowerCase(),
128
+ text: sanitizeDiagnosticText(visibleText(element)).slice(0, 220) || null,
129
+ role: element.getAttribute("role"),
130
+ href: element instanceof HTMLAnchorElement ? sanitizedUrl(element.href) : null,
131
+ name: element.getAttribute("name"),
132
+ type: element.getAttribute("type"),
133
+ rect,
134
+ };
135
+ }
136
+
137
+ function sanitizeNetworkEntry(entry: Record<string, unknown>): Record<string, unknown> {
138
+ return {
139
+ ...cloneSafe(entry) as Record<string, unknown>,
140
+ ...(typeof entry.url === "string" ? { url: sanitizedUrl(entry.url) } : {}),
141
+ };
142
+ }
143
+
144
+ function findTarget(args: Record<string, unknown>): {
145
+ ok: boolean;
146
+ element?: Element;
147
+ mode?: string;
148
+ error?: string;
149
+ repairHint?: string;
150
+ } {
151
+ const selector = string(args.selector);
152
+ if (selector) {
153
+ try {
154
+ const element = document.querySelector(selector);
155
+ return element
156
+ ? { ok: true, element, mode: "selector" }
157
+ : {
158
+ ok: false,
159
+ error: "Selector target not found.",
160
+ repairHint: "Call browser_snapshot or browser_select_element to find a selector that exists on the live page.",
161
+ };
162
+ } catch {
163
+ return {
164
+ ok: false,
165
+ error: `Invalid selector: ${selector}`,
166
+ repairHint: "Pass visible text or a valid CSS selector.",
167
+ };
168
+ }
169
+ }
170
+ const query = string(args.text ?? args.selectorOrText ?? args.instruction ?? args.condition);
171
+ if (!query) return { ok: false, error: "Target not found" };
172
+ try {
173
+ const element = document.querySelector(query);
174
+ if (element) return { ok: true, element, mode: "selectorOrText" };
175
+ } catch {}
176
+ const lower = query.toLowerCase();
177
+ const candidates = Array.from(document.querySelectorAll(
178
+ "button,a,input,textarea,select,label,[role='button'],[onclick],[data-testid]",
179
+ )).slice(0, 350).filter((element) => {
180
+ const aria = `${element.getAttribute("aria-label") ?? ""} ${element.getAttribute("title") ?? ""} ${element.getAttribute("value") ?? ""}`.toLowerCase();
181
+ return visibleText(element).toLowerCase().includes(lower) || aria.includes(lower);
182
+ });
183
+ if (candidates.length === 1) return { ok: true, element: candidates[0], mode: "text" };
184
+ if (candidates.length > 1) {
185
+ return {
186
+ ok: false,
187
+ error: "Browser target text is ambiguous.",
188
+ repairHint: "Pass the exact selector from browser_snapshot/browser_select_element instead of loose text.",
189
+ };
190
+ }
191
+ return { ok: false, error: "Target not found" };
192
+ }
193
+
194
+ function parseMutation(value: unknown): Mutation | null {
195
+ const input = record(value);
196
+ const selector = string(input?.selector);
197
+ const action = input?.action;
198
+ if (!selector || typeof action !== "string" || !MUTATION_ACTIONS.has(action as MutationAction)) return null;
199
+ const mutation: Mutation = { selector, action: action as MutationAction };
200
+ if (typeof input?.html === "string") mutation.html = input.html;
201
+ if (typeof input?.text === "string") mutation.text = input.text;
202
+ const attribute = record(input?.attribute);
203
+ if (attribute && typeof attribute.name === "string" &&
204
+ (typeof attribute.value === "string" || attribute.value === null)) {
205
+ mutation.attribute = { name: attribute.name, value: attribute.value };
206
+ }
207
+ const attributes = record(input?.attributes);
208
+ if (attributes) {
209
+ mutation.attributes = Object.fromEntries(Object.entries(attributes).filter(
210
+ ([, entry]) => typeof entry === "string" || entry === null,
211
+ )) as Record<string, string | null>;
212
+ }
213
+ const styles = record(input?.styles);
214
+ if (styles) {
215
+ mutation.styles = Object.fromEntries(Object.entries(styles).filter(
216
+ ([, entry]) => typeof entry === "string",
217
+ )) as Record<string, string>;
218
+ }
219
+ return mutation;
220
+ }
221
+
222
+ function parseArtifacts(value: unknown): EditorArtifacts | null {
223
+ const input = record(value);
224
+ if (!input || !Array.isArray(input.mutations)) return null;
225
+ const mutations = input.mutations.map(parseMutation);
226
+ if (mutations.some((mutation) => mutation === null)) return null;
227
+ return {
228
+ mutations: mutations as Mutation[],
229
+ customCss: typeof input.customCss === "string" ? input.customCss : "",
230
+ customJs: typeof input.customJs === "string" ? input.customJs : "",
231
+ };
232
+ }
233
+
234
+ function auditResult(audit: ArtifactCommandAudit): EditorCommandResult {
235
+ return { ...audit };
236
+ }
237
+
238
+ export class EditorCommandExecutor {
239
+ private readonly consoleLogs: Array<Record<string, unknown>> = [];
240
+ private readonly networkLogs: Array<Record<string, unknown>> = [];
241
+ private restoreConsole: (() => void) | null = null;
242
+ private performanceObserver: PerformanceObserver | null = null;
243
+
244
+ constructor(
245
+ private readonly controller: EditorController,
246
+ private readonly navigation: NavigationActions = {
247
+ navigate: (url) => location.assign(url),
248
+ reload: () => location.reload(),
249
+ back: () => history.back(),
250
+ forward: () => history.forward(),
251
+ },
252
+ ) {}
253
+
254
+ start(): void {
255
+ if (this.restoreConsole) return;
256
+ const originals = new Map<"log" | "info" | "warn" | "error", (...args: unknown[]) => void>();
257
+ for (const level of ["log", "info", "warn", "error"] as const) {
258
+ const original = console[level];
259
+ originals.set(level, original);
260
+ console[level] = (...args: unknown[]) => {
261
+ this.consoleLogs.push({ level, args: cloneSafe(args), ts: Date.now() });
262
+ this.consoleLogs.splice(0, Math.max(0, this.consoleLogs.length - MAX_LOGS));
263
+ original.apply(console, args);
264
+ };
265
+ }
266
+ this.restoreConsole = () => {
267
+ for (const [level, original] of originals) console[level] = original;
268
+ this.restoreConsole = null;
269
+ };
270
+ if (typeof PerformanceObserver !== "undefined") {
271
+ try {
272
+ this.performanceObserver = new PerformanceObserver((list) => {
273
+ for (const entry of list.getEntries()) {
274
+ this.networkLogs.push({
275
+ url: entry.name,
276
+ type: entry.entryType,
277
+ startTime: Math.round(entry.startTime),
278
+ duration: Math.round(entry.duration),
279
+ });
280
+ }
281
+ this.networkLogs.splice(0, Math.max(0, this.networkLogs.length - MAX_LOGS));
282
+ });
283
+ this.performanceObserver.observe({ type: "resource", buffered: true });
284
+ } catch {}
285
+ }
286
+ }
287
+
288
+ stop(): void {
289
+ this.restoreConsole?.();
290
+ this.performanceObserver?.disconnect();
291
+ this.performanceObserver = null;
292
+ }
293
+
294
+ async execute(command: ClaimedEditorCommand): Promise<CommandExecution> {
295
+ const args = command.args;
296
+ switch (command.command) {
297
+ case "current_url":
298
+ return {
299
+ result: {
300
+ ok: true,
301
+ url: sanitizedUrl(location.href),
302
+ originalUrl: sanitizedUrl(location.href),
303
+ },
304
+ };
305
+ case "snapshot":
306
+ return { result: { ok: true, snapshot: this.snapshot() } };
307
+ case "console_logs":
308
+ return {
309
+ result: {
310
+ ok: true,
311
+ logs: this.consoleLogs.slice(-50).map(
312
+ (entry) => cloneSafe(entry) as Record<string, unknown>,
313
+ ),
314
+ },
315
+ };
316
+ case "network_logs": {
317
+ const filter = string(args.filter).toLowerCase();
318
+ const logs = this.networkLogs.slice(-80).map(sanitizeNetworkEntry).filter(
319
+ (entry) => !filter || JSON.stringify(entry).toLowerCase().includes(filter),
320
+ );
321
+ return { result: { ok: true, logs } };
322
+ }
323
+ case "screenshot":
324
+ return {
325
+ result: {
326
+ ok: false,
327
+ error: "Live-page screenshot capture is unsupported in the merchant SDK runtime.",
328
+ repairHint: "Use browser_snapshot for DOM evidence or capture the store tab through the connected browser runner.",
329
+ screenshot: { captured: false, reason: "unsupported_live_origin" },
330
+ },
331
+ };
332
+ case "eval_readonly":
333
+ return { result: this.evaluateReadonly(args) };
334
+ case "click":
335
+ return { result: this.click(args) };
336
+ case "type":
337
+ return { result: this.type(args) };
338
+ case "select_element":
339
+ return { result: this.selectElement(args) };
340
+ case "wait_for":
341
+ return { result: await this.waitFor(args) };
342
+ case "network_probe":
343
+ return { result: await this.networkProbe(args) };
344
+ case "apply_artifacts": {
345
+ const artifacts = parseArtifacts(args.artifacts);
346
+ return {
347
+ result: artifacts
348
+ ? auditResult(this.controller.applyCommandArtifacts(artifacts))
349
+ : { ok: false, error: "apply_artifacts requires a valid full artifact snapshot." },
350
+ };
351
+ }
352
+ case "navigate": {
353
+ const rawUrl = string(args.url);
354
+ try {
355
+ const url = new URL(rawUrl, this.controller.session.targetOrigin);
356
+ if (url.origin !== this.controller.session.targetOrigin) {
357
+ return { result: { ok: false, error: "Navigation must stay on the editor session origin." } };
358
+ }
359
+ return {
360
+ result: { ok: true, url: sanitizedUrl(url.toString()) },
361
+ afterAcknowledge: () => this.navigation.navigate(url.toString()),
362
+ };
363
+ } catch {
364
+ return { result: { ok: false, error: "Invalid navigation URL" } };
365
+ }
366
+ }
367
+ case "reload":
368
+ return { result: { ok: true }, afterAcknowledge: () => this.navigation.reload() };
369
+ case "back":
370
+ return { result: { ok: true }, afterAcknowledge: () => this.navigation.back() };
371
+ case "forward":
372
+ return { result: { ok: true }, afterAcknowledge: () => this.navigation.forward() };
373
+ }
374
+ }
375
+
376
+ private snapshot(): Record<string, unknown> {
377
+ const summarize = (selector: string, limit: number) => Array.from(
378
+ document.querySelectorAll(selector),
379
+ ).slice(0, limit).map(elementSummary).filter(Boolean);
380
+ return {
381
+ title: document.title ? sanitizeDiagnosticText(document.title).slice(0, 220) : null,
382
+ url: sanitizedUrl(location.href),
383
+ originalUrl: sanitizedUrl(location.href),
384
+ bodyTextLength: (document.body.textContent ?? "").length,
385
+ headings: summarize("h1,h2,h3", 18),
386
+ interactive: summarize("button,a,input,textarea,select,[role='button'],form", 45),
387
+ generated: summarize("[data-drip-preview-generated], [class*='apex-'], [class*='abt-'], [class*='drip-']", 20),
388
+ consoleCount: this.consoleLogs.length,
389
+ networkCount: this.networkLogs.length,
390
+ };
391
+ }
392
+
393
+ private click(args: Record<string, unknown>): EditorCommandResult {
394
+ const target = findTarget(args);
395
+ if (!target.ok || !target.element) return target as EditorCommandResult;
396
+ try { target.element.scrollIntoView({ block: "center", inline: "center" }); } catch {}
397
+ if (target.element instanceof HTMLElement) target.element.click();
398
+ else target.element.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
399
+ return { ok: true, element: elementSummary(target.element), targetResolution: { mode: target.mode } };
400
+ }
401
+
402
+ private type(args: Record<string, unknown>): EditorCommandResult {
403
+ const target = findTarget(args);
404
+ if (!target.ok || !target.element) return target as EditorCommandResult;
405
+ const value = typeof args.value === "string" ? args.value : String(args.value ?? "");
406
+ if (target.element instanceof HTMLInputElement || target.element instanceof HTMLTextAreaElement || target.element instanceof HTMLSelectElement) {
407
+ target.element.focus();
408
+ target.element.value = value;
409
+ } else if (target.element instanceof HTMLElement && target.element.isContentEditable) {
410
+ target.element.focus();
411
+ target.element.textContent = value;
412
+ } else {
413
+ return { ok: false, error: "Target does not accept text input" };
414
+ }
415
+ target.element.dispatchEvent(new Event("input", { bubbles: true }));
416
+ target.element.dispatchEvent(new Event("change", { bubbles: true }));
417
+ return { ok: true, element: elementSummary(target.element), valueLength: value.length };
418
+ }
419
+
420
+ private selectElement(args: Record<string, unknown>): EditorCommandResult {
421
+ const target = findTarget(args);
422
+ return target.ok && target.element
423
+ ? { ok: true, element: elementSummary(target.element), targetResolution: { mode: target.mode } }
424
+ : target as EditorCommandResult;
425
+ }
426
+
427
+ private async waitFor(args: Record<string, unknown>): Promise<EditorCommandResult> {
428
+ const timeout = Math.max(100, Math.min(Number(args.timeoutMs) || 5_000, 15_000));
429
+ const startedAt = Date.now();
430
+ while (Date.now() - startedAt < timeout) {
431
+ const target = findTarget(args);
432
+ if (target.ok && target.element) {
433
+ return { ok: true, element: elementSummary(target.element), waitedMs: Date.now() - startedAt };
434
+ }
435
+ await new Promise((resolve) => setTimeout(resolve, 100));
436
+ }
437
+ return { ok: false, error: "Target not found before timeout", waitedMs: Date.now() - startedAt };
438
+ }
439
+
440
+ private async networkProbe(args: Record<string, unknown>): Promise<EditorCommandResult> {
441
+ const startIndex = this.networkLogs.length;
442
+ const waitMs = Math.max(250, Math.min(Number(args.waitMs) || 2_000, 8_000));
443
+ const action = this.click(args);
444
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
445
+ const entries = this.networkLogs.slice(startIndex).slice(-30).map(sanitizeNetworkEntry);
446
+ return {
447
+ ok: action.ok,
448
+ attemptedAction: action.ok,
449
+ action,
450
+ waitedMs: waitMs,
451
+ currentUrl: sanitizedUrl(location.href),
452
+ originalUrl: sanitizedUrl(location.href),
453
+ networkObserved: entries.length > 0,
454
+ successfulRequestCount: entries.length,
455
+ entries,
456
+ ...(entries.length ? {} : {
457
+ repairHint: "No resource request was observed after the action. Inspect the handler and selector, then retry browser_network_probe.",
458
+ }),
459
+ };
460
+ }
461
+
462
+ private evaluateReadonly(args: Record<string, unknown>): EditorCommandResult {
463
+ if (!isLoopbackOrigin(this.controller.session.targetOrigin)) {
464
+ return {
465
+ ok: false,
466
+ error: "eval_readonly is unsupported on live merchant origins.",
467
+ repairHint: "Use browser_snapshot, browser_select_element, or another allowlisted inspection command.",
468
+ };
469
+ }
470
+ if (string(args.code)) {
471
+ return {
472
+ ok: false,
473
+ error: "JavaScript evaluation is disabled in the merchant SDK runtime.",
474
+ repairHint: "Pass an allowlisted operation: document_title, current_url, viewport, element_exists, element_count, or element_summary.",
475
+ };
476
+ }
477
+ const operation = string(args.operation);
478
+ if (operation === "document_title") {
479
+ return { ok: true, operation, result: sanitizeDiagnosticText(document.title).slice(0, 220) };
480
+ }
481
+ if (operation === "current_url") {
482
+ return { ok: true, operation, result: sanitizedUrl(location.href) };
483
+ }
484
+ if (operation === "viewport") {
485
+ return {
486
+ ok: true,
487
+ operation,
488
+ result: { width: window.innerWidth, height: window.innerHeight },
489
+ };
490
+ }
491
+ if (["element_exists", "element_count", "element_summary"].includes(operation)) {
492
+ const selector = string(args.selector);
493
+ if (!selector) return { ok: false, error: `${operation} requires a selector.` };
494
+ try {
495
+ const elements = Array.from(document.querySelectorAll(selector));
496
+ if (operation === "element_exists") {
497
+ return { ok: true, operation, result: elements.length > 0 };
498
+ }
499
+ if (operation === "element_count") {
500
+ return { ok: true, operation, result: elements.length };
501
+ }
502
+ return { ok: true, operation, result: elementSummary(elements[0] ?? null) };
503
+ } catch {
504
+ return { ok: false, error: "The inspection selector is invalid." };
505
+ }
506
+ }
507
+ return {
508
+ ok: false,
509
+ error: "eval_readonly requires an allowlisted declarative operation.",
510
+ repairHint: "Use document_title, current_url, viewport, element_exists, element_count, or element_summary.",
511
+ };
512
+ }
513
+ }
514
+
515
+ export class EditorCommandPoller {
516
+ private running = false;
517
+ private timer: ReturnType<typeof setTimeout> | null = null;
518
+ private polling = false;
519
+
520
+ constructor(
521
+ private readonly api: EditorApi,
522
+ private readonly executor: EditorCommandExecutor,
523
+ ) {}
524
+
525
+ start(): void {
526
+ if (this.running) return;
527
+ this.running = true;
528
+ this.executor.start();
529
+ void this.poll();
530
+ }
531
+
532
+ stop(): void {
533
+ this.running = false;
534
+ if (this.timer) clearTimeout(this.timer);
535
+ this.timer = null;
536
+ this.executor.stop();
537
+ }
538
+
539
+ private async poll(): Promise<void> {
540
+ if (!this.running || this.polling) return;
541
+ this.polling = true;
542
+ try {
543
+ const commands = await this.api.claimCommands();
544
+ for (const command of commands) {
545
+ if (!this.running) break;
546
+ let execution: CommandExecution;
547
+ try {
548
+ execution = await this.executor.execute(command);
549
+ } catch (error) {
550
+ await this.api.completeCommand(command.commandId, {
551
+ error: error instanceof Error ? error.message : "Browser command execution failed",
552
+ });
553
+ continue;
554
+ }
555
+ await this.api.completeCommand(command.commandId, { result: execution.result });
556
+ if (execution.afterAcknowledge) {
557
+ const action = execution.afterAcknowledge;
558
+ this.stop();
559
+ setTimeout(action, 0);
560
+ break;
561
+ }
562
+ }
563
+ } catch (error) {
564
+ if (error instanceof EditorApiError && [401, 403, 410].includes(error.status)) {
565
+ this.stop();
566
+ }
567
+ } finally {
568
+ this.polling = false;
569
+ if (this.running) this.timer = setTimeout(() => void this.poll(), POLL_INTERVAL_MS);
570
+ }
571
+ }
572
+ }