Loading secrets in BAP
loadSecret() fills a credential straight from 1Password into a login form. The value is resolved in the browser at the moment it's typed, so it never appears in your code, your query, or the response. This guide covers the setup it needs and the session lockdown it deliberately triggers.
- A Browserless API token from your account dashboard
- The BAP SDK installed and connecting, from the BAP Quickstart
- A 1Password service account configured in your dashboard, with the target site in its allowed-domain list
Connect with an integration
The session has to know which 1Password service account to resolve against, and that's a connection-time decision: append integrationId to the endpoint. A loadSecret() call on a session without one fails.
- TypeScript
- Python
import Browserless from "@browserless.io/bap-ts";
const TOKEN = "YOUR_API_TOKEN_HERE";
const INTEGRATION_ID = "YOUR_SERVICE_ACCOUNT_ID";
const browser = Browserless.connect({
// The integration is bound to the session, not to the call.
browserWSEndpoint: `wss://production-sfo.browserless.io/chromium/bql?integrationId=${INTEGRATION_ID}`,
token: TOKEN,
});
import bap.sync_api as bap
TOKEN = "YOUR_API_TOKEN_HERE"
INTEGRATION_ID = "YOUR_SERVICE_ACCOUNT_ID"
# The integration is bound to the session, not to the call.
endpoint = (
"wss://production-sfo.browserless.io/chromium/bql"
f"?integrationId={INTEGRATION_ID}"
)
with bap.Browserless.connect(browser_ws_endpoint=endpoint, token=TOKEN) as browser:
...
Fill a credential
Pass the 1Password op:// reference and the input to fill. With no selector, the currently focused element receives the value, which suits flows where you've just tabbed or clicked into the field.
- TypeScript
- Python
try {
const page = await browser.newPage();
await page.goto("https://example.com/login");
// The username is not a secret, so type it normally.
await page.type("#username", "name@example.com");
// The password is resolved in the browser and never reaches this script.
const result = await page.loadSecret("op://Vault/ExampleLogin/password", {
selector: "#password",
});
// Bail out rather than submitting an empty password field.
if (!result.ok) {
throw new Error(`${result.error}: ${result.message}`);
}
const navigated = page.waitForNavigation({ waitUntil: "networkIdle" });
await page.click("#submit");
await navigated;
} finally {
await browser.close();
}
with browser.page() as page:
page.goto("https://example.com/login")
# The username is not a secret, so type it normally.
page.type("#username", "name@example.com")
# The password is resolved in the browser and never reaches this script.
result = page.load_secret(
"op://Vault/ExampleLogin/password",
selector="#password",
)
# Bail out rather than submitting an empty password field.
if not result["ok"]:
raise RuntimeError(f"{result['error']}: {result['message']}")
page.click("#submit")
page.wait_for_selector("#dashboard")
The response carries ok, plus an error code and human-readable message when it's false. Codes include DomainNotAllowed, SelectorNotFound, NoFocusedElement, TargetNotFillable, and CredentialNotResolved, which between them tell you whether the problem is your integration's allowed domains, your selector, or the reference itself.
The session locks down after the first secret
This is the part worth reading before you build on it. Once any secret has been filled, the session refuses everything that could read the credential back out, for the rest of its life:
- Captures: screenshots, PDFs, screencasts, Live URLs, and session recording
- Content readback:
evaluate(),html(),text(),querySelector(),querySelectorAll(), andcookies()
That's deliberate. A filled password is present in the DOM, so a screenshot or an html() call would hand back the very value the feature exists to protect. The consequence for your script is that anything you need to read or capture has to happen before the loadSecret() call, and afterward you're limited to interaction and navigation.
Reconnecting doesn't lift this. reconnect() returns you to the same browser session, and the lockdown lasts for that session's life, so the restrictions follow you across the reconnect. When a run needs both a loadSecret() login and captures afterward, use a separate session for the capture work.
- TypeScript
- Python
// Wrong order: this html() call rejects, because a secret was already filled.
await page.loadSecret("op://Vault/ExampleLogin/password", { selector: "#password" });
await page.click("#submit");
const { html } = await page.html();
// Right order: capture what you need first, then authenticate.
const { html: before } = await page.html();
await page.loadSecret("op://Vault/ExampleLogin/password", { selector: "#password" });
await page.click("#submit");
# Wrong order: this html() call fails, because a secret was already filled.
page.load_secret("op://Vault/ExampleLogin/password", selector="#password")
page.click("#submit")
html = page.html()["html"]
# Right order: capture what you need first, then authenticate.
before = page.html()["html"]
page.load_secret("op://Vault/ExampleLogin/password", selector="#password")
page.click("#submit")
When to use this instead of cookies
loadSecret() and cookie injection both avoid putting a password in your code, and they suit different situations.
Reach for loadSecret() when the site requires a real login each run, when a human rotates the credential in 1Password and you want automation to pick that up without a deploy, or when your threat model says the password must never exist in your process memory.
Reach for cookies when you can log in once and reuse the session, since you skip the login entirely and keep full capture and readback for the whole run.
FAQ & Troubleshooting
My screenshot fails after logging in with loadSecret()
That's the intended lockdown, not a bug. Filling a secret disables captures and content readback for the rest of the session, because a screenshot of a filled password field would defeat the purpose. Capture before the call, or do the capture work in a separate session. Reconnecting won't help, since it returns to the same session and inherits the same restrictions.
The call returns DomainNotAllowed
The page's origin isn't in the service account's allowed-domain list. Add it in the dashboard under the 1Password service account, and check you're on the origin you expect after any redirects.
The call returns NoFocusedElement
You omitted selector and nothing had focus. Either pass the input's selector, or click into the field first so the focused element is the one you want filled.
Can I read the secret's value in my script?
No, by design. The value is resolved in the browser and appears in neither the query nor the response, and the readback methods that could expose it are disabled once it's filled. If you need the value itself, fetch it from 1Password directly in your own code rather than through BAP.