@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.
- package/README.md +139 -62
- package/assets/login/vnc-input.js +9 -0
- package/assets/login/vnc.html +214 -202
- package/assets/screencaps/connect-walkthrough.png +0 -0
- package/assets/screencaps/connect-walkthrough.svg +1 -1
- package/dist/api-client.d.ts +26 -3
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +19 -31
- package/dist/api-client.js.map +1 -1
- package/dist/bin.js +2 -3
- package/dist/bin.js.map +1 -1
- package/dist/bot/browser.d.ts +134 -77
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +1788 -1543
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/compact-observation-v2.d.ts +167 -0
- package/dist/bot/compact-observation-v2.d.ts.map +1 -0
- package/dist/bot/compact-observation-v2.js +1038 -0
- package/dist/bot/compact-observation-v2.js.map +1 -0
- package/dist/bot/credential-shape.d.ts +2 -0
- package/dist/bot/credential-shape.d.ts.map +1 -1
- package/dist/bot/credential-shape.js +21 -8
- package/dist/bot/credential-shape.js.map +1 -1
- package/dist/bot/element-fingerprint.d.ts +9 -0
- package/dist/bot/element-fingerprint.d.ts.map +1 -0
- package/dist/bot/element-fingerprint.js +147 -0
- package/dist/bot/element-fingerprint.js.map +1 -0
- package/dist/bot/email-verification.d.ts +7 -1
- package/dist/bot/email-verification.d.ts.map +1 -1
- package/dist/bot/email-verification.js +48 -14
- package/dist/bot/email-verification.js.map +1 -1
- package/dist/bot/google-login.d.ts +28 -52
- package/dist/bot/google-login.d.ts.map +1 -1
- package/dist/bot/google-login.js +246 -1119
- package/dist/bot/google-login.js.map +1 -1
- package/dist/bot/install-completion.d.ts +1 -2
- package/dist/bot/install-completion.d.ts.map +1 -1
- package/dist/bot/install-completion.js +1 -2
- package/dist/bot/install-completion.js.map +1 -1
- package/dist/bot/login-state.d.ts +13 -7
- package/dist/bot/login-state.d.ts.map +1 -1
- package/dist/bot/login-state.js +50 -114
- package/dist/bot/login-state.js.map +1 -1
- package/dist/bot/oauth-providers.d.ts +1 -1
- package/dist/bot/oauth-providers.d.ts.map +1 -1
- package/dist/bot/oauth-providers.js +3 -3
- package/dist/bot/oauth-providers.js.map +1 -1
- package/dist/bot/oauth-scope.d.ts +4 -0
- package/dist/bot/oauth-scope.d.ts.map +1 -0
- package/dist/bot/oauth-scope.js +58 -0
- package/dist/bot/oauth-scope.js.map +1 -0
- package/dist/bot/operator-browser-watchdog.d.ts +127 -0
- package/dist/bot/operator-browser-watchdog.d.ts.map +1 -0
- package/dist/bot/operator-browser-watchdog.js +438 -0
- package/dist/bot/operator-browser-watchdog.js.map +1 -0
- package/dist/bot/owner-process-reaper-worker.d.ts +2 -0
- package/dist/bot/owner-process-reaper-worker.d.ts.map +1 -0
- package/dist/bot/owner-process-reaper-worker.js +17 -0
- package/dist/bot/owner-process-reaper-worker.js.map +1 -0
- package/dist/bot/owner-process-reaper.d.ts +103 -0
- package/dist/bot/owner-process-reaper.d.ts.map +1 -0
- package/dist/bot/owner-process-reaper.js +1058 -0
- package/dist/bot/owner-process-reaper.js.map +1 -0
- package/dist/bot/pay-operator.d.ts +13 -2
- package/dist/bot/pay-operator.d.ts.map +1 -1
- package/dist/bot/pay-operator.js +319 -123
- package/dist/bot/pay-operator.js.map +1 -1
- package/dist/bot/profile.d.ts +10 -1
- package/dist/bot/profile.d.ts.map +1 -1
- package/dist/bot/profile.js +137 -11
- package/dist/bot/profile.js.map +1 -1
- package/dist/bot/promote-to-skill.js +1 -1
- package/dist/bot/promote-to-skill.js.map +1 -1
- package/dist/bot/provision-session.d.ts +60 -164
- package/dist/bot/provision-session.d.ts.map +1 -1
- package/dist/bot/provision-session.js +2049 -1546
- package/dist/bot/provision-session.js.map +1 -1
- package/dist/bot/remote-login-display.d.ts +61 -0
- package/dist/bot/remote-login-display.d.ts.map +1 -0
- package/dist/bot/remote-login-display.js +765 -0
- package/dist/bot/remote-login-display.js.map +1 -0
- package/dist/bot/session/hosts.d.ts +8 -0
- package/dist/bot/session/hosts.d.ts.map +1 -0
- package/dist/bot/session/hosts.js +36 -0
- package/dist/bot/session/hosts.js.map +1 -0
- package/dist/bot/session/lifecycle.d.ts +56 -0
- package/dist/bot/session/lifecycle.d.ts.map +1 -0
- package/dist/bot/session/lifecycle.js +962 -0
- package/dist/bot/session/lifecycle.js.map +1 -0
- package/dist/bot/session/model.d.ts +175 -0
- package/dist/bot/session/model.d.ts.map +1 -0
- package/dist/bot/session/model.js +77 -0
- package/dist/bot/session/model.js.map +1 -0
- package/dist/install/cli.d.ts +29 -9
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +243 -322
- package/dist/install/cli.js.map +1 -1
- package/dist/install/interactive.d.ts +1 -0
- package/dist/install/interactive.d.ts.map +1 -1
- package/dist/install/interactive.js +19 -5
- package/dist/install/interactive.js.map +1 -1
- package/dist/install/ui.d.ts.map +1 -1
- package/dist/install/ui.js +3 -15
- package/dist/install/ui.js.map +1 -1
- package/dist/operator-env.js +1 -1
- package/dist/operator-env.js.map +1 -1
- package/dist/server-instance-registry.d.ts +103 -0
- package/dist/server-instance-registry.d.ts.map +1 -0
- package/dist/server-instance-registry.js +392 -0
- package/dist/server-instance-registry.js.map +1 -0
- package/dist/server.d.ts +15 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +164 -62
- package/dist/server.js.map +1 -1
- package/dist/session-guard.d.ts +28 -0
- package/dist/session-guard.d.ts.map +1 -0
- package/dist/session-guard.js +84 -0
- package/dist/session-guard.js.map +1 -0
- package/dist/session.d.ts +57 -13
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +246 -127
- package/dist/session.js.map +1 -1
- package/dist/tools/audit-log.d.ts +16 -2
- package/dist/tools/audit-log.d.ts.map +1 -1
- package/dist/tools/audit-log.js +223 -13
- package/dist/tools/audit-log.js.map +1 -1
- package/dist/tools/audit-rollup.d.ts +80 -0
- package/dist/tools/audit-rollup.d.ts.map +1 -0
- package/dist/tools/audit-rollup.js +0 -0
- package/dist/tools/audit-rollup.js.map +1 -0
- package/dist/tools/fetch-credential.d.ts +37 -0
- package/dist/tools/fetch-credential.d.ts.map +1 -0
- package/dist/tools/fetch-credential.js +153 -0
- package/dist/tools/fetch-credential.js.map +1 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +7 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/operate-pay.d.ts +4 -4
- package/dist/tools/operate-pay.d.ts.map +1 -1
- package/dist/tools/operate-pay.js +163 -96
- package/dist/tools/operate-pay.js.map +1 -1
- package/dist/tools/provision-drive.d.ts +43 -35
- package/dist/tools/provision-drive.d.ts.map +1 -1
- package/dist/tools/provision-drive.js +268 -164
- package/dist/tools/provision-drive.js.map +1 -1
- package/dist/tools/use-credential.d.ts.map +1 -1
- package/dist/tools/use-credential.js +3 -2
- package/dist/tools/use-credential.js.map +1 -1
- package/package.json +4 -6
- package/dist/bot/operator-direct-identity.d.ts +0 -26
- package/dist/bot/operator-direct-identity.d.ts.map +0 -1
- package/dist/bot/operator-direct-identity.js +0 -120
- package/dist/bot/operator-direct-identity.js.map +0 -1
- package/dist/bot/operator-profile-pool.d.ts +0 -97
- package/dist/bot/operator-profile-pool.d.ts.map +0 -1
- package/dist/bot/operator-profile-pool.js +0 -855
- package/dist/bot/operator-profile-pool.js.map +0 -1
- package/dist/bot/xvfb.d.ts +0 -13
- package/dist/bot/xvfb.d.ts.map +0 -1
- package/dist/bot/xvfb.js +0 -146
- 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
|