@opencode/browse 0.0.6-dev.27 → 0.0.6
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/dist/{cli-n0qhph13.mjs → cli-jp1jev7t.mjs} +10 -67
- package/dist/cli.mjs +9 -9
- package/dist/{helpers-zeewknn2.mjs → helpers-ynamkf6t.mjs} +1 -1
- package/dist/{main-3g2t8jtq.mjs → main-0g9jqyts.mjs} +2 -2
- package/dist/{main-a6mpfd6z.mjs → main-3cza6vx1.mjs} +2 -2
- package/dist/{main-xzc7qp1c.mjs → main-605j7tjk.mjs} +4 -34
- package/dist/{main-cxrsjs2z.mjs → main-hanwxg2c.mjs} +33 -468
- package/dist/{main-a4yj9bpn.mjs → main-njp635rz.mjs} +3 -3
- package/dist/{main-x96gxahz.mjs → main-q5j1am67.mjs} +2 -2
- package/dist/main-ry14nc9q.mjs +4 -0
- package/dist/{main-bq8az9yr.mjs → main-yv64m9we.mjs} +1 -1
- package/dist/{mcp-za7ymfdd.mjs → mcp-4w28g9h2.mjs} +11 -78
- package/dist/{relay-xqg264d4.mjs → relay-21t4e4tv.mjs} +35 -1104
- package/dist/{runtime-update-gteb0gp8.mjs → runtime-update-zjx0kaj7.mjs} +4 -4
- package/dist/{setup-zka6z53q.mjs → setup-0z9803a3.mjs} +5 -5
- package/dist/{skill-text-kyjcembk.mjs → skill-text-rc6te209.mjs} +0 -75
- package/dist/{supervisor-w6hs0gbw.mjs → supervisor-9qwn7rb1.mjs} +3 -3
- package/dist/{update-pj25xbh4.mjs → update-sxawv22k.mjs} +3 -3
- package/dist/{version-1hb8s8jj.mjs → version-8gwa4g0s.mjs} +1 -1
- package/package.json +1 -1
- package/skills/browse/SKILL.md +0 -75
- package/dist/main-vp3devjb.mjs +0 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
opencodeBrowserVersion2,
|
|
3
3
|
opencodeBrowserBuildId2
|
|
4
|
-
} from "./main-
|
|
4
|
+
} from "./main-ry14nc9q.mjs";
|
|
5
5
|
import {
|
|
6
6
|
dataRoot,
|
|
7
7
|
oldStoreMarker,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
setStoredMode,
|
|
20
20
|
autoUpdateStatus,
|
|
21
21
|
createAutoUpdater
|
|
22
|
-
} from "./main-
|
|
22
|
+
} from "./main-3cza6vx1.mjs";
|
|
23
23
|
import {
|
|
24
24
|
extensionProtocolVersion,
|
|
25
25
|
minExtensionProtocolVersion,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
relayToken,
|
|
44
44
|
secretsEqual,
|
|
45
45
|
ensurePrivateDirectory
|
|
46
|
-
} from "./main-
|
|
46
|
+
} from "./main-yv64m9we.mjs";
|
|
47
47
|
import {
|
|
48
48
|
ExecuteRequest,
|
|
49
49
|
SessionAdoptRequest,
|
|
@@ -57,7 +57,6 @@ import {
|
|
|
57
57
|
NetworkStartRequest,
|
|
58
58
|
NetworkSessionRequest,
|
|
59
59
|
NetworkStopRequest,
|
|
60
|
-
DeadboltLoginsRequest,
|
|
61
60
|
AuthProfileRequest,
|
|
62
61
|
AuthRefreshRequest,
|
|
63
62
|
AuthRunRequest,
|
|
@@ -93,7 +92,7 @@ import {
|
|
|
93
92
|
isRestrictedTarget2,
|
|
94
93
|
isRestrictedUrl2,
|
|
95
94
|
dataUrlDocument2
|
|
96
|
-
} from "./main-
|
|
95
|
+
} from "./main-605j7tjk.mjs";
|
|
97
96
|
import {
|
|
98
97
|
relayHostname,
|
|
99
98
|
isLocalPeer,
|
|
@@ -115,7 +114,7 @@ import {
|
|
|
115
114
|
wrapToken,
|
|
116
115
|
installedDevExtension,
|
|
117
116
|
syncDevExtension
|
|
118
|
-
} from "./main-
|
|
117
|
+
} from "./main-0g9jqyts.mjs";
|
|
119
118
|
import {
|
|
120
119
|
SettingsInputError,
|
|
121
120
|
parseSettingChanges,
|
|
@@ -123,17 +122,6 @@ import {
|
|
|
123
122
|
settingsToExtension,
|
|
124
123
|
describeSettings,
|
|
125
124
|
describeChanges,
|
|
126
|
-
DeadboltError,
|
|
127
|
-
readClient,
|
|
128
|
-
connect,
|
|
129
|
-
parseDeadboltName,
|
|
130
|
-
originOnSite,
|
|
131
|
-
pickLogin,
|
|
132
|
-
originsFromPatterns,
|
|
133
|
-
listLogins,
|
|
134
|
-
pickPasskey,
|
|
135
|
-
isBase64Url,
|
|
136
|
-
randomName,
|
|
137
125
|
terminateChildProcess,
|
|
138
126
|
SecretCollector,
|
|
139
127
|
looksLikeTokenSegment,
|
|
@@ -153,16 +141,16 @@ import {
|
|
|
153
141
|
checkNavigationUrl,
|
|
154
142
|
readSecret,
|
|
155
143
|
secretText
|
|
156
|
-
} from "./main-
|
|
144
|
+
} from "./main-hanwxg2c.mjs";
|
|
157
145
|
import {
|
|
158
146
|
defaultJournalBaseDir,
|
|
159
147
|
makeJournalEntry,
|
|
160
148
|
appendJournalEntry,
|
|
161
149
|
startJournalRetention
|
|
162
|
-
} from "./main-
|
|
150
|
+
} from "./main-njp635rz.mjs";
|
|
163
151
|
import {
|
|
164
152
|
createUpdateWatcher2
|
|
165
|
-
} from "./main-
|
|
153
|
+
} from "./main-q5j1am67.mjs";
|
|
166
154
|
|
|
167
155
|
// src/relay.ts
|
|
168
156
|
import crypto10 from "node:crypto";
|
|
@@ -4221,15 +4209,6 @@ async function detectChallenge(page, options) {
|
|
|
4221
4209
|
recommendation: current.kind === "block" ? "stop" : "handoff"
|
|
4222
4210
|
};
|
|
4223
4211
|
}
|
|
4224
|
-
var requestEvents = new Set(["request", "requestfinished", "requestfailed", "response", "websocket", "route"]);
|
|
4225
|
-
function pageHookCall(fn, args) {
|
|
4226
|
-
const name = typeof fn.name === "string" ? fn.name.replace(/^bound /, "") : "";
|
|
4227
|
-
if (["route", "routeFromHAR", "routeWebSocket", "addInitScript", "exposeBinding", "exposeFunction", "newCDPSession", "waitForRequest", "waitForResponse"].includes(name))
|
|
4228
|
-
return `${name}()`;
|
|
4229
|
-
if (["on", "once", "addListener", "prependListener", "waitForEvent"].includes(name) && typeof args[0] === "string" && requestEvents.has(args[0]))
|
|
4230
|
-
return `${name}("${args[0]}")`;
|
|
4231
|
-
return;
|
|
4232
|
-
}
|
|
4233
4212
|
function pageVisibleApiSourceWarnings(code) {
|
|
4234
4213
|
const warnings = [];
|
|
4235
4214
|
for (const name of ["addInitScript", "exposeFunction", "exposeBinding"]) {
|
|
@@ -8739,9 +8718,7 @@ function buildSandboxGlobals(deps) {
|
|
|
8739
8718
|
stop: (target) => stop(target ?? page)
|
|
8740
8719
|
},
|
|
8741
8720
|
secrets: {
|
|
8742
|
-
type: (target, name, typeOptions) => deps.secrets.typeSecret(page, target, name, typeOptions)
|
|
8743
|
-
signIn: (signInOptions) => deps.secrets.signIn(page, signInOptions),
|
|
8744
|
-
usePasskey: (passkeyOptions) => deps.secrets.usePasskey(passkeyOptions?.page ?? page, passkeyOptions)
|
|
8721
|
+
type: (target, name, typeOptions) => deps.secrets.typeSecret(page, target, name, typeOptions)
|
|
8745
8722
|
},
|
|
8746
8723
|
handoffTracker
|
|
8747
8724
|
};
|
|
@@ -8749,554 +8726,6 @@ function buildSandboxGlobals(deps) {
|
|
|
8749
8726
|
|
|
8750
8727
|
// src/execute/secrets-bridge.ts
|
|
8751
8728
|
import { Effect as Effect10 } from "effect";
|
|
8752
|
-
|
|
8753
|
-
// src/auth/sign-in.ts
|
|
8754
|
-
var googleSite = "accounts.google.com";
|
|
8755
|
-
var handoffHint = 'Hand it to the user with handoff("Finish signing in, then click Continue"), then check the page.';
|
|
8756
|
-
var readPageScript = () => {
|
|
8757
|
-
const limit = 20000;
|
|
8758
|
-
const all = [];
|
|
8759
|
-
const walk = (root) => {
|
|
8760
|
-
for (const element of Array.from(root.querySelectorAll("*"))) {
|
|
8761
|
-
if (all.length >= limit)
|
|
8762
|
-
return;
|
|
8763
|
-
all.push(element);
|
|
8764
|
-
if (element.shadowRoot)
|
|
8765
|
-
walk(element.shadowRoot);
|
|
8766
|
-
}
|
|
8767
|
-
};
|
|
8768
|
-
walk(document);
|
|
8769
|
-
const visible = (element) => {
|
|
8770
|
-
const rect = element.getBoundingClientRect();
|
|
8771
|
-
if (rect.width < 2 || rect.height < 2)
|
|
8772
|
-
return false;
|
|
8773
|
-
const style = getComputedStyle(element);
|
|
8774
|
-
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity) < 0.05)
|
|
8775
|
-
return false;
|
|
8776
|
-
return !element.closest("[hidden], [inert], [aria-hidden='true']");
|
|
8777
|
-
};
|
|
8778
|
-
const describe = (element) => {
|
|
8779
|
-
const input = element;
|
|
8780
|
-
const labelled = (element.getAttribute("aria-labelledby") ?? "").split(/\s+/).filter(Boolean).map((id) => element.ownerDocument.getElementById(id)?.textContent ?? "").join(" ");
|
|
8781
|
-
return [
|
|
8782
|
-
element.getAttribute("aria-label"),
|
|
8783
|
-
labelled,
|
|
8784
|
-
Array.from(input.labels ?? []).map((label) => label.textContent).join(" "),
|
|
8785
|
-
input.placeholder,
|
|
8786
|
-
input.name,
|
|
8787
|
-
element.id,
|
|
8788
|
-
element.getAttribute("autocomplete"),
|
|
8789
|
-
element.getAttribute("data-testid")
|
|
8790
|
-
].filter(Boolean).join(" ").toLowerCase().replace(/\s+/g, " ");
|
|
8791
|
-
};
|
|
8792
|
-
const ownText = (element) => `${element.innerText ?? element.textContent ?? ""} ${element.getAttribute("aria-label") ?? ""} ${element.value && element.type === "submit" ? element.value : ""}`.trim().toLowerCase().replace(/\s+/g, " ");
|
|
8793
|
-
const inputs = all.filter((element) => element instanceof HTMLInputElement && !element.disabled && !element.readOnly && visible(element) && !["hidden", "checkbox", "radio", "submit", "button", "image", "reset", "file", "range", "color", "search"].includes(element.type));
|
|
8794
|
-
const autocomplete = (element) => (element.getAttribute("autocomplete") ?? "").toLowerCase();
|
|
8795
|
-
const password = inputs.find((element) => element.type === "password" && !/new-password/.test(autocomplete(element)) && !/confirm|repeat|re-?enter|new password|retype/.test(describe(element)));
|
|
8796
|
-
const codeWords = /one[- ]?time|\botp\b|totp|\b2fa\b|\bmfa\b|two[- ]factor|verification|security code|auth\w* code|passcode|\bpin\b|digit|\bcode\b/;
|
|
8797
|
-
const singleBoxes = inputs.filter((element) => element.maxLength === 1 && element.type !== "password");
|
|
8798
|
-
const otpSingle = inputs.find((element) => element !== password && element.maxLength !== 1 && (autocomplete(element).includes("one-time-code") || codeWords.test(describe(element)) && !/zip|postal|promo|coupon|country|area|referral|invite/.test(describe(element))));
|
|
8799
|
-
const otp = singleBoxes.length >= 4 && singleBoxes.length <= 8 ? singleBoxes : otpSingle ? [otpSingle] : [];
|
|
8800
|
-
const usernameCandidates = inputs.filter((element) => element !== password && !otp.includes(element) && ["text", "email", "tel", ""].includes(element.type) && (/username|email|webauthn/.test(autocomplete(element)) || element.type === "email" || /user|e-?mail|login|identifier|account|phone|handle/.test(describe(element))) && !/search|newsletter|subscribe|coupon|promo/.test(describe(element)));
|
|
8801
|
-
const form = password?.form ?? null;
|
|
8802
|
-
const username = usernameCandidates.find((element) => form && element.form === form) ?? usernameCandidates[0] ?? null;
|
|
8803
|
-
const clickables = all.filter((element) => (element instanceof HTMLButtonElement || element instanceof HTMLAnchorElement || element.getAttribute("role") === "button" || element.getAttribute("role") === "link" || element instanceof HTMLInputElement && (element.type === "submit" || element.type === "button")) && visible(element) && !element.disabled);
|
|
8804
|
-
const byText = (pattern) => clickables.find((element) => pattern.test(ownText(element)));
|
|
8805
|
-
const field = otp[0] ?? password ?? username ?? null;
|
|
8806
|
-
const submitFor = (target) => {
|
|
8807
|
-
if (!target)
|
|
8808
|
-
return null;
|
|
8809
|
-
const owner = target.form;
|
|
8810
|
-
if (owner) {
|
|
8811
|
-
const inForm = clickables.filter((element) => element.form === owner || owner.contains(element));
|
|
8812
|
-
const typed = inForm.find((element) => element instanceof HTMLButtonElement && element.type === "submit" || element instanceof HTMLInputElement && element.type === "submit");
|
|
8813
|
-
if (typed)
|
|
8814
|
-
return typed;
|
|
8815
|
-
}
|
|
8816
|
-
const words = /^(sign ?in|log ?in|login|next|continue|verify|submit|confirm|done|weiter|suivant)\b/;
|
|
8817
|
-
const following = clickables.filter((element) => target.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
8818
|
-
return following.find((element) => words.test(ownText(element))) ?? clickables.find((element) => words.test(ownText(element))) ?? null;
|
|
8819
|
-
};
|
|
8820
|
-
const frames = Array.from(document.querySelectorAll("iframe")).filter(visible);
|
|
8821
|
-
const captchaFrame = frames.find((frame) => {
|
|
8822
|
-
const src = frame.src;
|
|
8823
|
-
if (/size=invisible/.test(src) || frame.closest(".grecaptcha-badge"))
|
|
8824
|
-
return false;
|
|
8825
|
-
if (/challenges\.cloudflare\.com/.test(src)) {
|
|
8826
|
-
const response = frame.ownerDocument.querySelector("input[name='cf-turnstile-response']");
|
|
8827
|
-
return !response?.value;
|
|
8828
|
-
}
|
|
8829
|
-
return /\/recaptcha\/(api2|enterprise)\/(anchor|bframe)|hcaptcha\.com|arkoselabs|funcaptcha|geo\.captcha-delivery|captcha/.test(src);
|
|
8830
|
-
});
|
|
8831
|
-
const captchaWidget = all.find((element) => /^(px-captcha|sec-cpt-if|ddv1-captcha-container)$/.test(element.id) && visible(element));
|
|
8832
|
-
const errors = all.filter((element) => (element.getAttribute("role") === "alert" || element.getAttribute("aria-live") === "assertive" || /(^|[\s_-])(error|alert|invalid)([\s_-]|$)/i.test(typeof element.className === "string" ? element.className : "")) && visible(element));
|
|
8833
|
-
const handles = {
|
|
8834
|
-
password: password ?? null,
|
|
8835
|
-
username,
|
|
8836
|
-
otp,
|
|
8837
|
-
submit: submitFor(field),
|
|
8838
|
-
passwordToggle: byText(/use (your )?password|(sign|log) ?in with (a |your )?password|enter (your )?password( instead)?|password instead|try another way/) ?? null,
|
|
8839
|
-
google: byText(/(sign ?in|log ?in|continue|sign ?up) (with|using|via) google|^google$/) ?? null,
|
|
8840
|
-
signInLink: byText(/^(sign ?in|log ?in|login)$/) ?? null,
|
|
8841
|
-
passkeyButton: byText(/(sign ?in|log ?in|continue) (with|using) (a |your )?passkey|use (a |your )?passkey|^passkey$/) ?? null
|
|
8842
|
-
};
|
|
8843
|
-
const state = {
|
|
8844
|
-
captcha: captchaFrame ? captchaFrame.src.slice(0, 120) : captchaWidget ? `#${captchaWidget.id}` : null,
|
|
8845
|
-
hasPassword: Boolean(password),
|
|
8846
|
-
hasUsername: Boolean(username),
|
|
8847
|
-
usernameFilled: username?.value ?? "",
|
|
8848
|
-
otpCount: otp.length,
|
|
8849
|
-
hasPasswordToggle: Boolean(handles.passwordToggle),
|
|
8850
|
-
hasGoogle: Boolean(handles.google),
|
|
8851
|
-
hasSignInLink: Boolean(handles.signInLink),
|
|
8852
|
-
hasPasskeyButton: Boolean(handles.passkeyButton),
|
|
8853
|
-
text: `${document.title}
|
|
8854
|
-
${document.body?.innerText ?? ""}`.slice(0, 4000).toLowerCase(),
|
|
8855
|
-
errorText: errors.map((element) => element.innerText ?? "").join(" ").replace(/\s+/g, " ").trim().slice(0, 120)
|
|
8856
|
-
};
|
|
8857
|
-
return { state, handles };
|
|
8858
|
-
};
|
|
8859
|
-
async function element(read, name) {
|
|
8860
|
-
const property = await read.handles.getProperty(name);
|
|
8861
|
-
return property.asElement() ?? undefined;
|
|
8862
|
-
}
|
|
8863
|
-
async function otpElements(read) {
|
|
8864
|
-
const list = await read.handles.getProperty("otp");
|
|
8865
|
-
const properties = await list.getProperties();
|
|
8866
|
-
const elements = [];
|
|
8867
|
-
for (const [key, value] of properties) {
|
|
8868
|
-
if (!/^\d+$/.test(key))
|
|
8869
|
-
continue;
|
|
8870
|
-
const handle = value.asElement();
|
|
8871
|
-
if (handle)
|
|
8872
|
-
elements.push(handle);
|
|
8873
|
-
}
|
|
8874
|
-
return elements;
|
|
8875
|
-
}
|
|
8876
|
-
async function readFrame(frame) {
|
|
8877
|
-
try {
|
|
8878
|
-
const handles = await frame.evaluateHandle(readPageScript);
|
|
8879
|
-
const state = await (await handles.getProperty("state")).jsonValue();
|
|
8880
|
-
return { frame, state, handles: await handles.getProperty("handles") };
|
|
8881
|
-
} catch {
|
|
8882
|
-
return;
|
|
8883
|
-
}
|
|
8884
|
-
}
|
|
8885
|
-
var actionable = (state) => Boolean(state.captcha || state.otpCount || state.hasPassword || state.hasUsername || state.hasPasswordToggle || state.hasGoogle || state.hasPasskeyButton);
|
|
8886
|
-
async function readPage(page, site) {
|
|
8887
|
-
const main = await readFrame(page.mainFrame());
|
|
8888
|
-
if (main && actionable(main.state))
|
|
8889
|
-
return main;
|
|
8890
|
-
for (const frame of page.frames()) {
|
|
8891
|
-
if (frame === page.mainFrame())
|
|
8892
|
-
continue;
|
|
8893
|
-
let origin;
|
|
8894
|
-
try {
|
|
8895
|
-
origin = new URL(frame.url()).origin;
|
|
8896
|
-
} catch {
|
|
8897
|
-
continue;
|
|
8898
|
-
}
|
|
8899
|
-
if (!originOnSite(origin, site))
|
|
8900
|
-
continue;
|
|
8901
|
-
const read = await readFrame(frame);
|
|
8902
|
-
if (read && actionable(read.state))
|
|
8903
|
-
return read;
|
|
8904
|
-
}
|
|
8905
|
-
return main;
|
|
8906
|
-
}
|
|
8907
|
-
var delay3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8908
|
-
function defaultSite(url) {
|
|
8909
|
-
let parsed;
|
|
8910
|
-
try {
|
|
8911
|
-
parsed = new URL(url);
|
|
8912
|
-
} catch {
|
|
8913
|
-
return;
|
|
8914
|
-
}
|
|
8915
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
8916
|
-
return;
|
|
8917
|
-
const host = parsed.hostname.toLowerCase();
|
|
8918
|
-
if (parsed.protocol === "http:" || /^[\d.]+$/.test(host) || host.includes(":") || host === "localhost" || host.endsWith(".localhost"))
|
|
8919
|
-
return parsed.origin;
|
|
8920
|
-
const labels = host.split(".");
|
|
8921
|
-
const twoLevel = labels.length >= 3 && /^(co|com|org|net|gov|ac|edu|ne|or)$/.test(labels.at(-2)) && labels.at(-1).length === 2;
|
|
8922
|
-
return labels.slice(twoLevel ? -3 : -2).join(".");
|
|
8923
|
-
}
|
|
8924
|
-
function codeKind(text) {
|
|
8925
|
-
if (/authenticator|authentication app|auth app|google authenticator|\btotp\b|one-time password app|security key app|2fa app|your app/.test(text))
|
|
8926
|
-
return "totp";
|
|
8927
|
-
if (/\bsms\b|text message|texted|we (just )?sent|sent (a|the|you a) code|sent to|check your (email|inbox|phone)|by email|via email|phone call|call you|ending in|whatsapp/.test(text))
|
|
8928
|
-
return "sent";
|
|
8929
|
-
return "unknown";
|
|
8930
|
-
}
|
|
8931
|
-
function verificationKind(text) {
|
|
8932
|
-
if (/approve (the|this|your) (sign|log)|check your phone|tap yes|duo push|push notification|approve (it|the request)|waiting for (your )?approval|open the .{0,20} app on your/.test(text))
|
|
8933
|
-
return "push_approval";
|
|
8934
|
-
if (/new device|unrecognized device|unfamiliar device|verify (it'?s|that it'?s) you|confirm (it'?s|your identity|that it'?s you)|verify your identity|security check/.test(text))
|
|
8935
|
-
return "new_device";
|
|
8936
|
-
if (/security question/.test(text))
|
|
8937
|
-
return "security_question";
|
|
8938
|
-
return;
|
|
8939
|
-
}
|
|
8940
|
-
function needsUser(step, why) {
|
|
8941
|
-
return { status: "needs_user", step, reason: `${why} ${handoffHint}` };
|
|
8942
|
-
}
|
|
8943
|
-
async function clickHandle(handle) {
|
|
8944
|
-
await handle.scrollIntoViewIfNeeded().catch(() => {});
|
|
8945
|
-
await handle.click({ timeout: 5000 }).catch(async () => {
|
|
8946
|
-
await handle.evaluate((node) => node.click());
|
|
8947
|
-
});
|
|
8948
|
-
}
|
|
8949
|
-
async function domSignature(frame) {
|
|
8950
|
-
return await frame.evaluate(() => `${document.querySelectorAll("input, iframe, [role='alert']").length}:${document.body?.innerText.length ?? 0}`).catch(() => "");
|
|
8951
|
-
}
|
|
8952
|
-
async function waitForProgress(page, field, timeoutMs) {
|
|
8953
|
-
const url = page.url();
|
|
8954
|
-
const until = Date.now() + timeoutMs;
|
|
8955
|
-
const frame = await field?.ownerFrame().catch(() => null) ?? page.mainFrame();
|
|
8956
|
-
const before = await domSignature(frame);
|
|
8957
|
-
while (Date.now() < until) {
|
|
8958
|
-
await delay3(250);
|
|
8959
|
-
if (page.isClosed())
|
|
8960
|
-
return true;
|
|
8961
|
-
if (page.url() !== url) {
|
|
8962
|
-
await page.waitForLoadState("domcontentloaded", { timeout: Math.max(1, until - Date.now()) }).catch(() => {});
|
|
8963
|
-
return true;
|
|
8964
|
-
}
|
|
8965
|
-
if (field && !await field.isVisible().catch(() => false))
|
|
8966
|
-
return true;
|
|
8967
|
-
const now = await domSignature(frame);
|
|
8968
|
-
if (before && now && now !== before) {
|
|
8969
|
-
await delay3(400);
|
|
8970
|
-
return true;
|
|
8971
|
-
}
|
|
8972
|
-
}
|
|
8973
|
-
return false;
|
|
8974
|
-
}
|
|
8975
|
-
async function clearSecretInputs(frames, fields = []) {
|
|
8976
|
-
const clearScript = () => {
|
|
8977
|
-
const own = (element) => {
|
|
8978
|
-
const input = element;
|
|
8979
|
-
if (!(input instanceof HTMLInputElement) || !input.value)
|
|
8980
|
-
return;
|
|
8981
|
-
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
8982
|
-
if (setter)
|
|
8983
|
-
setter.call(input, "");
|
|
8984
|
-
else
|
|
8985
|
-
input.value = "";
|
|
8986
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
8987
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
8988
|
-
};
|
|
8989
|
-
const all = [];
|
|
8990
|
-
const walk = (root) => {
|
|
8991
|
-
for (const element of Array.from(root.querySelectorAll("input"))) {
|
|
8992
|
-
const input = element;
|
|
8993
|
-
if (input.type === "password" || /one-time-code/i.test(input.autocomplete) || input.maxLength === 1 && input.type !== "checkbox" && input.type !== "radio")
|
|
8994
|
-
all.push(input);
|
|
8995
|
-
}
|
|
8996
|
-
for (const host of Array.from(root.querySelectorAll("*")))
|
|
8997
|
-
if (host.shadowRoot)
|
|
8998
|
-
walk(host.shadowRoot);
|
|
8999
|
-
};
|
|
9000
|
-
walk(document);
|
|
9001
|
-
for (const element of all)
|
|
9002
|
-
own(element);
|
|
9003
|
-
};
|
|
9004
|
-
for (const field of fields) {
|
|
9005
|
-
await field.evaluate((node) => {
|
|
9006
|
-
const input = node;
|
|
9007
|
-
if (!(input instanceof HTMLInputElement) || !input.value)
|
|
9008
|
-
return;
|
|
9009
|
-
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set?.call(input, "");
|
|
9010
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
9011
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
9012
|
-
}).catch(() => {});
|
|
9013
|
-
}
|
|
9014
|
-
for (const frame of new Set(frames)) {
|
|
9015
|
-
if (frame.isDetached())
|
|
9016
|
-
continue;
|
|
9017
|
-
await frame.evaluate(clearScript).catch(() => {});
|
|
9018
|
-
}
|
|
9019
|
-
}
|
|
9020
|
-
async function submit(read, field) {
|
|
9021
|
-
const button = await element(read, "submit");
|
|
9022
|
-
if (button)
|
|
9023
|
-
await clickHandle(button);
|
|
9024
|
-
else
|
|
9025
|
-
await field.press("Enter");
|
|
9026
|
-
}
|
|
9027
|
-
async function signIn(deps, options = {}) {
|
|
9028
|
-
const timeoutMs = options.timeoutMs ?? 90000;
|
|
9029
|
-
const deadline = Date.now() + timeoutMs;
|
|
9030
|
-
let page = deps.page;
|
|
9031
|
-
if (options.site !== undefined && (typeof options.site !== "string" || !options.site))
|
|
9032
|
-
return { status: "failed", step: "start", reason: "site must be a host like example.com or an origin" };
|
|
9033
|
-
const startSite = options.site?.toLowerCase() ?? defaultSite(page.url());
|
|
9034
|
-
if (!startSite)
|
|
9035
|
-
return { status: "failed", step: "start", reason: "The page isn't on a website; navigate to the site's sign-in page first, or pass {site}." };
|
|
9036
|
-
const siteSpec = { site: startSite, ...options.username ? { username: options.username } : {} };
|
|
9037
|
-
const typedFrames = new Set;
|
|
9038
|
-
const typedFields = [];
|
|
9039
|
-
const tracked = {
|
|
9040
|
-
...deps,
|
|
9041
|
-
type: async (handles, spec, field) => {
|
|
9042
|
-
for (const handle of handles) {
|
|
9043
|
-
const frame = await handle.ownerFrame().catch(() => null);
|
|
9044
|
-
if (frame)
|
|
9045
|
-
typedFrames.add(frame);
|
|
9046
|
-
if (field !== "username")
|
|
9047
|
-
typedFields.push(handle);
|
|
9048
|
-
}
|
|
9049
|
-
return await deps.type(handles, spec, field);
|
|
9050
|
-
}
|
|
9051
|
-
};
|
|
9052
|
-
let result;
|
|
9053
|
-
try {
|
|
9054
|
-
const onSite = (() => {
|
|
9055
|
-
try {
|
|
9056
|
-
return originOnSite(new URL(page.url()).origin, startSite);
|
|
9057
|
-
} catch {
|
|
9058
|
-
return false;
|
|
9059
|
-
}
|
|
9060
|
-
})();
|
|
9061
|
-
if (!onSite) {
|
|
9062
|
-
const start = /^https?:\/\//.test(startSite) ? startSite : `https://${startSite}/`;
|
|
9063
|
-
await page.goto(start, { waitUntil: "domcontentloaded", timeout: Math.min(30000, timeoutMs) });
|
|
9064
|
-
}
|
|
9065
|
-
if (options.username && deps.passkeys)
|
|
9066
|
-
await deps.passkeys.select(page, startSite, options.username).catch(() => {});
|
|
9067
|
-
result = await run2(tracked, page, siteSpec, deadline, 0);
|
|
9068
|
-
return result;
|
|
9069
|
-
} catch (error) {
|
|
9070
|
-
result = fromError(error, "start");
|
|
9071
|
-
return result;
|
|
9072
|
-
} finally {
|
|
9073
|
-
if (result?.status !== "signed_in" && typedFrames.size > 0)
|
|
9074
|
-
await clearSecretInputs(typedFrames, typedFields);
|
|
9075
|
-
if (options.username && deps.passkeys && !page.isClosed())
|
|
9076
|
-
await deps.passkeys.select(page, startSite, "").catch(() => {});
|
|
9077
|
-
}
|
|
9078
|
-
}
|
|
9079
|
-
function fromError(error, step) {
|
|
9080
|
-
if (error instanceof DeadboltError) {
|
|
9081
|
-
if (error.code === "not_granted")
|
|
9082
|
-
return { status: "not_granted", step, reason: error.message };
|
|
9083
|
-
if (error.code === "locked")
|
|
9084
|
-
return { status: "needs_user", step, reason: error.message };
|
|
9085
|
-
return { status: "failed", step, reason: `${error.code}: ${error.message}` };
|
|
9086
|
-
}
|
|
9087
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9088
|
-
if (/domain_not_allowed/.test(message))
|
|
9089
|
-
return { status: "failed", step, reason: message.replace(/^secrets\.\w+: /, "") };
|
|
9090
|
-
return { status: "failed", step, reason: message.slice(0, 300) };
|
|
9091
|
-
}
|
|
9092
|
-
async function run2(deps, startPage, siteSpec, deadline, depth) {
|
|
9093
|
-
const page = startPage;
|
|
9094
|
-
let step = "start";
|
|
9095
|
-
let googleSpec;
|
|
9096
|
-
let clickedSignInLink = false;
|
|
9097
|
-
let toggles = 0;
|
|
9098
|
-
let lastSignature = "";
|
|
9099
|
-
let repeats = 0;
|
|
9100
|
-
let passkeyClicks = 0;
|
|
9101
|
-
const started = Date.now();
|
|
9102
|
-
const needsChoice = async () => {
|
|
9103
|
-
if (siteSpec.username || !deps.passkeys)
|
|
9104
|
-
return;
|
|
9105
|
-
const choices = await deps.passkeys.choiceSince(page, started).catch(() => {
|
|
9106
|
-
return;
|
|
9107
|
-
});
|
|
9108
|
-
if (!choices || choices.length === 0)
|
|
9109
|
-
return;
|
|
9110
|
-
return { status: "needs_choice", step: "passkey", reason: `Several passkeys can sign in to ${siteSpec.site}; call again with { username } set to one of the choices.`, choices };
|
|
9111
|
-
};
|
|
9112
|
-
const specFor = (url) => {
|
|
9113
|
-
let origin;
|
|
9114
|
-
try {
|
|
9115
|
-
origin = new URL(url).origin;
|
|
9116
|
-
} catch {
|
|
9117
|
-
return;
|
|
9118
|
-
}
|
|
9119
|
-
if (originOnSite(origin, siteSpec.site))
|
|
9120
|
-
return siteSpec;
|
|
9121
|
-
if (googleSpec && originOnSite(origin, googleSite))
|
|
9122
|
-
return googleSpec;
|
|
9123
|
-
return;
|
|
9124
|
-
};
|
|
9125
|
-
while (Date.now() < deadline) {
|
|
9126
|
-
if (page.isClosed())
|
|
9127
|
-
return step === "start" ? { status: "failed", step, reason: "The page closed." } : { status: "signed_in", step: "done", reason: "The sign-in window closed after the last step." };
|
|
9128
|
-
await page.waitForLoadState("domcontentloaded", { timeout: Math.max(1, Math.min(15000, deadline - Date.now())) }).catch(() => {});
|
|
9129
|
-
const url = page.url();
|
|
9130
|
-
const spec = specFor(url);
|
|
9131
|
-
if (!spec) {
|
|
9132
|
-
if (step === "start")
|
|
9133
|
-
return { status: "failed", step, reason: `The page is on ${safeOrigin(url)}, not ${siteSpec.site}.` };
|
|
9134
|
-
return needsUser("provider", `The sign-in moved to ${safeOrigin(url)}, which isn't ${siteSpec.site}${googleSpec ? " or Google" : ""}.`);
|
|
9135
|
-
}
|
|
9136
|
-
const choice = await needsChoice();
|
|
9137
|
-
if (choice)
|
|
9138
|
-
return choice;
|
|
9139
|
-
let read;
|
|
9140
|
-
const settleUntil = Math.min(deadline, Date.now() + (step === "start" ? 8000 : 5000));
|
|
9141
|
-
while (true) {
|
|
9142
|
-
read = await readPage(page, spec.site);
|
|
9143
|
-
if (read && actionable(read.state) || Date.now() >= settleUntil)
|
|
9144
|
-
break;
|
|
9145
|
-
await delay3(300);
|
|
9146
|
-
}
|
|
9147
|
-
if (!read) {
|
|
9148
|
-
await delay3(300);
|
|
9149
|
-
continue;
|
|
9150
|
-
}
|
|
9151
|
-
const state = read.state;
|
|
9152
|
-
const signature = `${url}|${state.hasUsername}|${state.hasPassword}|${state.otpCount}|${state.usernameFilled.length}`;
|
|
9153
|
-
repeats = signature === lastSignature ? repeats + 1 : 0;
|
|
9154
|
-
lastSignature = signature;
|
|
9155
|
-
if (repeats >= 2) {
|
|
9156
|
-
return { status: "failed", step, reason: state.errorText ? `The site didn't accept the ${step === "code" ? "code" : step}: ${state.errorText}` : `The page didn't move on after the ${step} step.` };
|
|
9157
|
-
}
|
|
9158
|
-
if (state.captcha)
|
|
9159
|
-
return needsUser(step, `A captcha is showing (${state.captcha}).`);
|
|
9160
|
-
const challenge = await deps.challenge(page).catch(() => {
|
|
9161
|
-
return;
|
|
9162
|
-
});
|
|
9163
|
-
if (challenge && challenge.kind !== "non-interactive") {
|
|
9164
|
-
return challenge.kind === "block" ? { status: "failed", step, reason: `The site blocked the visit (${challenge.vendor}).` } : needsUser(step, `A ${challenge.vendor} bot check is showing.`);
|
|
9165
|
-
}
|
|
9166
|
-
if (state.otpCount > 0) {
|
|
9167
|
-
const kind = codeKind(state.text);
|
|
9168
|
-
if (kind === "sent")
|
|
9169
|
-
return needsUser("code", "The site sent a one-time code by SMS, email or call.");
|
|
9170
|
-
const logins = await deps.logins(new URL(read.frame.url()).origin).catch(() => []);
|
|
9171
|
-
const hasTotp = logins.some((login) => login.hasTotp && login.granted && (!login.fields || login.fields.includes("totp")) && (!spec.username || login.username?.toLowerCase() === spec.username.toLowerCase()));
|
|
9172
|
-
if (!hasTotp)
|
|
9173
|
-
return needsUser("code", kind === "totp" ? "The site asks for an authenticator code, and the granted login has no one-time password." : "The site asks for a one-time code.");
|
|
9174
|
-
const boxes = await otpElements(read);
|
|
9175
|
-
step = "code";
|
|
9176
|
-
await deps.type(boxes, spec, "totp");
|
|
9177
|
-
if (!await waitForProgress(page, boxes[0], 1500)) {
|
|
9178
|
-
const stillThere = await boxes[0].isVisible().catch(() => false);
|
|
9179
|
-
if (stillThere)
|
|
9180
|
-
await submit(read, boxes.at(-1));
|
|
9181
|
-
await waitForProgress(page, boxes[0], 15000);
|
|
9182
|
-
}
|
|
9183
|
-
continue;
|
|
9184
|
-
}
|
|
9185
|
-
if (state.hasPassword) {
|
|
9186
|
-
const passwordField = await element(read, "password");
|
|
9187
|
-
const usernameField = await element(read, "username");
|
|
9188
|
-
if (usernameField && !state.usernameFilled)
|
|
9189
|
-
await deps.type([usernameField], spec, "username");
|
|
9190
|
-
step = "password";
|
|
9191
|
-
await deps.type([passwordField], spec, "password");
|
|
9192
|
-
await submit(read, passwordField);
|
|
9193
|
-
await waitForProgress(page, passwordField, 15000);
|
|
9194
|
-
continue;
|
|
9195
|
-
}
|
|
9196
|
-
if (state.hasUsername) {
|
|
9197
|
-
const usernameField = await element(read, "username");
|
|
9198
|
-
await deps.type([usernameField], spec, "username");
|
|
9199
|
-
step = "identifier";
|
|
9200
|
-
await submit(read, usernameField);
|
|
9201
|
-
await waitForProgress(page, usernameField, 15000);
|
|
9202
|
-
continue;
|
|
9203
|
-
}
|
|
9204
|
-
if (state.hasPasswordToggle && toggles < 2) {
|
|
9205
|
-
toggles += 1;
|
|
9206
|
-
await clickHandle(await element(read, "passwordToggle"));
|
|
9207
|
-
await waitForProgress(page, undefined, 2000);
|
|
9208
|
-
continue;
|
|
9209
|
-
}
|
|
9210
|
-
if (state.hasGoogle && !googleSpec && depth === 0 && spec === siteSpec) {
|
|
9211
|
-
const siteLogins = await deps.logins(new URL(read.frame.url()).origin).catch(() => []);
|
|
9212
|
-
const viaGoogle = siteLogins.find((login) => login.signIn?.provider === "google");
|
|
9213
|
-
const googleLogins = (await deps.logins(`https://${googleSite}`).catch(() => [])).filter((login) => login.granted);
|
|
9214
|
-
const account = siteSpec.username ?? viaGoogle?.signIn?.account;
|
|
9215
|
-
const googleLogin = googleLogins.find((login) => !account || login.username?.toLowerCase() === account.toLowerCase()) ?? (googleLogins.length === 1 ? googleLogins[0] : undefined);
|
|
9216
|
-
if (googleLogin && (viaGoogle || !siteLogins.some((login) => login.granted && login.hasPassword))) {
|
|
9217
|
-
if (!deps.allowed(`https://${googleSite}/`))
|
|
9218
|
-
return needsUser("provider", `Signing in uses Google, but ${googleSite} isn't in this session's allowlist (add it with session_allowlist).`);
|
|
9219
|
-
googleSpec = { site: googleSite, ...googleLogin.username ? { username: googleLogin.username } : {} };
|
|
9220
|
-
step = "provider";
|
|
9221
|
-
const popupPromise = deps.context.waitForEvent("page", { timeout: 5000 }).catch(() => {
|
|
9222
|
-
return;
|
|
9223
|
-
});
|
|
9224
|
-
await clickHandle(await element(read, "google"));
|
|
9225
|
-
const popup = await popupPromise;
|
|
9226
|
-
if (popup) {
|
|
9227
|
-
await popup.waitForLoadState("domcontentloaded").catch(() => {});
|
|
9228
|
-
const result = await runProvider(deps, popup, googleSpec, deadline);
|
|
9229
|
-
if (result.status !== "signed_in")
|
|
9230
|
-
return result;
|
|
9231
|
-
await waitForProgress(page, undefined, 5000);
|
|
9232
|
-
} else {
|
|
9233
|
-
await waitForProgress(page, undefined, 1e4);
|
|
9234
|
-
}
|
|
9235
|
-
continue;
|
|
9236
|
-
}
|
|
9237
|
-
}
|
|
9238
|
-
if (state.hasPasskeyButton && passkeyClicks < 1) {
|
|
9239
|
-
const origin = new URL(read.frame.url()).origin;
|
|
9240
|
-
const passkeys = await deps.passkeyAccounts(origin).catch(() => []);
|
|
9241
|
-
const granted = spec.username ? pickPasskey(passkeys, spec.username) !== undefined : passkeys.some((passkey) => passkey.granted);
|
|
9242
|
-
if (granted) {
|
|
9243
|
-
passkeyClicks += 1;
|
|
9244
|
-
step = "passkey";
|
|
9245
|
-
const before = page.url();
|
|
9246
|
-
await clickHandle(await element(read, "passkeyButton"));
|
|
9247
|
-
const until = Math.min(deadline, Date.now() + 1e4);
|
|
9248
|
-
while (Date.now() < until && !page.isClosed() && page.url() === before) {
|
|
9249
|
-
const picked = await needsChoice();
|
|
9250
|
-
if (picked)
|
|
9251
|
-
return picked;
|
|
9252
|
-
await delay3(250);
|
|
9253
|
-
}
|
|
9254
|
-
await page.waitForLoadState("domcontentloaded").catch(() => {});
|
|
9255
|
-
continue;
|
|
9256
|
-
}
|
|
9257
|
-
}
|
|
9258
|
-
const verification = verificationKind(state.text);
|
|
9259
|
-
if (verification)
|
|
9260
|
-
return needsUser("verify", verification === "push_approval" ? "The site waits for a push approval on another device." : verification === "security_question" ? "The site asks a security question." : "The site wants to verify a new device or that it's really the user.");
|
|
9261
|
-
if (step !== "start" && codeKind(state.text) === "sent")
|
|
9262
|
-
return needsUser("code", "The site sent a one-time code by SMS, email or call.");
|
|
9263
|
-
if (step === "start") {
|
|
9264
|
-
if (state.hasSignInLink && !clickedSignInLink) {
|
|
9265
|
-
clickedSignInLink = true;
|
|
9266
|
-
await clickHandle(await element(read, "signInLink"));
|
|
9267
|
-
await waitForProgress(page, undefined, 8000);
|
|
9268
|
-
continue;
|
|
9269
|
-
}
|
|
9270
|
-
if (state.hasGoogle)
|
|
9271
|
-
return needsUser("provider", "The page offers Sign in with Google, but Deadbolt grants no Google login for it.");
|
|
9272
|
-
return { status: "failed", step, reason: "No sign-in form found on this page. Open the site's sign-in page first." };
|
|
9273
|
-
}
|
|
9274
|
-
if (state.errorText && /incorrect|wrong|invalid|doesn'?t match|not recognized|couldn'?t find|try again/i.test(state.errorText)) {
|
|
9275
|
-
return { status: "failed", step, reason: `The site refused the sign-in: ${state.errorText}` };
|
|
9276
|
-
}
|
|
9277
|
-
if (spec === googleSpec && spec !== siteSpec)
|
|
9278
|
-
return needsUser("provider", "Google shows a screen to choose an account or to give consent.");
|
|
9279
|
-
return { status: "signed_in", step: "done", reason: `Signed in to ${spec.site}: no sign-in field is left after the ${step} step.` };
|
|
9280
|
-
}
|
|
9281
|
-
return { status: "failed", step: "start", reason: "The sign-in didn't finish in time." };
|
|
9282
|
-
}
|
|
9283
|
-
async function runProvider(deps, popup, spec, deadline) {
|
|
9284
|
-
const result = await run2({ ...deps, page: popup }, popup, spec, deadline, 1);
|
|
9285
|
-
if (result.status !== "signed_in")
|
|
9286
|
-
return result;
|
|
9287
|
-
if (!popup.isClosed())
|
|
9288
|
-
await popup.waitForEvent("close", { timeout: Math.max(1, Math.min(20000, deadline - Date.now())) }).catch(() => {});
|
|
9289
|
-
return popup.isClosed() ? result : needsUser("provider", "Google's sign-in window stayed open (an account choice or consent screen).");
|
|
9290
|
-
}
|
|
9291
|
-
function safeOrigin(url) {
|
|
9292
|
-
try {
|
|
9293
|
-
return new URL(url).origin;
|
|
9294
|
-
} catch {
|
|
9295
|
-
return url.slice(0, 80);
|
|
9296
|
-
}
|
|
9297
|
-
}
|
|
9298
|
-
|
|
9299
|
-
// src/execute/secrets-bridge.ts
|
|
9300
8729
|
var isOpaqueDocumentUrl = (url) => /^(?:about|data|blob):/i.test(url);
|
|
9301
8730
|
var frameOrigin = (url) => {
|
|
9302
8731
|
try {
|
|
@@ -9313,8 +8742,6 @@ var elementHasFocus = (handle) => handle.evaluate((node) => {
|
|
|
9313
8742
|
class ExecuteSecretsBridge {
|
|
9314
8743
|
deps;
|
|
9315
8744
|
typedSecrets = new Map;
|
|
9316
|
-
deadboltConnection;
|
|
9317
|
-
pageHooks = new Set;
|
|
9318
8745
|
constructor(deps) {
|
|
9319
8746
|
this.deps = deps;
|
|
9320
8747
|
}
|
|
@@ -9372,9 +8799,6 @@ class ExecuteSecretsBridge {
|
|
|
9372
8799
|
async typeSecret(page, target, name, options = {}) {
|
|
9373
8800
|
if (typeof name !== "string" || !name)
|
|
9374
8801
|
throw new Error("secrets.type(target, name): name must be the name of a stored secret");
|
|
9375
|
-
const deadboltSpec = parseDeadboltName(name);
|
|
9376
|
-
if (deadboltSpec)
|
|
9377
|
-
return await this.typeDeadboltSecret(page, target, name, deadboltSpec, options);
|
|
9378
8802
|
const secret = await Effect10.runPromise(readSecret(name)).catch((error) => {
|
|
9379
8803
|
throw new Error(`secrets.type: ${error instanceof Error ? error.message : "could not read the secret"}`);
|
|
9380
8804
|
});
|
|
@@ -9429,158 +8853,6 @@ class ExecuteSecretsBridge {
|
|
|
9429
8853
|
await handle.dispose().catch(() => {});
|
|
9430
8854
|
}
|
|
9431
8855
|
}
|
|
9432
|
-
refuseWithPageHooks(label) {
|
|
9433
|
-
if (this.pageHooks.size === 0)
|
|
9434
|
-
return;
|
|
9435
|
-
throw new Error(`${label}: page_hooks: this session's code used ${[...this.pageHooks].sort().join(", ")}, which could capture what is typed, so Deadbolt passwords and codes aren't typed here. Reset the session (session_reset) and sign in before setting those up. Nothing was typed.`);
|
|
9436
|
-
}
|
|
9437
|
-
deadbolt() {
|
|
9438
|
-
const client = readClient();
|
|
9439
|
-
if (!this.deadboltConnection || !client || this.deadboltConnection.client.id !== client.id || this.deadboltConnection.client.token !== client.token) {
|
|
9440
|
-
this.deadboltConnection = connect(client ? { client } : {});
|
|
9441
|
-
}
|
|
9442
|
-
return this.deadboltConnection;
|
|
9443
|
-
}
|
|
9444
|
-
sessionAllows(url) {
|
|
9445
|
-
const patterns = this.deps.allowedOrigins();
|
|
9446
|
-
return !patterns || patterns.length === 0 || checkUrl(url, parseOriginPatterns(patterns)).allowed;
|
|
9447
|
-
}
|
|
9448
|
-
async typeDeadbolt(handles, spec, field, label) {
|
|
9449
|
-
if (!["username", "password", "totp"].includes(field))
|
|
9450
|
-
throw new Error(`${label}: field must be "username", "password" or "totp"`);
|
|
9451
|
-
if (handles.length === 0)
|
|
9452
|
-
throw new Error(`${label}: target_not_editable: no field to type into`);
|
|
9453
|
-
if (field !== "username")
|
|
9454
|
-
this.refuseWithPageHooks(label);
|
|
9455
|
-
const frame = await handles[0].ownerFrame();
|
|
9456
|
-
if (!frame)
|
|
9457
|
-
throw new Error(`${label}: target_not_editable: the field is not in a frame`);
|
|
9458
|
-
for (const handle of handles.slice(1)) {
|
|
9459
|
-
if (await handle.ownerFrame() !== frame)
|
|
9460
|
-
throw new Error(`${label}: target_not_editable: the code boxes are in different frames`);
|
|
9461
|
-
}
|
|
9462
|
-
const checkOrigin = () => {
|
|
9463
|
-
const url = frame.url();
|
|
9464
|
-
const origin = frameOrigin(url);
|
|
9465
|
-
if (isOpaqueDocumentUrl(url) || !originOnSite(origin, spec.site)) {
|
|
9466
|
-
throw new Error(`${label}: domain_not_allowed: the field's frame is ${origin}, which is not on ${spec.site}. Nothing was typed.`);
|
|
9467
|
-
}
|
|
9468
|
-
if (!this.sessionAllows(url)) {
|
|
9469
|
-
throw new Error(`${label}: domain_not_allowed: the field's frame is ${origin}, which is outside this session's domain allowlist. Nothing was typed.`);
|
|
9470
|
-
}
|
|
9471
|
-
return origin;
|
|
9472
|
-
};
|
|
9473
|
-
const origin = checkOrigin();
|
|
9474
|
-
for (const handle of handles) {
|
|
9475
|
-
if (!await handle.isEditable().catch(() => false))
|
|
9476
|
-
throw new Error(`${label}: target_not_editable: the target is not an editable field. Nothing was typed.`);
|
|
9477
|
-
}
|
|
9478
|
-
const connection = this.deadbolt();
|
|
9479
|
-
const login = pickLogin(await connection.logins(origin), spec.site, spec.username, field);
|
|
9480
|
-
const values = await connection.fill(origin, login, [field]);
|
|
9481
|
-
const value = values[field];
|
|
9482
|
-
if (!value)
|
|
9483
|
-
throw new DeadboltError("not_found", `The Deadbolt login for ${spec.site} has no ${field}.`);
|
|
9484
|
-
const name = `deadbolt:${spec.site}${login.username ? `/${login.username}` : ""} ${field}`;
|
|
9485
|
-
if (field !== "username") {
|
|
9486
|
-
this.typedSecrets.set(value, name);
|
|
9487
|
-
this.deps.networkCapture.addRedaction(value);
|
|
9488
|
-
}
|
|
9489
|
-
const characters = handles.length === 1 ? [value] : Array.from(value);
|
|
9490
|
-
if (handles.length > 1 && characters.length !== handles.length) {
|
|
9491
|
-
throw new Error(`${label}: target_not_editable: ${handles.length} code boxes for a ${characters.length}-character code. Nothing was typed.`);
|
|
9492
|
-
}
|
|
9493
|
-
for (const [index, handle] of handles.entries()) {
|
|
9494
|
-
await handle.fill("");
|
|
9495
|
-
await handle.focus();
|
|
9496
|
-
if (!await elementHasFocus(handle)) {
|
|
9497
|
-
throw new Error(`${label}: target_not_editable: the field did not keep focus.`);
|
|
9498
|
-
}
|
|
9499
|
-
checkOrigin();
|
|
9500
|
-
await frame.page().keyboard.insertText(characters[index]);
|
|
9501
|
-
}
|
|
9502
|
-
if (!this.deps.allowedOrigins()?.length) {
|
|
9503
|
-
this.deps.addWarningOnce(`A Deadbolt login was typed in a session without a domain allowlist. A prompt-injected page could steer this session elsewhere after the value is in the page: set one with session_allowlist (or \`session allow\`) before signing in.`);
|
|
9504
|
-
}
|
|
9505
|
-
return { origin, login };
|
|
9506
|
-
}
|
|
9507
|
-
async typeDeadboltSecret(page, target, name, spec, options) {
|
|
9508
|
-
const field = options.field ?? "password";
|
|
9509
|
-
if (field !== "username" && options.submit !== true) {
|
|
9510
|
-
throw new Error(`secrets.type: submit_required: a Deadbolt ${field} is typed only with { submit: true }, which submits the form in the same call and clears the field afterwards. Prefer secrets.signIn(). Nothing was typed.`);
|
|
9511
|
-
}
|
|
9512
|
-
const locator = typeof target === "string" ? page.locator(target) : target;
|
|
9513
|
-
if (!locator || typeof locator.elementHandle !== "function") {
|
|
9514
|
-
throw new Error("secrets.type(target, name): target must be a selector string or a Locator");
|
|
9515
|
-
}
|
|
9516
|
-
const handle = await locator.elementHandle({ timeout: options.timeout ?? 1e4 });
|
|
9517
|
-
const frame = await handle.ownerFrame();
|
|
9518
|
-
let typed = false;
|
|
9519
|
-
try {
|
|
9520
|
-
const { origin } = await this.typeDeadbolt([handle], spec, field, "secrets.type").catch((error) => {
|
|
9521
|
-
if (error instanceof DeadboltError)
|
|
9522
|
-
throw new Error(`secrets.type: ${error.code}: ${error.message} Nothing was typed.`);
|
|
9523
|
-
throw error;
|
|
9524
|
-
});
|
|
9525
|
-
typed = true;
|
|
9526
|
-
if (options.submit !== true)
|
|
9527
|
-
return { typed: true, name, origin, field };
|
|
9528
|
-
const submitControl = await handle.evaluateHandle((node) => {
|
|
9529
|
-
const form = node.form;
|
|
9530
|
-
if (!form)
|
|
9531
|
-
return null;
|
|
9532
|
-
return form.querySelector("button[type=submit], input[type=submit], button:not([type])");
|
|
9533
|
-
});
|
|
9534
|
-
const button = submitControl.asElement();
|
|
9535
|
-
if (button)
|
|
9536
|
-
await button.click({ timeout: 5000 }).catch(() => handle.press("Enter"));
|
|
9537
|
-
else
|
|
9538
|
-
await handle.press("Enter");
|
|
9539
|
-
const progressed = await waitForProgress(frame?.page() ?? page, handle, 15000);
|
|
9540
|
-
return { typed: true, name, origin, field, submitted: true, progressed };
|
|
9541
|
-
} finally {
|
|
9542
|
-
if (typed && field !== "username")
|
|
9543
|
-
await clearSecretInputs(frame ? [frame] : [], [handle]);
|
|
9544
|
-
await handle.dispose().catch(() => {});
|
|
9545
|
-
}
|
|
9546
|
-
}
|
|
9547
|
-
async usePasskey(page, options) {
|
|
9548
|
-
const site = typeof options?.site === "string" ? options.site.trim().toLowerCase() : "";
|
|
9549
|
-
const username = typeof options?.username === "string" ? options.username.trim() : "";
|
|
9550
|
-
if (!site || !username)
|
|
9551
|
-
throw new Error("secrets.usePasskey({ site, username }): site and username are required");
|
|
9552
|
-
if (!this.deps.passkeys)
|
|
9553
|
-
throw new Error("secrets.usePasskey: passkeys aren't available in this relay");
|
|
9554
|
-
this.deps.passkeys.select(await pageTargetId(page), site, username);
|
|
9555
|
-
return { selected: true, site, username };
|
|
9556
|
-
}
|
|
9557
|
-
async signIn(page, options = {}) {
|
|
9558
|
-
if (options !== null && typeof options !== "object")
|
|
9559
|
-
throw new Error("secrets.signIn(options): options must be an object like { site, username }");
|
|
9560
|
-
const site = typeof options?.site === "string" ? options.site : undefined;
|
|
9561
|
-
const username = typeof options?.username === "string" ? options.username : undefined;
|
|
9562
|
-
const timeoutMs = typeof options?.timeoutMs === "number" && options.timeoutMs > 0 ? Math.min(options.timeoutMs, 300000) : undefined;
|
|
9563
|
-
return await signIn({
|
|
9564
|
-
page,
|
|
9565
|
-
context: page.context(),
|
|
9566
|
-
type: async (handles, spec, field) => (await this.typeDeadbolt(handles, spec, field, "secrets.signIn")).login,
|
|
9567
|
-
logins: (origin) => this.deadbolt().logins(origin),
|
|
9568
|
-
passkeyAccounts: async (origin) => (await this.deadbolt().site(origin)).passkeys,
|
|
9569
|
-
challenge: async (target) => {
|
|
9570
|
-
const outcome = await detectChallenge(target, { waitMs: challengeWaitMs() }).catch(() => {
|
|
9571
|
-
return;
|
|
9572
|
-
});
|
|
9573
|
-
return outcome && outcome.status === "blocked" ? outcome : undefined;
|
|
9574
|
-
},
|
|
9575
|
-
allowed: (url) => this.sessionAllows(url),
|
|
9576
|
-
...this.deps.passkeys ? {
|
|
9577
|
-
passkeys: {
|
|
9578
|
-
select: async (target, passkeySite, passkeyUsername) => this.deps.passkeys.select(await pageTargetId(target), passkeySite, passkeyUsername),
|
|
9579
|
-
choiceSince: async (target, since) => this.deps.passkeys.choiceSince(await pageTargetId(target), since)
|
|
9580
|
-
}
|
|
9581
|
-
} : {}
|
|
9582
|
-
}, { ...site ? { site } : {}, ...username ? { username } : {}, ...timeoutMs ? { timeoutMs } : {} });
|
|
9583
|
-
}
|
|
9584
8856
|
}
|
|
9585
8857
|
|
|
9586
8858
|
// src/network-capture.ts
|
|
@@ -10476,7 +9748,6 @@ class ExecuteSandbox extends DefaultPageHost {
|
|
|
10476
9748
|
this.secrets = new ExecuteSecretsBridge({
|
|
10477
9749
|
networkCapture: this.networkCapture,
|
|
10478
9750
|
allowedOrigins: () => this.options.allowedOrigins?.(),
|
|
10479
|
-
...this.options.passkeys ? { passkeys: this.options.passkeys } : {},
|
|
10480
9751
|
addWarningOnce: (warning) => this.addWarningOnce(warning)
|
|
10481
9752
|
});
|
|
10482
9753
|
this.realm = this.allowNode ? undefined : this.createRealm();
|
|
@@ -10489,9 +9760,6 @@ class ExecuteSandbox extends DefaultPageHost {
|
|
|
10489
9760
|
return new ExecuteRealm({
|
|
10490
9761
|
onHostCall: (fn, receiver, args) => {
|
|
10491
9762
|
this.addWarningOnce(pageVisibleApiWarning(fn, receiver, args));
|
|
10492
|
-
const hook = pageHookCall(fn, args);
|
|
10493
|
-
if (hook)
|
|
10494
|
-
this.secrets.pageHooks.add(hook);
|
|
10495
9763
|
}
|
|
10496
9764
|
});
|
|
10497
9765
|
}
|
|
@@ -10641,7 +9909,6 @@ class ExecuteSandbox extends DefaultPageHost {
|
|
|
10641
9909
|
sandbox.realm?.dispose();
|
|
10642
9910
|
sandbox.realm = sandbox.allowNode ? undefined : sandbox.createRealm();
|
|
10643
9911
|
sandbox.state = sandbox.realm?.state ?? {};
|
|
10644
|
-
sandbox.secrets.pageHooks.clear();
|
|
10645
9912
|
const browser = sandbox.browser;
|
|
10646
9913
|
sandbox.browser = undefined;
|
|
10647
9914
|
sandbox.unbindDefaultPage(sandbox.defaultPageTargetId, {
|
|
@@ -10947,36 +10214,36 @@ function unsignedInteger(value) {
|
|
|
10947
10214
|
}
|
|
10948
10215
|
return Buffer.from(bytes);
|
|
10949
10216
|
}
|
|
10950
|
-
function
|
|
10217
|
+
function element(id, payload) {
|
|
10951
10218
|
return Buffer.concat([id, variableInteger(payload.length), payload]);
|
|
10952
10219
|
}
|
|
10953
10220
|
function mjpegMatroskaHeader(width, height) {
|
|
10954
|
-
const ebmlHeader =
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10221
|
+
const ebmlHeader = element(ebml, Buffer.concat([
|
|
10222
|
+
element(ebmlVersion, unsignedInteger(1)),
|
|
10223
|
+
element(ebmlReadVersion, unsignedInteger(1)),
|
|
10224
|
+
element(ebmlMaxIdLength, unsignedInteger(4)),
|
|
10225
|
+
element(ebmlMaxSizeLength, unsignedInteger(8)),
|
|
10226
|
+
element(docType, Buffer.from("matroska")),
|
|
10227
|
+
element(docTypeVersion, unsignedInteger(4)),
|
|
10228
|
+
element(docTypeReadVersion, unsignedInteger(2))
|
|
10962
10229
|
]));
|
|
10963
|
-
const streamInfo =
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10230
|
+
const streamInfo = element(info, Buffer.concat([
|
|
10231
|
+
element(timestampScale, unsignedInteger(1e6)),
|
|
10232
|
+
element(muxingApp, Buffer.from("opencode-browser")),
|
|
10233
|
+
element(writingApp, Buffer.from("opencode-browser"))
|
|
10967
10234
|
]));
|
|
10968
|
-
const track =
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10235
|
+
const track = element(trackEntry, Buffer.concat([
|
|
10236
|
+
element(trackNumber, unsignedInteger(1)),
|
|
10237
|
+
element(trackUid, unsignedInteger(1)),
|
|
10238
|
+
element(trackType, unsignedInteger(1)),
|
|
10239
|
+
element(flagLacing, unsignedInteger(0)),
|
|
10240
|
+
element(codecId, Buffer.from("V_MJPEG")),
|
|
10241
|
+
element(video, Buffer.concat([
|
|
10242
|
+
element(pixelWidth, unsignedInteger(width)),
|
|
10243
|
+
element(pixelHeight, unsignedInteger(height))
|
|
10977
10244
|
]))
|
|
10978
10245
|
]));
|
|
10979
|
-
return Buffer.concat([ebmlHeader, segment, unknownSize, streamInfo,
|
|
10246
|
+
return Buffer.concat([ebmlHeader, segment, unknownSize, streamInfo, element(tracks, track)]);
|
|
10980
10247
|
}
|
|
10981
10248
|
function mjpegMatroskaFrame(timestampMs, durationMs, frameLength) {
|
|
10982
10249
|
const blockHeader = Buffer.concat([
|
|
@@ -10986,8 +10253,8 @@ function mjpegMatroskaFrame(timestampMs, durationMs, frameLength) {
|
|
|
10986
10253
|
Buffer.from([0, 0]),
|
|
10987
10254
|
Buffer.from([0])
|
|
10988
10255
|
]);
|
|
10989
|
-
const clusterTimestamp =
|
|
10990
|
-
const duration =
|
|
10256
|
+
const clusterTimestamp = element(timestamp, unsignedInteger(timestampMs));
|
|
10257
|
+
const duration = element(blockDuration, unsignedInteger(Math.max(1, durationMs)));
|
|
10991
10258
|
const groupHeader = Buffer.concat([
|
|
10992
10259
|
blockGroup,
|
|
10993
10260
|
variableInteger(blockHeader.length + frameLength + duration.length),
|
|
@@ -14331,28 +13598,6 @@ function handleAuthRequest(options) {
|
|
|
14331
13598
|
}
|
|
14332
13599
|
function handleCliRequest(options) {
|
|
14333
13600
|
return Effect22.gen(function* () {
|
|
14334
|
-
if (options.pathname === "/cli/deadbolt/logins" && options.request.method === "POST") {
|
|
14335
|
-
const request = yield* decodeRequest(DeadboltLoginsRequest, yield* readJsonBody2(options.request), "deadbolt logins");
|
|
14336
|
-
const sessionId = request.sessionId;
|
|
14337
|
-
const patterns = sessionId ? options.sessions.allowedOrigins(sessionId) : undefined;
|
|
14338
|
-
const pageOrigins = options.registry.listRootTargets().filter((target) => sessionId !== undefined && target.opencodeBrowserSessionId === sessionId).flatMap((target) => {
|
|
14339
|
-
try {
|
|
14340
|
-
const url = new URL(target.targetInfo.url);
|
|
14341
|
-
const allowed = !patterns || patterns.length === 0 || checkUrl(url.href, parseOriginPatterns(patterns)).allowed;
|
|
14342
|
-
return (url.protocol === "https:" || url.protocol === "http:") && allowed ? [url.origin] : [];
|
|
14343
|
-
} catch {
|
|
14344
|
-
return [];
|
|
14345
|
-
}
|
|
14346
|
-
});
|
|
14347
|
-
const origins = Array.from(new Set([...patterns && patterns.length > 0 ? originsFromPatterns(patterns) : [], ...pageOrigins]));
|
|
14348
|
-
const listing = yield* Effect22.promise(() => listLogins(origins));
|
|
14349
|
-
sendJson2(options.response, {
|
|
14350
|
-
...listing,
|
|
14351
|
-
origins,
|
|
14352
|
-
...patterns && patterns.length > 0 ? {} : { note: "This session has no domain allowlist, so only the sites of its open pages were checked. Set one with session_allowlist to see the logins for the sites it may visit." }
|
|
14353
|
-
});
|
|
14354
|
-
return;
|
|
14355
|
-
}
|
|
14356
13601
|
if (options.pathname === "/cli/sessions" && options.request.method === "GET") {
|
|
14357
13602
|
sendJson2(options.response, { sessions: options.sessions.listSummaries() });
|
|
14358
13603
|
return;
|
|
@@ -14832,285 +14077,6 @@ async function paceKey(tab, params) {
|
|
|
14832
14077
|
tab.lastKeyAt = Date.now();
|
|
14833
14078
|
}
|
|
14834
14079
|
|
|
14835
|
-
// src/auth/passkey-bridge.ts
|
|
14836
|
-
var selectionTtlMs = 2 * 60000;
|
|
14837
|
-
var maxPayloadBytes = 64 * 1024;
|
|
14838
|
-
function hookSource(binding, reply) {
|
|
14839
|
-
return `(() => {
|
|
14840
|
-
const BINDING = ${JSON.stringify(binding)};
|
|
14841
|
-
const REPLY = ${JSON.stringify(reply)};
|
|
14842
|
-
// Chrome adds a binding only to the documents that exist when it is added (without Runtime.enable, which pages can
|
|
14843
|
-
// detect), so the relay adds it again after every navigation. Take it off the global object as soon as it shows up.
|
|
14844
|
-
let send = null;
|
|
14845
|
-
const capture = () => {
|
|
14846
|
-
if (send) return send;
|
|
14847
|
-
const binding = globalThis[BINDING];
|
|
14848
|
-
if (typeof binding === "function") {
|
|
14849
|
-
send = binding;
|
|
14850
|
-
try { delete globalThis[BINDING]; } catch {}
|
|
14851
|
-
}
|
|
14852
|
-
return send;
|
|
14853
|
-
};
|
|
14854
|
-
capture();
|
|
14855
|
-
document.addEventListener("readystatechange", capture);
|
|
14856
|
-
if (window !== window.top) return;
|
|
14857
|
-
const container = globalThis.CredentialsContainer && CredentialsContainer.prototype;
|
|
14858
|
-
if (!container || typeof container.get !== "function" || globalThis[REPLY]) return;
|
|
14859
|
-
const originalGet = container.get;
|
|
14860
|
-
const pending = new Map();
|
|
14861
|
-
let nextId = 1;
|
|
14862
|
-
const toBytes = (value) => value instanceof ArrayBuffer ? new Uint8Array(value) : ArrayBuffer.isView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : null;
|
|
14863
|
-
const encode = (value) => {
|
|
14864
|
-
const bytes = toBytes(value);
|
|
14865
|
-
if (!bytes) return null;
|
|
14866
|
-
let text = "";
|
|
14867
|
-
for (const byte of bytes) text += String.fromCharCode(byte);
|
|
14868
|
-
return btoa(text).replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=+$/, "");
|
|
14869
|
-
};
|
|
14870
|
-
const decode = (text) => {
|
|
14871
|
-
const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((text.length + 3) % 4));
|
|
14872
|
-
const bytes = new Uint8Array(binary.length);
|
|
14873
|
-
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
14874
|
-
return bytes.buffer;
|
|
14875
|
-
};
|
|
14876
|
-
const assertion = (result) => {
|
|
14877
|
-
const value = (v) => ({ value: v, enumerable: true });
|
|
14878
|
-
const response = Object.create(AuthenticatorAssertionResponse.prototype, {
|
|
14879
|
-
clientDataJSON: value(decode(result.client_data_json)),
|
|
14880
|
-
authenticatorData: value(decode(result.authenticator_data)),
|
|
14881
|
-
signature: value(decode(result.signature)),
|
|
14882
|
-
userHandle: value(result.user_handle ? decode(result.user_handle) : null),
|
|
14883
|
-
});
|
|
14884
|
-
const json = {
|
|
14885
|
-
id: result.credential_id, rawId: result.credential_id, type: "public-key", authenticatorAttachment: "platform",
|
|
14886
|
-
response: { clientDataJSON: result.client_data_json, authenticatorData: result.authenticator_data, signature: result.signature, ...(result.user_handle ? { userHandle: result.user_handle } : {}) },
|
|
14887
|
-
clientExtensionResults: {},
|
|
14888
|
-
};
|
|
14889
|
-
return Object.create(PublicKeyCredential.prototype, {
|
|
14890
|
-
id: value(result.credential_id),
|
|
14891
|
-
rawId: value(decode(result.credential_id)),
|
|
14892
|
-
type: value("public-key"),
|
|
14893
|
-
authenticatorAttachment: value("platform"),
|
|
14894
|
-
response: value(response),
|
|
14895
|
-
getClientExtensionResults: { value: () => ({}) },
|
|
14896
|
-
toJSON: { value: () => json },
|
|
14897
|
-
});
|
|
14898
|
-
};
|
|
14899
|
-
Object.defineProperty(globalThis, REPLY, {
|
|
14900
|
-
value: (id, result) => {
|
|
14901
|
-
const entry = pending.get(id);
|
|
14902
|
-
if (!entry) return;
|
|
14903
|
-
pending.delete(id);
|
|
14904
|
-
if (result && result.ok) {
|
|
14905
|
-
try { entry.resolve(assertion(result.assertion)); } catch (error) { entry.reject(new DOMException("The passkey answer was malformed.", "NotAllowedError")); }
|
|
14906
|
-
} else {
|
|
14907
|
-
entry.reject(new DOMException("The operation either timed out or was not allowed.", "NotAllowedError"));
|
|
14908
|
-
}
|
|
14909
|
-
},
|
|
14910
|
-
});
|
|
14911
|
-
const get = { get(options) {
|
|
14912
|
-
const publicKey = options && options.publicKey;
|
|
14913
|
-
if (!publicKey || options.mediation === "conditional" || options.password || options.federated || options.identity || options.otp) return originalGet.call(this, options);
|
|
14914
|
-
const challenge = encode(publicKey.challenge);
|
|
14915
|
-
if (!challenge) return Promise.reject(new TypeError("publicKey.challenge must be a BufferSource"));
|
|
14916
|
-
const allowCredentials = Array.isArray(publicKey.allowCredentials) ? publicKey.allowCredentials.map((credential) => encode(credential && credential.id)).filter(Boolean) : [];
|
|
14917
|
-
const signal = options.signal;
|
|
14918
|
-
if (signal && signal.aborted) return Promise.reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
14919
|
-
return new Promise(async (resolve, reject) => {
|
|
14920
|
-
const id = nextId++;
|
|
14921
|
-
pending.set(id, { resolve, reject });
|
|
14922
|
-
if (signal) signal.addEventListener("abort", () => { if (pending.delete(id)) reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError")); }, { once: true });
|
|
14923
|
-
for (let tries = 0; tries < 60 && !capture(); tries++) await new Promise((wake) => setTimeout(wake, 50));
|
|
14924
|
-
if (!send) {
|
|
14925
|
-
pending.delete(id);
|
|
14926
|
-
return reject(new DOMException("The operation either timed out or was not allowed.", "NotAllowedError"));
|
|
14927
|
-
}
|
|
14928
|
-
send(JSON.stringify({ id, challenge, rpId: typeof publicKey.rpId === "string" ? publicKey.rpId : undefined, allowCredentials, userVerification: typeof publicKey.userVerification === "string" ? publicKey.userVerification : "preferred" }));
|
|
14929
|
-
});
|
|
14930
|
-
} }.get;
|
|
14931
|
-
Object.defineProperty(container, "get", { value: get, writable: true, configurable: true, enumerable: true });
|
|
14932
|
-
})();`;
|
|
14933
|
-
}
|
|
14934
|
-
function parsePasskeyRequest(payload) {
|
|
14935
|
-
if (typeof payload !== "string" || payload.length > maxPayloadBytes)
|
|
14936
|
-
return;
|
|
14937
|
-
let value;
|
|
14938
|
-
try {
|
|
14939
|
-
value = JSON.parse(payload);
|
|
14940
|
-
} catch {
|
|
14941
|
-
return;
|
|
14942
|
-
}
|
|
14943
|
-
if (!value || typeof value !== "object")
|
|
14944
|
-
return;
|
|
14945
|
-
const { id, challenge, rpId, allowCredentials, userVerification } = value;
|
|
14946
|
-
if (typeof id !== "number" || !Number.isSafeInteger(id) || id < 1)
|
|
14947
|
-
return;
|
|
14948
|
-
if (!isBase64Url(challenge, 1024) || challenge.length < 16)
|
|
14949
|
-
return;
|
|
14950
|
-
if (rpId !== undefined && (typeof rpId !== "string" || rpId.length > 253 || !/^[a-z0-9.-]+$/i.test(rpId)))
|
|
14951
|
-
return;
|
|
14952
|
-
if (!Array.isArray(allowCredentials) || allowCredentials.length > 64 || !allowCredentials.every((entry) => isBase64Url(entry, 1400)))
|
|
14953
|
-
return;
|
|
14954
|
-
const verification = typeof userVerification === "string" && ["required", "preferred", "discouraged"].includes(userVerification) ? userVerification : "preferred";
|
|
14955
|
-
return { id, challenge, ...rpId ? { rpId: rpId.toLowerCase() } : {}, allowCredentials, userVerification: verification };
|
|
14956
|
-
}
|
|
14957
|
-
function rpIdAllowed(origin, rpId) {
|
|
14958
|
-
let url;
|
|
14959
|
-
try {
|
|
14960
|
-
url = new URL(origin);
|
|
14961
|
-
} catch {
|
|
14962
|
-
return false;
|
|
14963
|
-
}
|
|
14964
|
-
const host = url.hostname.toLowerCase();
|
|
14965
|
-
const secure = url.protocol === "https:" || url.protocol === "http:" && (host === "localhost" || host.endsWith(".localhost") || /^127\.\d+\.\d+\.\d+$/.test(host) || host === "[::1]");
|
|
14966
|
-
if (!secure)
|
|
14967
|
-
return false;
|
|
14968
|
-
if (rpId === undefined)
|
|
14969
|
-
return true;
|
|
14970
|
-
return host === rpId || rpId.includes(".") && host.endsWith(`.${rpId}`);
|
|
14971
|
-
}
|
|
14972
|
-
|
|
14973
|
-
class PasskeyBridge {
|
|
14974
|
-
installs = new Map;
|
|
14975
|
-
selections = new Map;
|
|
14976
|
-
choices = new Map;
|
|
14977
|
-
options;
|
|
14978
|
-
constructor(options) {
|
|
14979
|
-
this.options = options;
|
|
14980
|
-
}
|
|
14981
|
-
async install(tabId) {
|
|
14982
|
-
const connected = this.options.connected ?? (() => readClient() !== undefined);
|
|
14983
|
-
if (!connected())
|
|
14984
|
-
return false;
|
|
14985
|
-
const install = { binding: randomName("__b"), reply: randomName("__r") };
|
|
14986
|
-
this.installs.set(tabId, install);
|
|
14987
|
-
try {
|
|
14988
|
-
await this.options.send(tabId, "Runtime.addBinding", { name: install.binding });
|
|
14989
|
-
await this.options.send(tabId, "Page.addScriptToEvaluateOnNewDocument", { source: hookSource(install.binding, install.reply), runImmediately: true });
|
|
14990
|
-
this.options.log?.("passkey.install", { tabId });
|
|
14991
|
-
return true;
|
|
14992
|
-
} catch (error) {
|
|
14993
|
-
this.installs.delete(tabId);
|
|
14994
|
-
this.options.log?.("passkey.install.failed", { tabId, error: error instanceof Error ? error.message : String(error) });
|
|
14995
|
-
return false;
|
|
14996
|
-
}
|
|
14997
|
-
}
|
|
14998
|
-
navigated(tabId) {
|
|
14999
|
-
const install = this.installs.get(tabId);
|
|
15000
|
-
if (!install)
|
|
15001
|
-
return;
|
|
15002
|
-
this.options.send(tabId, "Runtime.addBinding", { name: install.binding }).catch(() => {});
|
|
15003
|
-
}
|
|
15004
|
-
select(tabId, site, username, now = Date.now()) {
|
|
15005
|
-
this.selections.set(tabId, { site, username, expiresAt: now + selectionTtlMs });
|
|
15006
|
-
}
|
|
15007
|
-
clearSelection(tabId) {
|
|
15008
|
-
this.selections.delete(tabId);
|
|
15009
|
-
}
|
|
15010
|
-
choiceSince(tabId, since) {
|
|
15011
|
-
const event = this.choices.get(tabId);
|
|
15012
|
-
return event && event.at >= since ? event.choices : undefined;
|
|
15013
|
-
}
|
|
15014
|
-
forget(tabId) {
|
|
15015
|
-
this.installs.delete(tabId);
|
|
15016
|
-
this.selections.delete(tabId);
|
|
15017
|
-
this.choices.delete(tabId);
|
|
15018
|
-
}
|
|
15019
|
-
handleEvent(tabId, method, params, fromRoot) {
|
|
15020
|
-
if (method !== "Runtime.bindingCalled")
|
|
15021
|
-
return false;
|
|
15022
|
-
const install = this.installs.get(tabId);
|
|
15023
|
-
if (!install || params?.name !== install.binding)
|
|
15024
|
-
return false;
|
|
15025
|
-
this.options.log?.("passkey.request", { tabId, fromRoot });
|
|
15026
|
-
if (!fromRoot)
|
|
15027
|
-
return true;
|
|
15028
|
-
const contextId = params.executionContextId;
|
|
15029
|
-
if (typeof contextId !== "number")
|
|
15030
|
-
return true;
|
|
15031
|
-
this.answer(tabId, install, contextId, params.payload);
|
|
15032
|
-
return true;
|
|
15033
|
-
}
|
|
15034
|
-
async answer(tabId, install, contextId, payload) {
|
|
15035
|
-
const request = parsePasskeyRequest(payload);
|
|
15036
|
-
if (!request) {
|
|
15037
|
-
this.options.log?.("passkey.refused", { tabId, reason: "malformed request" });
|
|
15038
|
-
return;
|
|
15039
|
-
}
|
|
15040
|
-
let result;
|
|
15041
|
-
try {
|
|
15042
|
-
result = { ok: true, assertion: await this.assert(tabId, contextId, request) };
|
|
15043
|
-
this.options.log?.("passkey.signed", { tabId });
|
|
15044
|
-
} catch (error) {
|
|
15045
|
-
result = { ok: false };
|
|
15046
|
-
this.options.log?.("passkey.refused", { tabId, reason: error instanceof DeadboltError ? error.code : error instanceof Error ? error.message : "failed" });
|
|
15047
|
-
}
|
|
15048
|
-
await this.options.send(tabId, "Runtime.evaluate", {
|
|
15049
|
-
expression: `globalThis[${JSON.stringify(install.reply)}]?.(${request.id}, ${JSON.stringify(result)})`,
|
|
15050
|
-
contextId,
|
|
15051
|
-
returnByValue: true
|
|
15052
|
-
}).catch(() => {});
|
|
15053
|
-
}
|
|
15054
|
-
async assert(tabId, contextId, request) {
|
|
15055
|
-
const top = await this.options.send(tabId, "Runtime.evaluate", { expression: "window === top", contextId, returnByValue: true });
|
|
15056
|
-
if (top.result?.value !== true)
|
|
15057
|
-
throw new Error("not the top frame");
|
|
15058
|
-
const tree = await this.options.send(tabId, "Page.getFrameTree", {});
|
|
15059
|
-
const frame = tree.frameTree?.frame;
|
|
15060
|
-
const url = typeof frame?.url === "string" ? frame.url : "";
|
|
15061
|
-
let origin = typeof frame?.securityOrigin === "string" ? frame.securityOrigin : "";
|
|
15062
|
-
try {
|
|
15063
|
-
if (!origin || origin === "null")
|
|
15064
|
-
origin = new URL(url).origin;
|
|
15065
|
-
} catch {
|
|
15066
|
-
throw new Error("no origin");
|
|
15067
|
-
}
|
|
15068
|
-
if (!rpIdAllowed(origin, request.rpId))
|
|
15069
|
-
throw new Error("rpId not allowed for the origin");
|
|
15070
|
-
const patterns = this.options.allowedOrigins(tabId);
|
|
15071
|
-
if (patterns && patterns.length > 0 && !checkUrl(url, parseOriginPatterns(patterns)).allowed)
|
|
15072
|
-
throw new Error("outside the session allowlist");
|
|
15073
|
-
const deadbolt = (this.options.deadbolt ?? (() => connect()))();
|
|
15074
|
-
const selection = this.selections.get(tabId);
|
|
15075
|
-
this.selections.delete(tabId);
|
|
15076
|
-
let login;
|
|
15077
|
-
if (selection && selection.expiresAt >= Date.now() && originOnSite(origin, selection.site)) {
|
|
15078
|
-
const passkey = pickPasskey((await deadbolt.site(origin)).passkeys, selection.username);
|
|
15079
|
-
if (!passkey)
|
|
15080
|
-
throw new DeadboltError("not_granted", `No granted passkey for ${selection.username} on ${selection.site}`);
|
|
15081
|
-
login = passkey.ref;
|
|
15082
|
-
}
|
|
15083
|
-
const assertion = await deadbolt.passkeyGet({
|
|
15084
|
-
origin,
|
|
15085
|
-
...request.rpId ? { rpId: request.rpId } : {},
|
|
15086
|
-
challenge: request.challenge,
|
|
15087
|
-
allowCredentials: request.allowCredentials,
|
|
15088
|
-
userVerification: request.userVerification,
|
|
15089
|
-
...login !== undefined ? { login } : {}
|
|
15090
|
-
}).catch((error) => {
|
|
15091
|
-
if (error instanceof DeadboltError && error.code === "choose_passkey") {
|
|
15092
|
-
const offered = (error.detail.choices ?? []).map((choice) => ({
|
|
15093
|
-
...choice.username ? { username: choice.username } : {},
|
|
15094
|
-
...choice.displayName ? { displayName: choice.displayName } : {}
|
|
15095
|
-
}));
|
|
15096
|
-
this.choices.set(tabId, { at: Date.now(), choices: offered });
|
|
15097
|
-
}
|
|
15098
|
-
throw error;
|
|
15099
|
-
});
|
|
15100
|
-
for (const value of [assertion.credentialId, assertion.clientDataJson, assertion.authenticatorData, assertion.signature, assertion.userHandle ?? ""]) {
|
|
15101
|
-
if (!isBase64Url(value, 16384))
|
|
15102
|
-
throw new Error("Deadbolt's assertion isn't base64url");
|
|
15103
|
-
}
|
|
15104
|
-
return {
|
|
15105
|
-
credential_id: assertion.credentialId,
|
|
15106
|
-
client_data_json: assertion.clientDataJson,
|
|
15107
|
-
authenticator_data: assertion.authenticatorData,
|
|
15108
|
-
signature: assertion.signature,
|
|
15109
|
-
...assertion.userHandle ? { user_handle: assertion.userHandle } : {}
|
|
15110
|
-
};
|
|
15111
|
-
}
|
|
15112
|
-
}
|
|
15113
|
-
|
|
15114
14080
|
// src/cdp/protected-frames.ts
|
|
15115
14081
|
import { Predicate as Predicate18 } from "effect";
|
|
15116
14082
|
var navigationIntentMethods = new Set(["Page.frameRequestedNavigation", "Page.frameScheduledNavigation", "Page.frameStartedNavigating"]);
|
|
@@ -15354,7 +14320,7 @@ function guardCdpMethod(context) {
|
|
|
15354
14320
|
return `browse blocked ${context.method}: ${reason}. This command is always blocked by the relay.`;
|
|
15355
14321
|
}
|
|
15356
14322
|
if (context.method.startsWith("WebAuthn.")) {
|
|
15357
|
-
return `browse blocked ${context.method}: agents can't drive WebAuthn or add virtual authenticators.
|
|
14323
|
+
return `browse blocked ${context.method}: agents can't drive WebAuthn or add virtual authenticators. This command is always blocked by the relay.`;
|
|
15358
14324
|
}
|
|
15359
14325
|
if (context.method === "Page.bringToFront" && !allowsForeground()) {
|
|
15360
14326
|
return "browse blocked Page.bringToFront: it raises the user's browser window over their work. Use handoff() when the user needs to act in the tab, or set OPENCODE_BROWSER_ALLOW_FOREGROUND=1 on the relay to allow it.";
|
|
@@ -16007,7 +14973,6 @@ function createExtensionEventHandler(options) {
|
|
|
16007
14973
|
handoffs,
|
|
16008
14974
|
recordingRelay,
|
|
16009
14975
|
flightRecorder,
|
|
16010
|
-
passkeys,
|
|
16011
14976
|
navigationGuard,
|
|
16012
14977
|
ghostCursor,
|
|
16013
14978
|
tabActivity,
|
|
@@ -16316,8 +15281,6 @@ function createExtensionEventHandler(options) {
|
|
|
16316
15281
|
if (sourceSessionId && method !== "Target.attachedToTarget" && method !== "Target.detachedFromTarget" && method !== "Target.targetInfoChanged" && (suppressedChildSessions.has(sourceSessionId) || sourceChild && !shouldExposeChildTarget(sourceChild))) {
|
|
16317
15282
|
return;
|
|
16318
15283
|
}
|
|
16319
|
-
if (passkeys.handleEvent(tabId, method, params, sourceSessionId === undefined || sourceSessionId === target.sessionId))
|
|
16320
|
-
return;
|
|
16321
15284
|
if (flightRecorder.handleDebuggerEvent({ tabId, method, params }) || recordingRelay.handleDebuggerEvent({ tabId, method, params }))
|
|
16322
15285
|
return;
|
|
16323
15286
|
if (sourceSessionId === undefined || sourceSessionId === target.sessionId)
|
|
@@ -16450,7 +15413,6 @@ function createExtensionEventHandler(options) {
|
|
|
16450
15413
|
Effect27.runPromise(Effect27.ignore(ghostCursor.documentCreated(tabId))).catch(() => {});
|
|
16451
15414
|
contextDebugLog?.(`main-frame-navigated frame=${boundedToken(frameId)} loader=${boundedToken(getString2(frame, "loaderId"))} ${targetDiagnosticIdentity(registry, target)} ${summarizeDiagnosticUrl(frameUrl)}`);
|
|
16452
15415
|
registry.updateTargetUrl(tabId, frameUrl);
|
|
16453
|
-
passkeys.navigated(tabId);
|
|
16454
15416
|
blankNavigationWaiters.get(tabId)?.(frameUrl, getString2(frame, "loaderId"));
|
|
16455
15417
|
Effect27.runPromise(Effect27.ignore(sendDebuggerCommand({ tabId, method: "Emulation.setFocusEmulationEnabled", params: { enabled: tabActivity.isFocused(tabId) } }))).catch(() => {});
|
|
16456
15418
|
}
|
|
@@ -17157,7 +16119,6 @@ function createTabPresentation(options) {
|
|
|
17157
16119
|
registry,
|
|
17158
16120
|
browsers,
|
|
17159
16121
|
handoffs,
|
|
17160
|
-
passkeys,
|
|
17161
16122
|
tabActivity,
|
|
17162
16123
|
mainFrameIdsByTab,
|
|
17163
16124
|
sendToExtension,
|
|
@@ -17380,9 +16341,6 @@ function createTabPresentation(options) {
|
|
|
17380
16341
|
if (committedTarget.opencodeBrowserSessionId) {
|
|
17381
16342
|
pruneInvisibleAnnouncementsForSession(committedTarget.opencodeBrowserSessionId);
|
|
17382
16343
|
}
|
|
17383
|
-
if (committedTarget.owner === "relay" && committedTarget.opencodeBrowserSessionId) {
|
|
17384
|
-
passkeys.install(tabId);
|
|
17385
|
-
}
|
|
17386
16344
|
if (browsers.browserTab(tabId)?.link.rpc.connected) {
|
|
17387
16345
|
refreshTabGrouping(tabId);
|
|
17388
16346
|
}
|
|
@@ -18195,14 +17153,6 @@ var makeRelay = Effect34.fnUntraced(function* (options = {}) {
|
|
|
18195
17153
|
const handoffs = new HandoffRegistry;
|
|
18196
17154
|
const ownersAcrossReconnect = new Map;
|
|
18197
17155
|
const sweptBrowsers = new Set;
|
|
18198
|
-
const passkeys = new PasskeyBridge({
|
|
18199
|
-
send: (tabId, method, params) => Effect34.runPromise(sendDebuggerCommand({ tabId, method, params })),
|
|
18200
|
-
allowedOrigins: (tabId) => {
|
|
18201
|
-
const sessionId = registry.tabTargets.get(tabId)?.opencodeBrowserSessionId;
|
|
18202
|
-
return sessionId ? sessions.allowedOrigins(sessionId) : undefined;
|
|
18203
|
-
},
|
|
18204
|
-
log: (event, fields) => relayLog(event, fields)
|
|
18205
|
-
});
|
|
18206
17156
|
const recordingRelay = new RecordingRelay({
|
|
18207
17157
|
sendToExtension: (command) => Effect34.runPromise(sendToExtension(command)),
|
|
18208
17158
|
sendDebuggerCommand: (command) => Effect34.runPromise(sendDebuggerCommand(command)),
|
|
@@ -18237,7 +17187,6 @@ var makeRelay = Effect34.fnUntraced(function* (options = {}) {
|
|
|
18237
17187
|
registry,
|
|
18238
17188
|
browsers,
|
|
18239
17189
|
handoffs,
|
|
18240
|
-
passkeys,
|
|
18241
17190
|
tabActivity,
|
|
18242
17191
|
mainFrameIdsByTab,
|
|
18243
17192
|
sendToExtension,
|
|
@@ -18304,22 +17253,6 @@ var makeRelay = Effect34.fnUntraced(function* (options = {}) {
|
|
|
18304
17253
|
onDefaultTargetChange,
|
|
18305
17254
|
allowedOrigins: () => sessions.allowedOrigins(id),
|
|
18306
17255
|
drainGuardWarnings: () => navigationGuard.drainWarnings(id),
|
|
18307
|
-
passkeys: {
|
|
18308
|
-
select: (targetId, site, username) => {
|
|
18309
|
-
const target = registry.targetsByTargetId.get(targetId);
|
|
18310
|
-
if (!target || !("owner" in target) || target.owner !== "relay" || target.opencodeBrowserSessionId !== id) {
|
|
18311
|
-
throw new Error("secrets.usePasskey: passkeys are answered only in tabs this session opened");
|
|
18312
|
-
}
|
|
18313
|
-
if (username)
|
|
18314
|
-
passkeys.select(target.tabId, site, username);
|
|
18315
|
-
else
|
|
18316
|
-
passkeys.clearSelection(target.tabId);
|
|
18317
|
-
},
|
|
18318
|
-
choiceSince: (targetId, since) => {
|
|
18319
|
-
const target = registry.targetsByTargetId.get(targetId);
|
|
18320
|
-
return target && target.opencodeBrowserSessionId === id ? passkeys.choiceSince(target.tabId, since) : undefined;
|
|
18321
|
-
}
|
|
18322
|
-
},
|
|
18323
17256
|
isRecordingTarget: (targetId) => {
|
|
18324
17257
|
const target = registry.targetsByTargetId.get(targetId);
|
|
18325
17258
|
return target !== undefined && isTabRecorded(target.tabId);
|
|
@@ -18515,7 +17448,6 @@ var makeRelay = Effect34.fnUntraced(function* (options = {}) {
|
|
|
18515
17448
|
navigationGuard.forgetTab(tabId);
|
|
18516
17449
|
ghostCursor.forget(tabId);
|
|
18517
17450
|
forgetTab(tabId);
|
|
18518
|
-
passkeys.forget(tabId);
|
|
18519
17451
|
for (const [sessionId, childTabId] of suppressedChildSessions) {
|
|
18520
17452
|
if (childTabId === tabId)
|
|
18521
17453
|
suppressedChildSessions.delete(sessionId);
|
|
@@ -18596,7 +17528,6 @@ var makeRelay = Effect34.fnUntraced(function* (options = {}) {
|
|
|
18596
17528
|
handoffs,
|
|
18597
17529
|
recordingRelay,
|
|
18598
17530
|
flightRecorder,
|
|
18599
|
-
passkeys,
|
|
18600
17531
|
navigationGuard,
|
|
18601
17532
|
ghostCursor,
|
|
18602
17533
|
tabActivity,
|