@trusty-squire/mcp 1.1.13-rc.8 → 1.1.13

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 (162) hide show
  1. package/README.md +139 -62
  2. package/assets/login/vnc-input.js +9 -0
  3. package/assets/login/vnc.html +214 -202
  4. package/assets/screencaps/connect-walkthrough.png +0 -0
  5. package/assets/screencaps/connect-walkthrough.svg +1 -1
  6. package/dist/api-client.d.ts +26 -3
  7. package/dist/api-client.d.ts.map +1 -1
  8. package/dist/api-client.js +19 -31
  9. package/dist/api-client.js.map +1 -1
  10. package/dist/bin.js +2 -3
  11. package/dist/bin.js.map +1 -1
  12. package/dist/bot/browser.d.ts +134 -77
  13. package/dist/bot/browser.d.ts.map +1 -1
  14. package/dist/bot/browser.js +1788 -1543
  15. package/dist/bot/browser.js.map +1 -1
  16. package/dist/bot/compact-observation-v2.d.ts +167 -0
  17. package/dist/bot/compact-observation-v2.d.ts.map +1 -0
  18. package/dist/bot/compact-observation-v2.js +1038 -0
  19. package/dist/bot/compact-observation-v2.js.map +1 -0
  20. package/dist/bot/credential-shape.d.ts +2 -0
  21. package/dist/bot/credential-shape.d.ts.map +1 -1
  22. package/dist/bot/credential-shape.js +21 -8
  23. package/dist/bot/credential-shape.js.map +1 -1
  24. package/dist/bot/element-fingerprint.d.ts +9 -0
  25. package/dist/bot/element-fingerprint.d.ts.map +1 -0
  26. package/dist/bot/element-fingerprint.js +147 -0
  27. package/dist/bot/element-fingerprint.js.map +1 -0
  28. package/dist/bot/email-verification.d.ts +7 -1
  29. package/dist/bot/email-verification.d.ts.map +1 -1
  30. package/dist/bot/email-verification.js +48 -14
  31. package/dist/bot/email-verification.js.map +1 -1
  32. package/dist/bot/google-login.d.ts +28 -52
  33. package/dist/bot/google-login.d.ts.map +1 -1
  34. package/dist/bot/google-login.js +246 -1119
  35. package/dist/bot/google-login.js.map +1 -1
  36. package/dist/bot/install-completion.d.ts +1 -2
  37. package/dist/bot/install-completion.d.ts.map +1 -1
  38. package/dist/bot/install-completion.js +1 -2
  39. package/dist/bot/install-completion.js.map +1 -1
  40. package/dist/bot/login-state.d.ts +13 -7
  41. package/dist/bot/login-state.d.ts.map +1 -1
  42. package/dist/bot/login-state.js +50 -114
  43. package/dist/bot/login-state.js.map +1 -1
  44. package/dist/bot/oauth-providers.d.ts +1 -1
  45. package/dist/bot/oauth-providers.d.ts.map +1 -1
  46. package/dist/bot/oauth-providers.js +3 -3
  47. package/dist/bot/oauth-providers.js.map +1 -1
  48. package/dist/bot/oauth-scope.d.ts +4 -0
  49. package/dist/bot/oauth-scope.d.ts.map +1 -0
  50. package/dist/bot/oauth-scope.js +58 -0
  51. package/dist/bot/oauth-scope.js.map +1 -0
  52. package/dist/bot/operator-browser-watchdog.d.ts +127 -0
  53. package/dist/bot/operator-browser-watchdog.d.ts.map +1 -0
  54. package/dist/bot/operator-browser-watchdog.js +438 -0
  55. package/dist/bot/operator-browser-watchdog.js.map +1 -0
  56. package/dist/bot/owner-process-reaper-worker.d.ts +2 -0
  57. package/dist/bot/owner-process-reaper-worker.d.ts.map +1 -0
  58. package/dist/bot/owner-process-reaper-worker.js +17 -0
  59. package/dist/bot/owner-process-reaper-worker.js.map +1 -0
  60. package/dist/bot/owner-process-reaper.d.ts +103 -0
  61. package/dist/bot/owner-process-reaper.d.ts.map +1 -0
  62. package/dist/bot/owner-process-reaper.js +1058 -0
  63. package/dist/bot/owner-process-reaper.js.map +1 -0
  64. package/dist/bot/pay-operator.d.ts +13 -2
  65. package/dist/bot/pay-operator.d.ts.map +1 -1
  66. package/dist/bot/pay-operator.js +319 -123
  67. package/dist/bot/pay-operator.js.map +1 -1
  68. package/dist/bot/profile.d.ts +10 -1
  69. package/dist/bot/profile.d.ts.map +1 -1
  70. package/dist/bot/profile.js +137 -11
  71. package/dist/bot/profile.js.map +1 -1
  72. package/dist/bot/promote-to-skill.js +1 -1
  73. package/dist/bot/promote-to-skill.js.map +1 -1
  74. package/dist/bot/provision-session.d.ts +60 -164
  75. package/dist/bot/provision-session.d.ts.map +1 -1
  76. package/dist/bot/provision-session.js +2049 -1546
  77. package/dist/bot/provision-session.js.map +1 -1
  78. package/dist/bot/remote-login-display.d.ts +61 -0
  79. package/dist/bot/remote-login-display.d.ts.map +1 -0
  80. package/dist/bot/remote-login-display.js +765 -0
  81. package/dist/bot/remote-login-display.js.map +1 -0
  82. package/dist/bot/session/hosts.d.ts +8 -0
  83. package/dist/bot/session/hosts.d.ts.map +1 -0
  84. package/dist/bot/session/hosts.js +36 -0
  85. package/dist/bot/session/hosts.js.map +1 -0
  86. package/dist/bot/session/lifecycle.d.ts +56 -0
  87. package/dist/bot/session/lifecycle.d.ts.map +1 -0
  88. package/dist/bot/session/lifecycle.js +962 -0
  89. package/dist/bot/session/lifecycle.js.map +1 -0
  90. package/dist/bot/session/model.d.ts +175 -0
  91. package/dist/bot/session/model.d.ts.map +1 -0
  92. package/dist/bot/session/model.js +77 -0
  93. package/dist/bot/session/model.js.map +1 -0
  94. package/dist/install/cli.d.ts +29 -9
  95. package/dist/install/cli.d.ts.map +1 -1
  96. package/dist/install/cli.js +243 -322
  97. package/dist/install/cli.js.map +1 -1
  98. package/dist/install/interactive.d.ts +1 -0
  99. package/dist/install/interactive.d.ts.map +1 -1
  100. package/dist/install/interactive.js +19 -5
  101. package/dist/install/interactive.js.map +1 -1
  102. package/dist/install/ui.d.ts.map +1 -1
  103. package/dist/install/ui.js +3 -15
  104. package/dist/install/ui.js.map +1 -1
  105. package/dist/operator-env.js +1 -1
  106. package/dist/operator-env.js.map +1 -1
  107. package/dist/server-instance-registry.d.ts +103 -0
  108. package/dist/server-instance-registry.d.ts.map +1 -0
  109. package/dist/server-instance-registry.js +392 -0
  110. package/dist/server-instance-registry.js.map +1 -0
  111. package/dist/server.d.ts +15 -1
  112. package/dist/server.d.ts.map +1 -1
  113. package/dist/server.js +164 -62
  114. package/dist/server.js.map +1 -1
  115. package/dist/session-guard.d.ts +28 -0
  116. package/dist/session-guard.d.ts.map +1 -0
  117. package/dist/session-guard.js +84 -0
  118. package/dist/session-guard.js.map +1 -0
  119. package/dist/session.d.ts +57 -13
  120. package/dist/session.d.ts.map +1 -1
  121. package/dist/session.js +246 -127
  122. package/dist/session.js.map +1 -1
  123. package/dist/tools/audit-log.d.ts +16 -2
  124. package/dist/tools/audit-log.d.ts.map +1 -1
  125. package/dist/tools/audit-log.js +223 -13
  126. package/dist/tools/audit-log.js.map +1 -1
  127. package/dist/tools/audit-rollup.d.ts +80 -0
  128. package/dist/tools/audit-rollup.d.ts.map +1 -0
  129. package/dist/tools/audit-rollup.js +0 -0
  130. package/dist/tools/audit-rollup.js.map +1 -0
  131. package/dist/tools/fetch-credential.d.ts +37 -0
  132. package/dist/tools/fetch-credential.d.ts.map +1 -0
  133. package/dist/tools/fetch-credential.js +153 -0
  134. package/dist/tools/fetch-credential.js.map +1 -0
  135. package/dist/tools/index.d.ts +3 -1
  136. package/dist/tools/index.d.ts.map +1 -1
  137. package/dist/tools/index.js +7 -3
  138. package/dist/tools/index.js.map +1 -1
  139. package/dist/tools/operate-pay.d.ts +4 -4
  140. package/dist/tools/operate-pay.d.ts.map +1 -1
  141. package/dist/tools/operate-pay.js +163 -96
  142. package/dist/tools/operate-pay.js.map +1 -1
  143. package/dist/tools/provision-drive.d.ts +43 -35
  144. package/dist/tools/provision-drive.d.ts.map +1 -1
  145. package/dist/tools/provision-drive.js +268 -164
  146. package/dist/tools/provision-drive.js.map +1 -1
  147. package/dist/tools/use-credential.d.ts.map +1 -1
  148. package/dist/tools/use-credential.js +3 -2
  149. package/dist/tools/use-credential.js.map +1 -1
  150. package/package.json +4 -6
  151. package/dist/bot/operator-direct-identity.d.ts +0 -26
  152. package/dist/bot/operator-direct-identity.d.ts.map +0 -1
  153. package/dist/bot/operator-direct-identity.js +0 -120
  154. package/dist/bot/operator-direct-identity.js.map +0 -1
  155. package/dist/bot/operator-profile-pool.d.ts +0 -97
  156. package/dist/bot/operator-profile-pool.d.ts.map +0 -1
  157. package/dist/bot/operator-profile-pool.js +0 -855
  158. package/dist/bot/operator-profile-pool.js.map +0 -1
  159. package/dist/bot/xvfb.d.ts +0 -13
  160. package/dist/bot/xvfb.d.ts.map +0 -1
  161. package/dist/bot/xvfb.js +0 -146
  162. package/dist/bot/xvfb.js.map +0 -1
