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