@@ -0,0 +1,1038 @@
1
+ import { Buffer } from "node:buffer";
2
+ export const OBSERVE_V2_MAX_WIRE_BYTES = 4_096;
3
+ export const OBSERVE_V2_MAX_TOKENS = 1_024;
4
+ const FIRST_PAGE_ROW_LIMIT = 4;
5
+ export function compactV2PayloadWithinBudget(payload) {
6
+ const bytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
7
+ return bytes <= OBSERVE_V2_MAX_WIRE_BYTES && bytes <= OBSERVE_V2_MAX_TOKENS;
8
+ }
9
+ /**
10
+ * Semantic essentials are sticky across V2 deltas. A repeat observation only
11
+ * needs to carry them when their already-sealed representation changed; the
12
+ * consumer retains the prior title/heading just like it retains safe_table.
13
+ */
14
+ export function equalSafePageSemanticsV2(left, right) {
15
+ return left.title === right.title && (left.headings?.[0] ?? "") === (right.headings?.[0] ?? "");
16
+ }
17
+ /** `@e:` + a truncated session-secret HMAC of (document epoch, fingerprint). */
18
+ export const COMPACT_V2_HANDLE_LENGTH = 10;
19
+ const COMPACT_V2_HANDLE_RE = new RegExp(`^@e:[A-Za-z0-9_-]{${COMPACT_V2_HANDLE_LENGTH}}$`);
20
+ const COMPACT_V2_LABEL_RE = /^@[a-z0-9]+(?:-[a-z0-9]+)*$/;
21
+ export function isCompactV2Handle(target) {
22
+ return COMPACT_V2_HANDLE_RE.test(target);
23
+ }
24
+ /** The `@label` alias form, distinguishable from a handle by its `@e:` prefix. */
25
+ export function isCompactV2Label(target) {
26
+ return COMPACT_V2_LABEL_RE.test(target);
27
+ }
28
+ /**
29
+ * Resolve a handle only when it is well-formed AND a member of the current
30
+ * snapshot's sealed map. Callers must never turn an unknown @e: value into a
31
+ * label or legacy-ref lookup. The handle is document-scoped by construction
32
+ * (the epoch is hashed into it), so a ref from a replaced document finds no
33
+ * live match rather than resolving onto whatever now occupies its position.
34
+ */
35
+ export function compactV2LegacyRefForHandle(handles, target) {
36
+ if (!isCompactV2Handle(target))
37
+ return null;
38
+ return handles.get(target) ?? null;
39
+ }
40
+ const LABEL_MAX_CHARS = 32;
41
+ /**
42
+ * The addressable alias for a screened control description. Slugified so the
43
+ * agent can type it back verbatim; `undefined` when the description screened
44
+ * out or carries no alphanumeric content.
45
+ */
46
+ export function controlLabelV2(description) {
47
+ if (description === undefined)
48
+ return undefined;
49
+ const slug = description
50
+ .toLowerCase()
51
+ .replace(/[^a-z0-9]+/g, "-")
52
+ .replace(/^-+|-+$/g, "")
53
+ .slice(0, LABEL_MAX_CHARS)
54
+ .replace(/-+$/g, "");
55
+ return slug.length === 0 ? undefined : `@${slug}`;
56
+ }
57
+ // The protocol is intentionally positional to keep repeated observes small.
58
+ // Tuple schema: [ref, role(b/l/t/s/c/r/tb/m/f), optional compact description].
59
+ // The description starts with the `@label` alias (itself a valid act target)
60
+ // and appends only present code-owned facts (`s=`, `a=`, `f=`, `q=`, `x=`). It
61
+ // is deliberately one sparse string rather than nullable columns:
62
+ // checked/unchecked, disabled, action, field, card choice, and frame context
63
+ // remain distinguishable without paying for empty slots on every row. The
64
+ // label's slug charset excludes `|` and `=`, so no escaping is needed.
65
+ function wireControl(row) {
66
+ const role = {
67
+ button: "b",
68
+ link: "l",
69
+ textbox: "t",
70
+ select: "s",
71
+ checkbox: "c",
72
+ radio: "r",
73
+ tab: "tb",
74
+ menuitem: "m",
75
+ file: "f",
76
+ };
77
+ const facts = [
78
+ row.label,
79
+ ...(row.state === undefined ? [] : [`s=${row.state}`]),
80
+ ...(row.action === undefined ? [] : [`a=${row.action}`]),
81
+ ...(row.field === undefined ? [] : [`f=${row.field}`]),
82
+ ...(row.choice === undefined ? [] : [`q=${row.choice}`]),
83
+ ...(row.frame === "main" ? [] : [`x=${row.frame === "same_origin" ? "s" : "x"}`]),
84
+ ].filter((value) => value !== undefined);
85
+ return facts.length === 0
86
+ ? [row.ref, role[row.role]]
87
+ : [row.ref, role[row.role], facts.join("|")];
88
+ }
89
+ const SAFE_DESCRIPTION_MAX_CHARS = 40;
90
+ // There is NO description screening on the observation path. Every string a
91
+ // page renders — OAuth button copy, an email address, a Gmail subject line, a
92
+ // street address, a rendered API key, a card number — is content the driving
93
+ // agent needs to read. The only transform left is truncation, which is the
94
+ // compactness budget, not masking.
95
+ const CARD_SECURITY_VALUE_RE = /\b(?:cvv|cvc|security\s*code)\s*[:#-]?\s*\d{3,4}\b/i;
96
+ const PAN_MAX_SPAN_CHARS = 96;
97
+ function luhnValid(digits) {
98
+ let sum = 0;
99
+ let double = false;
100
+ for (let index = digits.length - 1; index >= 0; index -= 1) {
101
+ let digit = Number(digits[index]);
102
+ if (double) {
103
+ digit *= 2;
104
+ if (digit > 9)
105
+ digit -= 9;
106
+ }
107
+ sum += digit;
108
+ double = !double;
109
+ }
110
+ return sum % 10 === 0;
111
+ }
112
+ /** A rendered card number: 13-19 digits in one span that passes Luhn. */
113
+ function containsLuhnPan(value) {
114
+ const positions = Array.from(value.matchAll(/\d/g), (match) => match.index);
115
+ for (let start = 0; start + 13 <= positions.length; start += 1) {
116
+ const maxLength = Math.min(19, positions.length - start);
117
+ for (let length = 13; length <= maxLength; length += 1) {
118
+ const span = positions.slice(start, start + length);
119
+ if (span[span.length - 1] - span[0] + 1 > PAN_MAX_SPAN_CHARS)
120
+ break;
121
+ if (luhnValid(span.map((position) => value[position]).join("")))
122
+ return true;
123
+ }
124
+ }
125
+ return false;
126
+ }
127
+ /**
128
+ * Card material, used ONLY by the recordable-token screen below (the operator's
129
+ * stderr audit trail and the action trace a capture publishes to the SHARED
130
+ * skill registry). It is never applied to what the agent is shown.
131
+ */
132
+ function carriesPaymentMaterial(value) {
133
+ return containsLuhnPan(value) || CARD_SECURITY_VALUE_RE.test(value);
134
+ }
135
+ // ---- Recordable-token screen (NOT observation redaction) -------------------
136
+ // Two surfaces are not the agent's view of the page and keep their original
137
+ // closed-vocabulary screen: the operator's structured stderr audit trail, and
138
+ // the recorded action trace that a captured run publishes to the SHARED skill
139
+ // registry. Both cross a boundary the payment-only observation policy says
140
+ // nothing about — one is a log, the other is cross-user institutional memory —
141
+ // so a page-derived string still has to be recognizable code-owned vocabulary
142
+ // before it lands there. Never use this to screen what `operate_observe`,
143
+ // `operate_observe_query`, or `operate_screenshot` shows the driving agent.
144
+ const RECORDABLE_TOKEN_MAX_CHARS = 40;
145
+ const RECORDABLE_EMAIL_RE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
146
+ const RECORDABLE_SECRET_ASSIGNMENT_RE = /\b(?:password|passcode|token|api[_ -]?key|secret)\b\s*[:=]\s*\S{4,}/i;
147
+ const RECORDABLE_HIGH_ENTROPY_RE = /\b[A-Za-z0-9_-]{24,}\b/;
148
+ const RECORDABLE_GRAMMAR_RE = /^[\p{L}\p{N}][\p{L}\p{N}\s&'’+,.!?():=/@_$|-]*$/u;
149
+ const RECORDABLE_WORDS = new Set([
150
+ "a",
151
+ "access",
152
+ "account",
153
+ "add",
154
+ "address",
155
+ "an",
156
+ "and",
157
+ "api",
158
+ "back",
159
+ "bag",
160
+ "basket",
161
+ "billing",
162
+ "button",
163
+ "cancel",
164
+ "card",
165
+ "cart",
166
+ "choose",
167
+ "checkout",
168
+ "city",
169
+ "close",
170
+ "code",
171
+ "confirm",
172
+ "contact",
173
+ "continue",
174
+ "control",
175
+ "controls",
176
+ "copy",
177
+ "country",
178
+ "create",
179
+ "customer",
180
+ "date",
181
+ "dense",
182
+ "delivery",
183
+ "details",
184
+ "different",
185
+ "discount",
186
+ "dismiss",
187
+ "done",
188
+ "economy",
189
+ "email",
190
+ "example",
191
+ "express",
192
+ "external",
193
+ "first",
194
+ "finish",
195
+ "form",
196
+ "free",
197
+ "gift",
198
+ "google",
199
+ "ground",
200
+ "in",
201
+ "item",
202
+ "key",
203
+ "large",
204
+ "log",
205
+ "login",
206
+ "merchant",
207
+ "method",
208
+ "name",
209
+ "native",
210
+ "next",
211
+ "no",
212
+ "now",
213
+ "option",
214
+ "order",
215
+ "overnight",
216
+ "page",
217
+ "password",
218
+ "pay",
219
+ "payment",
220
+ "phone",
221
+ "pickup",
222
+ "postal",
223
+ "previous",
224
+ "priority",
225
+ "private",
226
+ "proceed",
227
+ "product",
228
+ "promo",
229
+ "quantity",
230
+ "rate",
231
+ "region",
232
+ "register",
233
+ "registration",
234
+ "review",
235
+ "save",
236
+ "sample",
237
+ "search",
238
+ "secure",
239
+ "select",
240
+ "serialized",
241
+ "shipping",
242
+ "sign",
243
+ "signup",
244
+ "size",
245
+ "standard",
246
+ "storefront",
247
+ "style",
248
+ "submit",
249
+ "terms",
250
+ "thanks",
251
+ "to",
252
+ "token",
253
+ "united",
254
+ "username",
255
+ "variant",
256
+ "verification",
257
+ "view",
258
+ "with",
259
+ "workspace",
260
+ "your",
261
+ ]);
262
+ function hasRecordableGrammar(value) {
263
+ if (!RECORDABLE_GRAMMAR_RE.test(value))
264
+ return false;
265
+ const words = value.match(/[\p{L}\p{N}]+/gu) ?? [];
266
+ return (words.length > 0 &&
267
+ words.every((word) => RECORDABLE_WORDS.has(word.toLowerCase()) || /^\d{1,6}$/.test(word)));
268
+ }
269
+ /**
270
+ * The screen for the audit trail and the registry-bound action trace. Returns
271
+ * `undefined` for anything outside the code-owned vocabulary; callers turn that
272
+ * into `<sealed>` or refuse to record the action.
273
+ */
274
+ export function recordableTokenV2(value) {
275
+ if (typeof value !== "string")
276
+ return undefined;
277
+ const normalized = value.replace(/\s+/g, " ").trim();
278
+ if (normalized.length === 0 ||
279
+ !hasRecordableGrammar(normalized) ||
280
+ carriesPaymentMaterial(normalized) ||
281
+ normalized.replace(/\D/g, "").length >= 13 ||
282
+ RECORDABLE_EMAIL_RE.test(normalized) ||
283
+ RECORDABLE_SECRET_ASSIGNMENT_RE.test(normalized) ||
284
+ RECORDABLE_HIGH_ENTROPY_RE.test(normalized)) {
285
+ return undefined;
286
+ }
287
+ return normalized.length <= RECORDABLE_TOKEN_MAX_CHARS
288
+ ? normalized
289
+ : `${normalized.slice(0, RECORDABLE_TOKEN_MAX_CHARS - 1)}…`;
290
+ }
291
+ /**
292
+ * Normalize and length-budget a page-derived description for the wire row. It
293
+ * rejects nothing: the truncation is the compactness budget, not redaction.
294
+ */
295
+ export function safeDescriptionV2(value) {
296
+ if (typeof value !== "string")
297
+ return undefined;
298
+ // Control characters would break the positional wire encoding; they are not
299
+ // page copy, so dropping them is formatting rather than masking.
300
+ const normalized = value
301
+ .replace(/[\p{Cc}\p{Cf}]/gu, " ")
302
+ .replace(/\s+/g, " ")
303
+ .trim();
304
+ if (normalized.length === 0)
305
+ return undefined;
306
+ return normalized.length <= SAFE_DESCRIPTION_MAX_CHARS
307
+ ? normalized
308
+ : `${normalized.slice(0, SAFE_DESCRIPTION_MAX_CHARS - 1)}…`;
309
+ }
310
+ const SAFE_AUTOCOMPLETE_TOKENS = new Set([
311
+ "address-level1",
312
+ "address-level2",
313
+ "address-line1",
314
+ "address-line2",
315
+ "address-line3",
316
+ "bday",
317
+ "bday-day",
318
+ "bday-month",
319
+ "bday-year",
320
+ "cc-csc",
321
+ "cc-exp",
322
+ "cc-exp-month",
323
+ "cc-exp-year",
324
+ "cc-name",
325
+ "cc-number",
326
+ "country",
327
+ "country-name",
328
+ "current-password",
329
+ "email",
330
+ "family-name",
331
+ "given-name",
332
+ "honorific-prefix",
333
+ "honorific-suffix",
334
+ "name",
335
+ "new-password",
336
+ "nickname",
337
+ "one-time-code",
338
+ "organization",
339
+ "organization-title",
340
+ "postal-code",
341
+ "shipping",
342
+ "street-address",
343
+ "tel",
344
+ "tel-area-code",
345
+ "tel-country-code",
346
+ "tel-extension",
347
+ "tel-local",
348
+ "tel-local-prefix",
349
+ "tel-local-suffix",
350
+ "tel-national",
351
+ "transaction-amount",
352
+ "transaction-currency",
353
+ "username",
354
+ ]);
355
+ function safeAutocompleteV2(value) {
356
+ if (typeof value !== "string")
357
+ return null;
358
+ const tokens = value.toLowerCase().trim().split(/\s+/).filter(Boolean);
359
+ return tokens.length > 0 && tokens.every((token) => SAFE_AUTOCOMPLETE_TOKENS.has(token))
360
+ ? tokens.join(" ")
361
+ : null;
362
+ }
363
+ // Structural hostname validity only — no content screen. A host that fails this
364
+ // is malformed, not sensitive.
365
+ function safeHostnameV2(value) {
366
+ const hostname = value.toLowerCase();
367
+ if (hostname.length === 0 || hostname.length > 253)
368
+ return false;
369
+ return hostname.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
370
+ }
371
+ export function safeOriginV2(value) {
372
+ if (typeof value !== "string")
373
+ return null;
374
+ try {
375
+ const parsed = new URL(value);
376
+ if ((parsed.protocol !== "https:" && parsed.protocol !== "http:") ||
377
+ parsed.username.length > 0 ||
378
+ parsed.password.length > 0 ||
379
+ !safeHostnameV2(parsed.hostname)) {
380
+ return null;
381
+ }
382
+ return parsed.origin;
383
+ }
384
+ catch {
385
+ return null;
386
+ }
387
+ }
388
+ // Audit-trail sealing: the operator's structured stderr trail must never carry
389
+ // a raw URL or host while compact-v2 is sealing an observation. These two are
390
+ // the audit-side counterpart of safeOriginV2, and live here with it so both the
391
+ // lifecycle transaction and the observe/act surface can reach them.
392
+ export function compactV2AuditUrl(rawUrl) {
393
+ return safeOriginV2(rawUrl) ?? "<sealed-origin>";
394
+ }
395
+ export function compactV2AuditHost(rawHost) {
396
+ const origin = safeOriginV2(rawHost.includes("://") ? rawHost : `https://${rawHost}`);
397
+ if (origin === null)
398
+ return "<sealed-host>";
399
+ return new URL(origin).host;
400
+ }
401
+ export function compactV2AuditValue(key, value) {
402
+ if ((key === "url" || key === "service_url") && typeof value === "string") {
403
+ return compactV2AuditUrl(value);
404
+ }
405
+ if ((key === "host" || key === "url_host" || key === "frame_origin" || key === "recipe_domain") &&
406
+ typeof value === "string") {
407
+ return compactV2AuditHost(value);
408
+ }
409
+ if (key === "allowed_hosts" && Array.isArray(value)) {
410
+ return value.map((host) => typeof host === "string" ? compactV2AuditHost(host) : "<sealed-host>");
411
+ }
412
+ if (typeof value === "string")
413
+ return recordableTokenV2(value) ?? "<sealed>";
414
+ if (Array.isArray(value))
415
+ return value.map((item) => compactV2AuditValue("", item));
416
+ if (value !== null && typeof value === "object") {
417
+ return Object.fromEntries(Object.entries(value).map(([nestedKey, nestedValue]) => [
418
+ nestedKey,
419
+ compactV2AuditValue(nestedKey, nestedValue),
420
+ ]));
421
+ }
422
+ return value;
423
+ }
424
+ export function sealRetainedInteractiveElementsV2(elements, selectorFor = () => "") {
425
+ // The retained inventory is what the recorded action trace reads its
426
+ // dom_hint / accessible_name from, so it is screened for RECORDABILITY, not
427
+ // by the payment-only observation policy. What the agent SEES is built from
428
+ // freshly extracted elements through safeDescriptionV2.
429
+ const description = (value) => recordableTokenV2(value) ?? null;
430
+ const finiteToken = (value, allowed) => {
431
+ const normalized = value?.toLowerCase();
432
+ return normalized !== undefined && allowed.has(normalized) ? normalized : null;
433
+ };
434
+ const tags = new Set([
435
+ "a",
436
+ "button",
437
+ "details",
438
+ "input",
439
+ "label",
440
+ "option",
441
+ "select",
442
+ "summary",
443
+ "textarea",
444
+ ]);
445
+ const types = new Set([
446
+ "button",
447
+ "checkbox",
448
+ "date",
449
+ "email",
450
+ "file",
451
+ "hidden",
452
+ "image",
453
+ "month",
454
+ "number",
455
+ "password",
456
+ "radio",
457
+ "reset",
458
+ "search",
459
+ "submit",
460
+ "tel",
461
+ "text",
462
+ "time",
463
+ "url",
464
+ "week",
465
+ ]);
466
+ const roles = new Set([
467
+ "button",
468
+ "checkbox",
469
+ "combobox",
470
+ "link",
471
+ "listbox",
472
+ "menuitem",
473
+ "option",
474
+ "radio",
475
+ "searchbox",
476
+ "switch",
477
+ "tab",
478
+ "textbox",
479
+ ]);
480
+ return elements.map((element) => ({
481
+ index: element.index,
482
+ tag: finiteToken(element.tag, tags) ?? "unknown",
483
+ type: finiteToken(element.type, types),
484
+ id: description(element.id),
485
+ name: description(element.name),
486
+ placeholder: description(element.placeholder),
487
+ ariaLabel: description(element.ariaLabel),
488
+ role: finiteToken(element.role, roles),
489
+ labelText: description(element.labelText),
490
+ visibleText: description(element.visibleText),
491
+ selector: selectorFor(element),
492
+ visible: element.visible === true,
493
+ inViewport: element.inViewport === true,
494
+ inConsentWidget: element.inConsentWidget === true,
495
+ href: null,
496
+ iconLabel: description(element.iconLabel),
497
+ title: description(element.title),
498
+ testId: description(element.testId),
499
+ autocomplete: safeAutocompleteV2(element.autocomplete),
500
+ dataRole: null,
501
+ landmark: typeof element.landmark === "string" &&
502
+ /^(?:header|main|footer|nav|aside|article|section)$/.test(element.landmark)
503
+ ? element.landmark
504
+ : null,
505
+ value: null,
506
+ checked: element.checked ?? null,
507
+ disabled: element.disabled ?? null,
508
+ required: element.required ?? null,
509
+ selectOptions: null,
510
+ selectedOptionText: null,
511
+ interactedThisRun: element.interactedThisRun === true,
512
+ screenPath: null,
513
+ container: null,
514
+ containerId: typeof element.containerId === "number" &&
515
+ Number.isSafeInteger(element.containerId) &&
516
+ element.containerId > 0
517
+ ? element.containerId
518
+ : null,
519
+ formId: typeof element.formId === "number" &&
520
+ Number.isSafeInteger(element.formId) &&
521
+ element.formId > 0
522
+ ? element.formId
523
+ : null,
524
+ topmost: element.topmost ?? null,
525
+ occludedBy: null,
526
+ cardRadioGroup: element.cardRadioGroup !== null &&
527
+ element.cardRadioGroup !== undefined &&
528
+ Number.isSafeInteger(element.cardRadioGroup.id) &&
529
+ Number.isSafeInteger(element.cardRadioGroup.position) &&
530
+ Number.isSafeInteger(element.cardRadioGroup.total)
531
+ ? {
532
+ id: element.cardRadioGroup.id,
533
+ position: element.cardRadioGroup.position,
534
+ total: element.cardRadioGroup.total,
535
+ }
536
+ : null,
537
+ frameOrigin: safeOriginV2(element.frameOrigin),
538
+ frameUrl: null,
539
+ framePath: typeof element.framePath === "string" && /^\d+(?:\/\d+)*$/.test(element.framePath)
540
+ ? element.framePath
541
+ : null,
542
+ frameOpaque: element.frameOpaque === true,
543
+ }));
544
+ }
545
+ export function safePageSemanticsV2(source) {
546
+ const title = safeDescriptionV2(source.title);
547
+ const headings = source.headings
548
+ .map((heading) => safeDescriptionV2(heading))
549
+ .filter((value) => value !== undefined)
550
+ .filter((value, index, all) => all.indexOf(value) === index)
551
+ .slice(0, 1);
552
+ return {
553
+ ...(title === undefined ? {} : { title }),
554
+ ...(headings.length === 0 ? {} : { headings }),
555
+ };
556
+ }
557
+ /**
558
+ * Diff only the sealed representation. In particular, raw changes to a
559
+ * field value, card number, live region, or merchant copy cannot cause either
560
+ * the raw data or a page-derived derivative to cross into the delta.
561
+ */
562
+ export function diffSafeControlsV2(previous, stage, rows) {
563
+ const current = new Map(rows.map((row) => [row.ref, row]));
564
+ const added = [];
565
+ const changed = [];
566
+ for (const row of rows) {
567
+ const before = previous.byRef.get(row.ref);
568
+ if (before === undefined)
569
+ added.push(row);
570
+ else if (JSON.stringify(wireControl(before)) !== JSON.stringify(wireControl(row)))
571
+ changed.push(row);
572
+ }
573
+ const removed = [...previous.byRef.keys()].filter((ref) => !current.has(ref));
574
+ return { added, changed, removed, stageChanged: previous.stage !== stage };
575
+ }
576
+ /**
577
+ * Encodes a structural V2 delta. `null` deliberately asks the caller to send
578
+ * a paged full resync instead: a delta must never exceed the hard wire budget.
579
+ */
580
+ export function encodeV2Delta(args) {
581
+ const payload = {
582
+ format: "compact-v2",
583
+ url: args.pageUrl ?? "",
584
+ text: "",
585
+ session_id: args.sessionId,
586
+ delta: true,
587
+ ...(args.semantics === undefined ? {} : { semantic: args.semantics }),
588
+ ...(args.delta.stageChanged ? { stage: args.stage } : {}),
589
+ // `safe_table` follows the established TS delta protocol: rows are
590
+ // upserts, irrespective of whether their ref is new or changed. This keeps
591
+ // existing delta consumers compatible while the @e: map itself stays V2.
592
+ ...(args.delta.added.length + args.delta.changed.length > 0
593
+ ? { safe_table: [...args.delta.added, ...args.delta.changed].map(wireControl) }
594
+ : {}),
595
+ ...(args.delta.removed.length > 0 ? { removed: args.delta.removed } : {}),
596
+ };
597
+ return compactV2PayloadWithinBudget(payload) ? payload : null;
598
+ }
599
+ const INTENTS = [
600
+ ["add_to_cart", /add\s+(?:to\s+)?(?:cart|bag|basket)/i],
601
+ ["view_cart", /view\s+(?:cart|bag|basket)/i],
602
+ ["destructive", /\b(?:delete|remove|erase|destroy)\b/i],
603
+ ["checkout", /^(?:(?:go|proceed|continue)(?:\s+to)?\s+)?(?:secure\s+)?checkout(?:\s+now)?$/i],
604
+ ["payment", /\b(?:pay(?:ment)?|card)\b/i],
605
+ ["signup", /sign\s*up|create\s+(?:account|workspace)|register/i],
606
+ ["login", /log\s*in|sign\s*in|continue\s+with/i],
607
+ ["search", /search|find/i],
608
+ ["close", /close|dismiss|cancel|no\s+thanks/i],
609
+ ["next", /next/i],
610
+ ["previous", /previous|back/i],
611
+ ["submit", /submit|save|create|send/i],
612
+ ["continue", /continue|proceed|start|accept|allow|keep|retain/i],
613
+ ];
614
+ const BUTTON_INPUT_TYPES = new Set(["button", "submit", "reset", "image"]);
615
+ function isButtonInput(el) {
616
+ return el.tag === "input" && BUTTON_INPUT_TYPES.has((el.type ?? "").toLowerCase());
617
+ }
618
+ function controlNamingTexts(el) {
619
+ return [
620
+ el.visibleText,
621
+ el.labelText,
622
+ el.ariaLabel,
623
+ el.iconLabel,
624
+ el.title,
625
+ el.placeholder,
626
+ isButtonInput(el) ? el.value : undefined,
627
+ ];
628
+ }
629
+ function candidateTexts(el) {
630
+ return [
631
+ el.visibleText,
632
+ el.labelText,
633
+ el.ariaLabel,
634
+ el.iconLabel,
635
+ el.title,
636
+ el.placeholder,
637
+ el.name,
638
+ el.id,
639
+ ...(isButtonInput(el) ? [el.value] : []),
640
+ ]
641
+ .filter((part) => typeof part === "string")
642
+ .map((part) => part.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim())
643
+ .filter(Boolean);
644
+ }
645
+ function candidateText(el) {
646
+ return candidateTexts(el).join(" ");
647
+ }
648
+ function controlDescription(el) {
649
+ // Labels are chosen from visible/accessibility naming sources only. Native
650
+ // button values are names; field values, `name`, and `id` stay excluded.
651
+ return controlNamingTexts(el)
652
+ .map((candidate) => safeDescriptionV2(candidate))
653
+ .find((candidate) => candidate !== undefined);
654
+ }
655
+ function privateQueryTokenV2(value) {
656
+ const normalized = value.normalize("NFKC").trim().toLowerCase();
657
+ if (normalized.length < 1 ||
658
+ normalized.length > 48 ||
659
+ !/^[\p{L}\p{M}\p{N}]{1,48}$/u.test(normalized) ||
660
+ (/^\p{N}+$/u.test(normalized) && normalized.length > 4)) {
661
+ return null;
662
+ }
663
+ return normalized;
664
+ }
665
+ function privateQueryTermsV2(value) {
666
+ const normalized = value.normalize("NFKC").trim().toLowerCase();
667
+ if (normalized.length < 1 || normalized.length > 96)
668
+ return null;
669
+ const rawTerms = normalized.match(/[\p{L}\p{M}\p{N}]{1,48}/gu);
670
+ if (rawTerms === null || rawTerms.length === 0 || rawTerms.length > 6)
671
+ return null;
672
+ const separators = normalized.replace(/[\p{L}\p{M}\p{N}]{1,48}/gu, "");
673
+ if (!/^[\s&'’+,.!?():/_|-]*$/u.test(separators))
674
+ return null;
675
+ const terms = rawTerms.map(privateQueryTokenV2);
676
+ return terms.every((term) => term !== null) ? [...new Set(terms)] : null;
677
+ }
678
+ export function controlMatchesPrivateQueryV2(el, query) {
679
+ const needles = privateQueryTermsV2(query);
680
+ if (needles === null)
681
+ return false;
682
+ // The private match also consults the element's name/id slugs so
683
+ // choice-group members without their own copy — Shopify shipping-rate
684
+ // radios (name "checkout[shipping_rate][id]", id "checkout_shipping_rate_…")
685
+ // — stay queryable. These are DOM identifiers, never field values, and each
686
+ // candidate still passes the full secret-shape gate below before any token
687
+ // comparison.
688
+ return [...controlNamingTexts(el), el.name, el.id].some((candidate) => {
689
+ if (typeof candidate !== "string")
690
+ return false;
691
+ const tokens = candidate.normalize("NFKC").match(/[\p{L}\p{M}\p{N}]{1,48}/gu);
692
+ if (tokens === null)
693
+ return false;
694
+ const safeTokens = new Set(tokens.map(privateQueryTokenV2).filter((token) => token !== null));
695
+ return needles.every((needle) => safeTokens.has(needle));
696
+ });
697
+ }
698
+ function roleOf(el) {
699
+ const role = (el.role ?? "").toLowerCase();
700
+ const type = (el.type ?? "").toLowerCase();
701
+ if (type === "file")
702
+ return "file";
703
+ if (isButtonInput(el))
704
+ return "button";
705
+ if (type === "checkbox" || role === "checkbox")
706
+ return "checkbox";
707
+ if (type === "radio" || role === "radio")
708
+ return "radio";
709
+ if (el.tag === "select" || role === "combobox" || role === "listbox")
710
+ return "select";
711
+ if (el.tag === "input" || el.tag === "textarea" || role === "textbox")
712
+ return "textbox";
713
+ if (el.tag === "a" || role === "link")
714
+ return "link";
715
+ if (role === "tab")
716
+ return "tab";
717
+ if (role === "menuitem" || role === "option")
718
+ return "menuitem";
719
+ if (el.tag === "button" ||
720
+ role === "button" ||
721
+ (el.topmost !== null && el.topmost !== undefined)) {
722
+ return "button";
723
+ }
724
+ return null;
725
+ }
726
+ function stateOf(el) {
727
+ // A compact code-owned bitset: c=checked, u=unchecked, d=disabled,
728
+ // r=required. It is deliberately not a page-provided string.
729
+ let state = "";
730
+ if (el.checked === true)
731
+ state += "c";
732
+ else if (el.checked === false)
733
+ state += "u";
734
+ if (el.disabled === true)
735
+ state += "d";
736
+ if (el.required === true)
737
+ state += "r";
738
+ return state || undefined;
739
+ }
740
+ function intentOf(el) {
741
+ const texts = candidateTexts(el);
742
+ return INTENTS.find(([, expression]) => texts.some((text) => expression.test(text)))?.[0];
743
+ }
744
+ function hasExplicitPaymentFieldSignal(el) {
745
+ const autocomplete = (el.autocomplete ?? "").toLowerCase();
746
+ const text = candidateText(el).toLowerCase();
747
+ return (autocomplete
748
+ .trim()
749
+ .split(/\s+/)
750
+ .some((token) => token.startsWith("cc-")) ||
751
+ /\b(?:cvc|cvv|csc|card verification(?: (?:value|number))?|card security code)\b/.test(text) ||
752
+ /\b(?:card number|payment card|credit card|debit card)\b/.test(text));
753
+ }
754
+ function fieldOf(el, paymentContext = false) {
755
+ const autocomplete = (el.autocomplete ?? "").toLowerCase();
756
+ const autocompleteTokens = new Set(autocomplete.trim().split(/\s+/).filter(Boolean));
757
+ const hasAutocomplete = (...tokens) => tokens.some((token) => autocompleteTokens.has(token));
758
+ const type = (el.type ?? "").toLowerCase();
759
+ const text = candidateText(el).toLowerCase();
760
+ if (hasExplicitPaymentFieldSignal(el) ||
761
+ (paymentContext && /\b(?:security code|cvn|cid)\b/.test(text))) {
762
+ return "payment";
763
+ }
764
+ if (type === "password" || hasAutocomplete("current-password", "new-password")) {
765
+ return "password";
766
+ }
767
+ if (type === "email" || hasAutocomplete("email") || /\be-?mail\b/.test(text))
768
+ return "email";
769
+ if (type === "tel" ||
770
+ [...autocompleteTokens].some((token) => token === "tel" || token.startsWith("tel-")) ||
771
+ /\b(phone|mobile|telephone)\b/.test(text))
772
+ return "phone";
773
+ if (type === "search" || /\bsearch\b/.test(text))
774
+ return "search";
775
+ if (hasAutocomplete("username") || /\b(user ?name|handle)\b/.test(text))
776
+ return "username";
777
+ if (/\b(card|payment|cc-)/.test(`${autocomplete} ${text}`))
778
+ return "payment";
779
+ if (hasAutocomplete("address-level2") || /\b(city|town)\b/.test(text))
780
+ return "city";
781
+ if (hasAutocomplete("address-level1") || /\b(state|province|region)\b/.test(text))
782
+ return "region";
783
+ if (hasAutocomplete("postal-code") || /\b(zip|postal)\b/.test(text))
784
+ return "postal";
785
+ if (hasAutocomplete("country", "country-name") || /\bcountry\b/.test(text))
786
+ return "country";
787
+ // `address-line2` is the optional apartment/suite field in the HTML
788
+ // autocomplete vocabulary. Shopify renders it directly below a required
789
+ // Places-backed `address-line1` combobox. Treating both as `address` makes
790
+ // the compact action map advertise two interchangeable street fields, and a
791
+ // caller can put the delivery address in the apartment field instead. Keep
792
+ // `address` reserved for the required street-address / line-1 control; the
793
+ // optional secondary line is intentionally not a checkout fill target.
794
+ if (hasAutocomplete("address-line2", "address-line3") ||
795
+ /\b(?:apartment|apt\.?|suite|unit|floor|building)\b/.test(text) ||
796
+ /\boptional\b/.test(text))
797
+ return undefined;
798
+ if (hasAutocomplete("street-address", "address-line1") || /\b(address|street)\b/.test(text))
799
+ return "address";
800
+ if (type === "date" ||
801
+ [...autocompleteTokens].some((token) => token.startsWith("bday")) ||
802
+ /\bdate\b/.test(text))
803
+ return "date";
804
+ if (/\b(quantity|qty)\b/.test(text))
805
+ return "quantity";
806
+ if (/\b(promo|coupon|discount)\b/.test(text))
807
+ return "promo";
808
+ if ([...autocompleteTokens].some((token) => token === "name" || token.endsWith("-name")) ||
809
+ /\b(full )?name\b/.test(text))
810
+ return "name";
811
+ return undefined;
812
+ }
813
+ /** A code-owned page-stage enum; it never forwards the URL or page copy. */
814
+ export function safeStageV2(url, elements) {
815
+ let pathname = "";
816
+ try {
817
+ pathname = decodeURIComponent(new URL(url).pathname).toLowerCase();
818
+ }
819
+ catch { }
820
+ if (/(?:^|\/)(?:success|complete|thank-you|thank_you|finished|done)(?:\.(?:php|html?))?(?:\/|$)/.test(pathname)) {
821
+ return "complete";
822
+ }
823
+ const actionableElements = elements.filter((element) => element.visible === true && element.topmost !== false);
824
+ const routeStage = checkoutStageFromUrlV2(url);
825
+ const paymentContext = routeStage !== null ||
826
+ actionableElements.some((element) => hasExplicitPaymentFieldSignal(element));
827
+ const checkoutFields = new Set([
828
+ "address",
829
+ "city",
830
+ "region",
831
+ "postal",
832
+ "country",
833
+ "payment",
834
+ ]);
835
+ const hasCheckoutField = actionableElements.some((el) => {
836
+ const role = roleOf(el);
837
+ const field = fieldOf(el, paymentContext);
838
+ return ((role === "textbox" || role === "select") && field !== undefined && checkoutFields.has(field));
839
+ });
840
+ const authFields = actionableElements.filter((el) => {
841
+ const role = roleOf(el);
842
+ if (role !== "textbox" && role !== "select")
843
+ return false;
844
+ const field = fieldOf(el, paymentContext);
845
+ return field === "email" || field === "username" || field === "password";
846
+ });
847
+ const hasPasswordField = authFields.some((el) => fieldOf(el, paymentContext) === "password");
848
+ const hasScopedAuthForm = actionableElements.some((el) => {
849
+ if (roleOf(el) !== "button" || (intentOf(el) !== "login" && intentOf(el) !== "signup")) {
850
+ return false;
851
+ }
852
+ const ownerId = el.formId ?? el.containerId;
853
+ const framePath = el.framePath ?? null;
854
+ return (ownerId !== undefined &&
855
+ ownerId !== null &&
856
+ authFields.some((field) => (field.formId ?? field.containerId) === ownerId &&
857
+ (field.framePath ?? null) === framePath));
858
+ });
859
+ if (hasPasswordField || hasScopedAuthForm)
860
+ return "auth";
861
+ if (routeStage !== null)
862
+ return routeStage;
863
+ const hasPaymentField = actionableElements.some((el) => {
864
+ const role = roleOf(el);
865
+ return (role === "textbox" || role === "select") && fieldOf(el, paymentContext) === "payment";
866
+ });
867
+ const hasCheckoutAction = actionableElements.some((el) => roleOf(el) === "button" && intentOf(el) === "checkout");
868
+ if (hasPaymentField || (hasCheckoutAction && hasCheckoutField))
869
+ return "checkout";
870
+ if (actionableElements.some((el) => intentOf(el) === "add_to_cart"))
871
+ return "browse";
872
+ if (actionableElements.some((el) => roleOf(el) === "textbox" || roleOf(el) === "select"))
873
+ return "form";
874
+ return "browse";
875
+ }
876
+ export function checkoutStageFromUrlV2(url) {
877
+ let pathname = "";
878
+ try {
879
+ pathname = decodeURIComponent(new URL(url).pathname).toLowerCase();
880
+ }
881
+ catch {
882
+ return null;
883
+ }
884
+ if (/(?:^|\/)(?:checkout|secure[-_]?checkout|payment|order[-_]?review|order\/review)(?:\.(?:php|html?))?(?:\/|$)/i.test(pathname)) {
885
+ return "checkout";
886
+ }
887
+ if (/(?:^|\/)(?:cart|shopping[-_]?cart|view[-_]?cart|basket|bag)(?:\.(?:php|html?))?(?:\/|$)/i.test(pathname)) {
888
+ return "cart";
889
+ }
890
+ return null;
891
+ }
892
+ function frameOf(el, pageOrigin) {
893
+ if (el.frameOrigin === null || el.frameOrigin === undefined)
894
+ return "main";
895
+ return el.frameOrigin === pageOrigin ? "same_origin" : "cross_origin";
896
+ }
897
+ export function buildSafeControlsV2(args) {
898
+ const rows = [];
899
+ const paymentContext = checkoutStageFromUrlV2(args.pageUrl ?? "") !== null ||
900
+ args.elements.some((element) => hasExplicitPaymentFieldSignal(element));
901
+ for (const el of args.elements) {
902
+ if (el.visible !== true || el.topmost === false)
903
+ continue;
904
+ const role = roleOf(el);
905
+ const legacy = args.legacyRefs.get(el);
906
+ const ref = args.handles.get(el);
907
+ if (role === null || legacy === undefined || ref === undefined)
908
+ continue;
909
+ const state = stateOf(el);
910
+ const action = intentOf(el);
911
+ const field = fieldOf(el, paymentContext);
912
+ const cardChoice = el.cardRadioGroup;
913
+ // Native TypeScript port of browse-use's compact DOM formatting: the
914
+ // already CDP-derived interactive inventory supplies each visible control's
915
+ // descendant/accessibility name. This pass binds that name to its own live
916
+ // element, so no cross-serializer tag/role fallback can swap labels.
917
+ const label = controlLabelV2(controlDescription(el));
918
+ const row = {
919
+ role,
920
+ visibility: el.inViewport ? "viewport" : "near",
921
+ frame: frameOf(el, args.pageOrigin),
922
+ ...(state === undefined ? {} : { state }),
923
+ ...(action === undefined ? {} : { action }),
924
+ ...(field === undefined ? {} : { field }),
925
+ ...(label === undefined ? {} : { label }),
926
+ ...(cardChoice === null || cardChoice === undefined
927
+ ? {}
928
+ : { choice: `${cardChoice.position}/${cardChoice.total}` }),
929
+ };
930
+ // A signup/login action is the useful first move on a landing page. Put it
931
+ // ahead of unlabeled navigation chrome while preserving the existing
932
+ // viewport and role ordering within each group.
933
+ const actionPriority = action === "signup" ? -20 : action === "login" ? -10 : 0;
934
+ rows.push({
935
+ ref,
936
+ legacy,
937
+ row,
938
+ priority: actionPriority + (el.inViewport ? 0 : 10) + (role === "button" ? 0 : 1),
939
+ });
940
+ }
941
+ rows.sort((a, b) => a.priority - b.priority || a.legacy.localeCompare(b.legacy));
942
+ const byRef = new Map();
943
+ const safeRows = rows.map(({ ref, legacy, row }) => {
944
+ byRef.set(ref, legacy);
945
+ return { ref, ...row };
946
+ });
947
+ return { rows: safeRows, byRef };
948
+ }
949
+ export function encodeV2Page(args) {
950
+ const offset = args.offset ?? 0;
951
+ if (args.unchanged === true && offset === 0) {
952
+ const payload = {
953
+ format: "compact-v2",
954
+ url: args.pageUrl ?? "",
955
+ text: "",
956
+ session_id: args.sessionId,
957
+ stage: args.stage,
958
+ ...(args.startMetadata?.hint === undefined ? {} : { hint: args.startMetadata.hint }),
959
+ ...(args.startMetadata?.userEmail === undefined
960
+ ? {}
961
+ : { user_email: args.startMetadata.userEmail }),
962
+ ...(args.startMetadata?.hintOverflow === undefined
963
+ ? {}
964
+ : { hint_overflow: args.startMetadata.hintOverflow }),
965
+ ...(args.semantics === undefined || Object.keys(args.semantics).length === 0
966
+ ? {}
967
+ : { semantic: args.semantics }),
968
+ delta: true,
969
+ };
970
+ if (!compactV2PayloadWithinBudget(payload)) {
971
+ throw new Error("compact-v2 budget metadata exceeded");
972
+ }
973
+ return {
974
+ payload,
975
+ nextOffset: 0,
976
+ };
977
+ }
978
+ const visible = [];
979
+ // Initial observations stay decisively small. Overflow is retrieved through
980
+ // the in-MCP query cursor, never a shell-readable snapshot file.
981
+ const pageEnd = offset === 0 ? Math.min(args.rows.length, FIRST_PAGE_ROW_LIMIT) : args.rows.length;
982
+ for (let index = offset; index < pageEnd; index += 1) {
983
+ const candidate = args.rows[index];
984
+ const remainder = args.rows.length - (index + 1);
985
+ const payload = {
986
+ format: "compact-v2",
987
+ url: args.pageUrl ?? "",
988
+ text: "",
989
+ session_id: args.sessionId,
990
+ stage: args.stage,
991
+ ...(args.startMetadata?.hint === undefined ? {} : { hint: args.startMetadata.hint }),
992
+ ...(args.startMetadata?.userEmail === undefined
993
+ ? {}
994
+ : { user_email: args.startMetadata.userEmail }),
995
+ ...(args.startMetadata?.hintOverflow === undefined
996
+ ? {}
997
+ : { hint_overflow: args.startMetadata.hintOverflow }),
998
+ ...(args.semantics === undefined || Object.keys(args.semantics).length === 0
999
+ ? {}
1000
+ : { semantic: args.semantics }),
1001
+ safe_table: [...visible, wireControl(candidate)],
1002
+ ...(remainder > 0
1003
+ ? { overflow: { remaining: remainder, next_cursor: args.cursorFor(index + 1) } }
1004
+ : {}),
1005
+ };
1006
+ if (!compactV2PayloadWithinBudget(payload))
1007
+ break;
1008
+ visible.push(wireControl(candidate));
1009
+ }
1010
+ const nextOffset = offset + visible.length;
1011
+ const remaining = args.rows.length - nextOffset;
1012
+ const payload = {
1013
+ format: "compact-v2",
1014
+ url: args.pageUrl ?? "",
1015
+ text: "",
1016
+ session_id: args.sessionId,
1017
+ stage: args.stage,
1018
+ ...(args.startMetadata?.hint === undefined ? {} : { hint: args.startMetadata.hint }),
1019
+ ...(args.startMetadata?.userEmail === undefined
1020
+ ? {}
1021
+ : { user_email: args.startMetadata.userEmail }),
1022
+ ...(args.startMetadata?.hintOverflow === undefined
1023
+ ? {}
1024
+ : { hint_overflow: args.startMetadata.hintOverflow }),
1025
+ ...(args.semantics === undefined || Object.keys(args.semantics).length === 0
1026
+ ? {}
1027
+ : { semantic: args.semantics }),
1028
+ safe_table: visible,
1029
+ ...(remaining > 0 ? { overflow: { remaining, next_cursor: args.cursorFor(nextOffset) } } : {}),
1030
+ };
1031
+ // The fixed fields are deliberately tiny, so failure means a hostilely long
1032
+ // session id/cursor. Fail closed rather than exceeding the wire contract.
1033
+ if (!compactV2PayloadWithinBudget(payload)) {
1034
+ throw new Error("compact-v2 budget metadata exceeded");
1035
+ }
1036
+ return { payload, nextOffset };
1037
+ }
1038
+ //# sourceMappingURL=compact-observation-v2.js.map