libretto 0.6.27 → 0.6.29
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/commands/execution.js +24 -10
- package/dist/cli/core/browser.js +1 -0
- package/dist/cli/core/daemon/daemon.js +2 -0
- package/dist/shared/run/browser.js +3 -32
- package/dist/shared/run/window-position.d.ts +9 -0
- package/dist/shared/run/window-position.js +36 -0
- package/dist/shared/workflow/authenticate.d.ts +2 -2
- package/dist/shared/workflow/authenticate.js +4 -4
- package/docs/releasing.md +9 -6
- package/package.json +5 -2
- package/skills/libretto/SKILL.md +12 -15
- package/skills/libretto/references/code-generation-rules.md +2 -9
- package/skills/libretto/references/shipped-source-and-documentation.md +13 -0
- package/skills/libretto/references/site-security-review.md +7 -6
- package/skills/libretto/references/website-authentication.md +73 -0
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/execution.ts +38 -12
- package/src/cli/core/browser.ts +1 -1
- package/src/cli/core/daemon/daemon.ts +2 -0
- package/src/shared/run/browser.ts +5 -44
- package/src/shared/run/window-position.ts +49 -0
- package/src/shared/workflow/authenticate.ts +6 -6
- package/skills/libretto/references/auth-profiles.md +0 -79
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
connect,
|
|
7
7
|
disconnectBrowser,
|
|
8
8
|
runClose,
|
|
9
|
+
resolveWindowPosition,
|
|
9
10
|
resolveViewport
|
|
10
11
|
} from "../core/browser.js";
|
|
11
12
|
import { parseViewportArg } from "./browser.js";
|
|
@@ -20,7 +21,9 @@ import {
|
|
|
20
21
|
writeSessionState
|
|
21
22
|
} from "../core/session.js";
|
|
22
23
|
import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
readLibrettoConfig
|
|
26
|
+
} from "../core/config.js";
|
|
24
27
|
import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
|
|
25
28
|
import {
|
|
26
29
|
getProviderStartupTimeoutMs,
|
|
@@ -51,6 +54,20 @@ import {
|
|
|
51
54
|
withRequiredSession
|
|
52
55
|
} from "./shared.js";
|
|
53
56
|
const require2 = moduleBuiltin.createRequire(import.meta.url);
|
|
57
|
+
function createRunBrowserConfig(args) {
|
|
58
|
+
if (args.providerName) {
|
|
59
|
+
return {
|
|
60
|
+
kind: "provider",
|
|
61
|
+
providerName: args.providerName
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
kind: "launch",
|
|
66
|
+
headed: !args.headless,
|
|
67
|
+
viewport: args.viewport ?? { width: 1366, height: 768 },
|
|
68
|
+
...!args.headless && args.windowPosition ? { windowPosition: args.windowPosition } : {}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
54
71
|
function writeDaemonExecOutput(output) {
|
|
55
72
|
if (output?.stdout) {
|
|
56
73
|
process.stdout.write(output.stdout);
|
|
@@ -427,14 +444,7 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
427
444
|
config: {
|
|
428
445
|
session: args.session,
|
|
429
446
|
experiments: args.experiments,
|
|
430
|
-
browser: args
|
|
431
|
-
kind: "provider",
|
|
432
|
-
providerName: args.providerName
|
|
433
|
-
} : {
|
|
434
|
-
kind: "launch",
|
|
435
|
-
headed: !args.headless,
|
|
436
|
-
viewport: args.viewport ?? { width: 1366, height: 768 }
|
|
437
|
-
},
|
|
447
|
+
browser: createRunBrowserConfig(args),
|
|
438
448
|
workflow: {
|
|
439
449
|
integrationPath: absoluteIntegrationPath,
|
|
440
450
|
params: args.params,
|
|
@@ -690,15 +700,18 @@ const runCommand = SimpleCLI.command({
|
|
|
690
700
|
});
|
|
691
701
|
console.log(`Connecting to ${providerName} browser...`);
|
|
692
702
|
}
|
|
703
|
+
const headless = daemonProviderName ? true : headlessMode ?? false;
|
|
704
|
+
const windowPosition = headless ? void 0 : resolveWindowPosition(ctx.logger);
|
|
693
705
|
await runIntegrationFromFile(
|
|
694
706
|
{
|
|
695
707
|
integrationPath: input.integrationFile,
|
|
696
708
|
session: ctx.session,
|
|
697
709
|
params,
|
|
698
710
|
tsconfigPath: input.tsconfig,
|
|
699
|
-
headless
|
|
711
|
+
headless,
|
|
700
712
|
visualize,
|
|
701
713
|
viewport,
|
|
714
|
+
windowPosition,
|
|
702
715
|
accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : readLibrettoConfig().sessionMode ?? "write-access",
|
|
703
716
|
providerName: daemonProviderName,
|
|
704
717
|
stayOpenOnSuccess: input.stayOpenOnSuccess,
|
|
@@ -725,6 +738,7 @@ const executionCommands = {
|
|
|
725
738
|
resume: resumeCommand
|
|
726
739
|
};
|
|
727
740
|
export {
|
|
741
|
+
createRunBrowserConfig,
|
|
728
742
|
execCommand,
|
|
729
743
|
execInput,
|
|
730
744
|
executionCommands,
|
package/dist/cli/core/browser.js
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
import { WorkflowController } from "../workflow-runner/runner.js";
|
|
50
50
|
import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
|
|
51
51
|
import { captureAuthProfileStorageState } from "../../../shared/workflow/auth-profile-state.js";
|
|
52
|
+
import { applyWindowPosition } from "../../../shared/run/window-position.js";
|
|
52
53
|
function isOperationalPage(page) {
|
|
53
54
|
const url = page.url();
|
|
54
55
|
return !url.startsWith("devtools://") && !url.startsWith("chrome-error://");
|
|
@@ -266,6 +267,7 @@ class BrowserDaemon {
|
|
|
266
267
|
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
|
267
268
|
});
|
|
268
269
|
const page = await context.newPage();
|
|
270
|
+
await applyWindowPosition(browser, context, page, config.windowPosition);
|
|
269
271
|
page.setDefaultTimeout(3e4);
|
|
270
272
|
page.setDefaultNavigationTimeout(45e3);
|
|
271
273
|
const daemon = await BrowserDaemon.initialize({
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
SessionStateFileSchema
|
|
10
10
|
} from "../state/session-state.js";
|
|
11
11
|
import { readLibrettoConfig } from "../../cli/core/config.js";
|
|
12
|
+
import {
|
|
13
|
+
applyWindowPosition
|
|
14
|
+
} from "./window-position.js";
|
|
12
15
|
async function pickFreePort() {
|
|
13
16
|
return await new Promise((resolve, reject) => {
|
|
14
17
|
const server = createServer();
|
|
@@ -27,38 +30,6 @@ async function pickFreePort() {
|
|
|
27
30
|
function resolveWindowPosition() {
|
|
28
31
|
return readLibrettoConfig().windowPosition;
|
|
29
32
|
}
|
|
30
|
-
async function applyWindowPosition(browser, context, page, windowPosition) {
|
|
31
|
-
if (!windowPosition) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const requestedBounds = {
|
|
35
|
-
left: windowPosition.x,
|
|
36
|
-
top: windowPosition.y,
|
|
37
|
-
windowState: "normal"
|
|
38
|
-
};
|
|
39
|
-
const pageCdp = await context.newCDPSession(page);
|
|
40
|
-
let browserCdp;
|
|
41
|
-
try {
|
|
42
|
-
const targetInfo = await pageCdp.send("Target.getTargetInfo");
|
|
43
|
-
const targetId = targetInfo.targetInfo?.targetId;
|
|
44
|
-
browserCdp = await browser.newBrowserCDPSession();
|
|
45
|
-
const windowResult = await browserCdp.send(
|
|
46
|
-
"Browser.getWindowForTarget",
|
|
47
|
-
targetId ? { targetId } : {}
|
|
48
|
-
);
|
|
49
|
-
await browserCdp.send("Browser.setWindowBounds", {
|
|
50
|
-
windowId: windowResult.windowId,
|
|
51
|
-
bounds: requestedBounds
|
|
52
|
-
});
|
|
53
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
54
|
-
} catch {
|
|
55
|
-
} finally {
|
|
56
|
-
await pageCdp.detach().catch(() => {
|
|
57
|
-
});
|
|
58
|
-
await browserCdp?.detach().catch(() => {
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
33
|
async function launchBrowser({
|
|
63
34
|
sessionName,
|
|
64
35
|
headless = false,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Browser, BrowserContext, Page } from 'playwright';
|
|
2
|
+
|
|
3
|
+
type WindowPosition = {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
};
|
|
7
|
+
declare function applyWindowPosition(browser: Browser, context: BrowserContext, page: Page, windowPosition: WindowPosition | undefined): Promise<void>;
|
|
8
|
+
|
|
9
|
+
export { type WindowPosition, applyWindowPosition };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
async function applyWindowPosition(browser, context, page, windowPosition) {
|
|
2
|
+
if (!windowPosition) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
const requestedBounds = {
|
|
6
|
+
left: windowPosition.x,
|
|
7
|
+
top: windowPosition.y,
|
|
8
|
+
windowState: "normal"
|
|
9
|
+
};
|
|
10
|
+
let pageCdp;
|
|
11
|
+
let browserCdp;
|
|
12
|
+
try {
|
|
13
|
+
pageCdp = await context.newCDPSession(page);
|
|
14
|
+
const targetInfo = await pageCdp.send("Target.getTargetInfo");
|
|
15
|
+
const targetId = targetInfo.targetInfo?.targetId;
|
|
16
|
+
browserCdp = await browser.newBrowserCDPSession();
|
|
17
|
+
const windowResult = await browserCdp.send(
|
|
18
|
+
"Browser.getWindowForTarget",
|
|
19
|
+
targetId ? { targetId } : {}
|
|
20
|
+
);
|
|
21
|
+
await browserCdp.send("Browser.setWindowBounds", {
|
|
22
|
+
windowId: windowResult.windowId,
|
|
23
|
+
bounds: requestedBounds
|
|
24
|
+
});
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
26
|
+
} catch {
|
|
27
|
+
} finally {
|
|
28
|
+
await pageCdp?.detach().catch(() => {
|
|
29
|
+
});
|
|
30
|
+
await browserCdp?.detach().catch(() => {
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
applyWindowPosition
|
|
36
|
+
};
|
|
@@ -5,8 +5,8 @@ import '../../runtime/recovery/page-fallbacks.js';
|
|
|
5
5
|
import 'ai';
|
|
6
6
|
|
|
7
7
|
type LibrettoAuthenticateOptions = {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
isSignedIn: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
|
|
9
|
+
signIn: (ctx: LibrettoWorkflowContext, credentials: Record<string, string>) => Promise<void> | void;
|
|
10
10
|
credentials?: Record<string, unknown>;
|
|
11
11
|
envPrefix?: string;
|
|
12
12
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
async function librettoAuthenticate(ctx, options) {
|
|
2
|
-
if (await options.
|
|
2
|
+
if (await options.isSignedIn(ctx)) {
|
|
3
3
|
return { usedProfile: true };
|
|
4
4
|
}
|
|
5
5
|
const credentials = normalizeCredentials(
|
|
6
6
|
options.credentials ?? readCredentialsFromEnv(options.envPrefix)
|
|
7
7
|
);
|
|
8
|
-
await options.
|
|
9
|
-
if (!await options.
|
|
10
|
-
throw new Error("
|
|
8
|
+
await options.signIn(ctx, credentials);
|
|
9
|
+
if (!await options.isSignedIn(ctx)) {
|
|
10
|
+
throw new Error("Sign-in completed, but the session is still not signed in.");
|
|
11
11
|
}
|
|
12
12
|
return { usedProfile: false };
|
|
13
13
|
}
|
package/docs/releasing.md
CHANGED
|
@@ -66,10 +66,13 @@ The root `scripts/prepare-release.sh` script does the following:
|
|
|
66
66
|
1. Checks that the working tree is clean.
|
|
67
67
|
2. Updates local `main` from `origin/main`.
|
|
68
68
|
3. Runs `pnpm install --frozen-lockfile`, `pnpm --filter libretto type-check`, and `pnpm --filter libretto test`.
|
|
69
|
-
4.
|
|
70
|
-
5.
|
|
71
|
-
6.
|
|
72
|
-
7.
|
|
69
|
+
4. Checks whether `packages/affordance` changed since the current Libretto release tag, excluding `packages/affordance/package.json` version-only changes. If source/package contents changed, the script runs Affordance type-check/tests and bumps `packages/affordance/package.json` by one patch version when the current Affordance version is already published to npm.
|
|
70
|
+
5. Bumps the version in `packages/libretto/package.json`.
|
|
71
|
+
6. Creates a release branch.
|
|
72
|
+
7. Commits the version bump.
|
|
73
|
+
8. Pushes the branch and opens a PR to `main` with the `release` label.
|
|
74
|
+
|
|
75
|
+
Affordance is published before Libretto in `.github/workflows/release.yml`. Keeping this automatic patch bump in the release PR prevents Libretto from depending on unpublished Affordance APIs while still avoiding unnecessary Affordance releases when its package contents did not change.
|
|
73
76
|
|
|
74
77
|
Release PRs also run the eval workflow. That workflow records score, duration, token, cost, and tool-call metrics for review. Scores are informational: low scores do not fail the workflow, but setup/runtime failures and zero completed records do.
|
|
75
78
|
|
|
@@ -80,12 +83,12 @@ After the release PR merges, `.github/workflows/release.yml` runs on `main`.
|
|
|
80
83
|
The workflow:
|
|
81
84
|
|
|
82
85
|
1. Reads the version from `packages/libretto/package.json`.
|
|
83
|
-
2. Checks whether that version already exists on npm and in GitHub Releases.
|
|
86
|
+
2. Checks whether that version already exists on npm and in GitHub Releases. It also checks whether the current `packages/affordance/package.json` version and matching `create-libretto` version already exist on npm.
|
|
84
87
|
3. Runs install, type-check, and tests for the `libretto` package in a verification job.
|
|
85
88
|
4. Publishes `affordance` first, then `libretto@X.Y.Z`, then `create-libretto@X.Y.Z` with trusted publishing.
|
|
86
89
|
5. Creates GitHub release `vX.Y.Z` with generated release notes if it does not already exist.
|
|
87
90
|
|
|
88
|
-
This makes the workflow safe to re-run after partial failures. For example, if npm publish succeeds but GitHub release creation fails, a re-run will skip npm and only create the missing release.
|
|
91
|
+
This makes the workflow safe to re-run after partial failures. For example, if npm publish succeeds but GitHub release creation fails, a re-run will skip npm and only create the missing release. It also means Affordance can still be published if its version is missing even when the Libretto npm package and GitHub release for the current Libretto version already exist.
|
|
89
92
|
|
|
90
93
|
## Eval gating on release PRs
|
|
91
94
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.29",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://libretto.sh",
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/saffron-health/libretto"
|
|
10
10
|
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/saffron-health/libretto/issues"
|
|
13
|
+
},
|
|
11
14
|
"type": "module",
|
|
12
15
|
"publishConfig": {
|
|
13
16
|
"access": "public"
|
|
@@ -67,7 +70,7 @@
|
|
|
67
70
|
"playwright": "^1.58.2",
|
|
68
71
|
"tsx": "^4.21.0",
|
|
69
72
|
"zod": "^4.3.6",
|
|
70
|
-
"affordance": "^0.2.
|
|
73
|
+
"affordance": "^0.2.1"
|
|
71
74
|
},
|
|
72
75
|
"scripts": {
|
|
73
76
|
"sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
|
package/skills/libretto/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: saffron-health
|
|
7
|
-
version: "0.6.
|
|
7
|
+
version: "0.6.29"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## How Libretto Works
|
|
@@ -15,17 +15,7 @@ metadata:
|
|
|
15
15
|
|
|
16
16
|
## Shipped Source & Documentation
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Full documentation is published at [libretto.sh](https://libretto.sh). Available pages:
|
|
21
|
-
|
|
22
|
-
- Get started: [quickstart](https://libretto.sh/docs/get-started/quickstart), [first workflow](https://libretto.sh/docs/get-started/first-workflow), [deploying](https://libretto.sh/docs/get-started/deploying)
|
|
23
|
-
- Fundamentals: [core concepts](https://libretto.sh/docs/understand-libretto/core-concepts), [how workflow generation works](https://libretto.sh/docs/understand-libretto/how-workflow-generation-works), [automation and bot detection](https://libretto.sh/docs/understand-libretto/automation-and-bot-detection), [website authentication](https://libretto.sh/docs/understand-libretto/website-authentication)
|
|
24
|
-
- Workflow guides: [one-shot generation](https://libretto.sh/docs/guides/one-shot-workflow-generation), [interactive building](https://libretto.sh/docs/guides/interactive-workflow-building), [debugging workflows](https://libretto.sh/docs/guides/debugging-workflows), [convert to network requests](https://libretto.sh/docs/guides/convert-to-network-requests)
|
|
25
|
-
- CLI reference: [open and connect](https://libretto.sh/docs/reference/cli/open-and-connect), [sessions](https://libretto.sh/docs/reference/cli/sessions), [profiles](https://libretto.sh/docs/reference/cli/profiles), [snapshot](https://libretto.sh/docs/reference/cli/snapshot), [exec](https://libretto.sh/docs/reference/cli/exec), [run and resume](https://libretto.sh/docs/reference/cli/run-and-resume), [session logs](https://libretto.sh/docs/reference/cli/session-logs), [pages](https://libretto.sh/docs/reference/cli/pages)
|
|
26
|
-
- Library API: [workflow](https://libretto.sh/docs/reference/runtime/workflow), [network requests](https://libretto.sh/docs/reference/runtime/network-requests), [file downloads](https://libretto.sh/docs/reference/runtime/file-downloads)
|
|
27
|
-
- Libretto Cloud Hosting: [overview](https://libretto.sh/docs/libretto-cloud-hosting/overview), [authentication](https://libretto.sh/docs/libretto-cloud-hosting/authentication), [deployments](https://libretto.sh/docs/libretto-cloud-hosting/deployments), [stealth](https://libretto.sh/docs/libretto-cloud-hosting/stealth)
|
|
28
|
-
- Alternative providers: [overview](https://libretto.sh/docs/alternative-providers/overview), [Kernel](https://libretto.sh/docs/alternative-providers/kernel), [Browserbase](https://libretto.sh/docs/alternative-providers/browserbase), [Steel](https://libretto.sh/docs/alternative-providers/steel), [GCP](https://libretto.sh/docs/alternative-providers/gcp), [AWS](https://libretto.sh/docs/alternative-providers/aws)
|
|
18
|
+
Read `references/shipped-source-and-documentation.md` for shipped source details, published documentation links, and implementation context beyond what this skill file covers.
|
|
29
19
|
|
|
30
20
|
## Default Integration Approach
|
|
31
21
|
|
|
@@ -48,6 +38,7 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
|
|
|
48
38
|
- Use the package manager convention for the target project. The examples use `npx libretto`; pnpm, yarn, and bun projects should use their equivalent package-manager execution form.
|
|
49
39
|
- Use `npx libretto setup` for first-time workspace onboarding. It installs Chromium and syncs skills.
|
|
50
40
|
- Use `npx libretto status` to inspect open sessions without triggering setup.
|
|
41
|
+
- Use `npx libretto update` to upgrade the project-local Libretto package. Use `npx libretto update --dry-run` to preview the package-manager command first.
|
|
51
42
|
|
|
52
43
|
## Experiments
|
|
53
44
|
|
|
@@ -60,7 +51,11 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
|
|
|
60
51
|
- Do not treat visibility as interactivity. If an element will not act, inspect blockers before retrying.
|
|
61
52
|
- Defer repo/code review until you begin generating code, unless the user explicitly asks for it earlier.
|
|
62
53
|
- Read and follow guidelines in `references/code-generation-rules.md` before generating or editing production workflow code.
|
|
63
|
-
-
|
|
54
|
+
- For authenticated workflows, manual login is discovery only. After the user logs in, read only the sign-in action logs and identify the required credentials; if credentials are unclear, ask before writing code.
|
|
55
|
+
- Add missing blank `LIBRETTO_CLOUD_<secret_name>=` entries without overwriting populated values. If any required credential is blank, stop and ask the user to fill it; until then, do not inspect logged-in pages, read authenticated network bodies, write workflow code, open validation sessions, or continue discovery.
|
|
56
|
+
- Authenticated workflows must implement `librettoAuthenticate` with declared credentials before validation. Use a reusable `*_totp_secret` credential for authenticator-app MFA, not a one-time `otp_code`; text and email verification codes are not supported for fully automated sign-in.
|
|
57
|
+
- Read `references/website-authentication.md` when you need `librettoAuthenticate` examples or auth-profile details.
|
|
58
|
+
- Validation requires a successful clean `run` on a fresh, unauthenticated session with confirmation of the actual returned output, not just process success. Use the same headed or headless mode that the workflow run is already using.
|
|
64
59
|
- After validation, always show the user: (1) the output/results from the validation run, and (2) the same command so they can re-run it themselves. Include any `--params`, `--headed`, or `--headless` flags the workflow needs.
|
|
65
60
|
- Treat exploration sessions as disposable unless the user explicitly wants one kept open.
|
|
66
61
|
- Get explicit user confirmation before mutating actions or replaying network requests that may have side effects.
|
|
@@ -241,13 +236,15 @@ Key fields: `id` (incrementing request id), `ts` (ISO timestamp), `pageId` (page
|
|
|
241
236
|
[Context: The user wants to build a new browser workflow and does not yet know the page structure]
|
|
242
237
|
Assistant: I'll inspect the real site first if needed, but before I finish I'll create `target-workflow.ts` so the task produces reusable automation code.
|
|
243
238
|
Assistant: [Runs `npx libretto open https://target.example.com --headed`]
|
|
239
|
+
Assistant: [If sign-in is required, follows the authenticated workflow rules before continuing]
|
|
244
240
|
Assistant: [Reads `references/site-security-review.md` before choosing between passive network inspection, direct browser fetch calls, and Playwright-first automation]
|
|
245
241
|
Assistant: [Runs `npx libretto snapshot --session <session>`]
|
|
246
242
|
Assistant: [Uses `snapshot` and `exec` as needed to understand the site and decide the implementation path]
|
|
247
243
|
Assistant: [Reads `references/code-generation-rules.md` before writing production workflow code]
|
|
248
244
|
Assistant: I found the working path. I'll now create the workflow file and verify it.
|
|
249
245
|
Assistant: [Creates or edits `target-workflow.ts` following `references/code-generation-rules.md`]
|
|
250
|
-
Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` to
|
|
246
|
+
Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` on a fresh, unauthenticated session to verify both sign-in and workflow behavior]
|
|
247
|
+
Assistant: [If sign-in validation passes and a reusable session is useful, adds an auth profile and reruns validation]
|
|
251
248
|
Assistant: Validation passed. Here are the results:
|
|
252
249
|
[Shows the output/results from the validation run]
|
|
253
250
|
To run it again, use: npx libretto run ./target-workflow.ts --params '{"status":"open"}'
|
|
@@ -279,7 +276,7 @@ To run it again, use: npx libretto run ./integration.ts
|
|
|
279
276
|
- Read `references/configuration-file-reference.md` when you need to inspect or change `.libretto/config.json` for viewport or session defaults.
|
|
280
277
|
- Read `references/site-security-review.md` before reviewing the site's security posture and deciding whether to lead with network requests, passive interception, or Playwright DOM automation on a new site.
|
|
281
278
|
- Read `references/code-generation-rules.md` before writing or editing production workflow files.
|
|
282
|
-
- Read `references/
|
|
279
|
+
- Read `references/website-authentication.md` when website sign-in implementation or auth-profile behavior is relevant.
|
|
283
280
|
- Read `references/pages-and-page-targeting.md` when a session has multiple open pages or you need `--page`.
|
|
284
281
|
- Read `references/action-logs.md` for full action log field descriptions and user-vs-agent event semantics.
|
|
285
282
|
- If the workflow code is deployed to the Libretto Cloud platform and you need to reference its API docs, fetch [https://libretto.sh/docs/llms.txt](https://libretto.sh/docs/llms.txt) and follow the relevant page links.
|
|
@@ -49,7 +49,7 @@ Key points:
|
|
|
49
49
|
- After validation is complete and the workflow is confirmed working end to end, remove all `pause()` calls and pause-only workflow params unless the user explicitly says to keep them.
|
|
50
50
|
- The browser is launched and closed automatically by the CLI. Do not launch or close it in the handler.
|
|
51
51
|
|
|
52
|
-
## Workflow Credentials And
|
|
52
|
+
## Workflow Credentials And Authentication
|
|
53
53
|
|
|
54
54
|
Declare `credentials` for runtime secrets instead of putting them in the Zod input schema or `--params`. Only declared names are injected into `input.credentials`; local runs read matching `LIBRETTO_CLOUD_` variables from `.env`, and hosted runs read matching Libretto Cloud credentials. For example, `LIBRETTO_CLOUD_OPENAI_API_KEY` becomes `input.credentials.openai_api_key`.
|
|
55
55
|
|
|
@@ -70,14 +70,7 @@ export default workflow("sentimentWorkflow", {
|
|
|
70
70
|
});
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
For
|
|
74
|
-
|
|
75
|
-
- `authProfile: { name, refresh: true }`
|
|
76
|
-
- `librettoAuthenticate(...)` fallback login logic using declared credentials such as `credentials: ["username", "password"]`
|
|
77
|
-
|
|
78
|
-
After generating or validating a credentialed workflow, include the local `.env` variable names the user must set, and never print secret values.
|
|
79
|
-
|
|
80
|
-
The fallback should validate the signed-in state, log in when validation fails, and validate again before continuing. Follow `references/auth-profiles.md` for profile naming, refresh behavior, hosted profile behavior, and the fallback-login pattern.
|
|
73
|
+
For website workflows that require signing in, build working sign-in logic with `librettoAuthenticate` and verify it from a clean signed-out browser before adding an auth profile. Follow `references/website-authentication.md`.
|
|
81
74
|
|
|
82
75
|
## Workflow Error Handling
|
|
83
76
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Shipped Source & Documentation
|
|
2
|
+
|
|
3
|
+
The npm package includes `src/` (full TypeScript source) and `docs/` for deeper understanding of internals and design decisions. Resolve paths from the package root, such as `node_modules/libretto/`.
|
|
4
|
+
|
|
5
|
+
Full documentation is published at [libretto.sh](https://libretto.sh). Available pages:
|
|
6
|
+
|
|
7
|
+
- Get started: [quickstart](https://libretto.sh/docs/get-started/quickstart), [first workflow](https://libretto.sh/docs/get-started/first-workflow), [deploying](https://libretto.sh/docs/get-started/deploying)
|
|
8
|
+
- Fundamentals: [core concepts](https://libretto.sh/docs/understand-libretto/core-concepts), [how workflow generation works](https://libretto.sh/docs/understand-libretto/how-workflow-generation-works), [automation and bot detection](https://libretto.sh/docs/understand-libretto/automation-and-bot-detection), [website authentication](https://libretto.sh/docs/understand-libretto/website-authentication)
|
|
9
|
+
- Workflow guides: [one-shot generation](https://libretto.sh/docs/guides/one-shot-workflow-generation), [interactive building](https://libretto.sh/docs/guides/interactive-workflow-building), [debugging workflows](https://libretto.sh/docs/guides/debugging-workflows), [convert to network requests](https://libretto.sh/docs/guides/convert-to-network-requests)
|
|
10
|
+
- CLI reference: [open and connect](https://libretto.sh/docs/reference/cli/open-and-connect), [sessions](https://libretto.sh/docs/reference/cli/sessions), [profiles](https://libretto.sh/docs/reference/cli/profiles), [snapshot](https://libretto.sh/docs/reference/cli/snapshot), [exec](https://libretto.sh/docs/reference/cli/exec), [run and resume](https://libretto.sh/docs/reference/cli/run-and-resume), [session logs](https://libretto.sh/docs/reference/cli/session-logs), [pages](https://libretto.sh/docs/reference/cli/pages)
|
|
11
|
+
- Library API: [workflow](https://libretto.sh/docs/reference/runtime/workflow), [network requests](https://libretto.sh/docs/reference/runtime/network-requests), [file downloads](https://libretto.sh/docs/reference/runtime/file-downloads)
|
|
12
|
+
- Libretto Cloud Hosting: [overview](https://libretto.sh/docs/libretto-cloud-hosting/overview), [authentication](https://libretto.sh/docs/libretto-cloud-hosting/authentication), [deployments](https://libretto.sh/docs/libretto-cloud-hosting/deployments), [stealth](https://libretto.sh/docs/libretto-cloud-hosting/stealth)
|
|
13
|
+
- Alternative providers: [overview](https://libretto.sh/docs/alternative-providers/overview), [Kernel](https://libretto.sh/docs/alternative-providers/kernel), [Browserbase](https://libretto.sh/docs/alternative-providers/browserbase), [Steel](https://libretto.sh/docs/alternative-providers/steel), [GCP](https://libretto.sh/docs/alternative-providers/gcp), [AWS](https://libretto.sh/docs/alternative-providers/aws)
|
|
@@ -43,7 +43,7 @@ General guidance: determine whether the site has bot protection and roughly how
|
|
|
43
43
|
|
|
44
44
|
### Probe 2: Fetch and XHR Interception
|
|
45
45
|
|
|
46
|
-
Check whether the site has monkey-patched `window.fetch` or `XMLHttpRequest`.
|
|
46
|
+
Check whether the site has monkey-patched `window.fetch` or `XMLHttpRequest`. Patching is a caution signal, not an automatic blocker. Browser-context network requests are usually fine when the target endpoint is already called by the site with fetch/XHR and there is no strong bot-protection evidence.
|
|
47
47
|
|
|
48
48
|
```js
|
|
49
49
|
window.fetch.toString()
|
|
@@ -52,7 +52,7 @@ Object.getOwnPropertyDescriptor(window, 'fetch')
|
|
|
52
52
|
window.fetch.hasOwnProperty('prototype')
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Important:
|
|
55
|
+
Important: ordinary app instrumentation and Libretto's own page-stability tracking can also wrap fetch/XHR. Treat browser fetch as risky only when the wrapper appears security-related, obfuscated, tied to bot-protection scripts, or likely to inspect call stacks. Some `Proxy` wrappers still stringify as `"[native code]"`, so these checks are only heuristics.
|
|
56
56
|
|
|
57
57
|
## Choosing a Data Capture Strategy
|
|
58
58
|
|
|
@@ -66,7 +66,7 @@ When to prioritize this:
|
|
|
66
66
|
|
|
67
67
|
- The target endpoint is normally called by the site with fetch/XHR
|
|
68
68
|
- No enterprise bot protection is detected
|
|
69
|
-
-
|
|
69
|
+
- No security-related fetch/XHR interception is detected, or the observed wrapper appears to be ordinary app instrumentation
|
|
70
70
|
- The API responses are parseable and useful
|
|
71
71
|
- You need data that requires many API calls (deep pagination, bulk queries) where driving the UI would be slow
|
|
72
72
|
|
|
@@ -83,7 +83,7 @@ Listen to network responses that the browser naturally makes as you navigate. Yo
|
|
|
83
83
|
When to prioritize this:
|
|
84
84
|
|
|
85
85
|
- Enterprise bot protection is detected
|
|
86
|
-
-
|
|
86
|
+
- Security-related fetch/XHR interception is detected
|
|
87
87
|
- The site's normal UI flow triggers API calls that return the data you need
|
|
88
88
|
- You want to minimize detection risk as much as possible
|
|
89
89
|
|
|
@@ -112,8 +112,9 @@ Trade-off: it is slower, more fragile against DOM changes, and you only get data
|
|
|
112
112
|
|
|
113
113
|
| Site Profile | Primary Strategy | Supplement With |
|
|
114
114
|
| --- | --- | --- |
|
|
115
|
-
| No bot protection, fetch/XHR endpoint,
|
|
116
|
-
| No bot protection,
|
|
115
|
+
| No bot protection, fetch/XHR endpoint, no security-related interception | A (`page.evaluate(fetch)`) | Playwright for navigation/auth |
|
|
116
|
+
| No bot protection, harmless app instrumentation, fetch/XHR endpoint | A (`page.evaluate(fetch)`) | B (`page.on('response', ...)`) if requests fail |
|
|
117
|
+
| No bot protection, security-related interception or endpoint is not fetch/XHR | B (`page.on('response', ...)`) | Playwright for navigation; DOM extraction as fallback |
|
|
117
118
|
| Bot protection detected | B (`page.on('response', ...)`) | Playwright for navigation; cautious use of `page.evaluate(fetch)` only if needed |
|
|
118
119
|
| Server-rendered content (no API calls) | C (DOM extraction) | Playwright for all interaction |
|
|
119
120
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Website Authentication
|
|
2
|
+
|
|
3
|
+
Use this reference when a workflow needs a logged-in website session. The Working Rules in `../SKILL.md` define the required auth workflow; this file explains how to implement sign-in logic with `librettoAuthenticate`, and how auth profiles save signed-in state for later runs.
|
|
4
|
+
|
|
5
|
+
Build and verify working sign-in logic first. The sign-in code takes priority; an auth profile is added only after the sign-in logic is verified, never as a substitute for it.
|
|
6
|
+
|
|
7
|
+
## Sign-In Logic
|
|
8
|
+
|
|
9
|
+
Use `librettoAuthenticate` so the workflow can sign in from a fresh browser. Declare each required secret in the workflow credentials array and use those credentials inside `signIn`.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { librettoAuthenticate, workflow } from "libretto";
|
|
13
|
+
|
|
14
|
+
export default workflow("accountWorkflow", {
|
|
15
|
+
credentials: ["portal_username", "portal_password"],
|
|
16
|
+
async handler(ctx, input) {
|
|
17
|
+
const { page } = ctx;
|
|
18
|
+
|
|
19
|
+
await page.goto("https://app.example.com/dashboard");
|
|
20
|
+
|
|
21
|
+
// Sign in when the session is not already authenticated.
|
|
22
|
+
await librettoAuthenticate(ctx, {
|
|
23
|
+
credentials: input.credentials,
|
|
24
|
+
isSignedIn: async () =>
|
|
25
|
+
await page
|
|
26
|
+
.getByRole("heading", { name: "Dashboard" })
|
|
27
|
+
.isVisible()
|
|
28
|
+
.catch(() => false),
|
|
29
|
+
signIn: async (_ctx, credentials) => {
|
|
30
|
+
await page.goto("https://app.example.com/login");
|
|
31
|
+
await page.getByLabel("Email").fill(credentials.portal_username);
|
|
32
|
+
await page.getByLabel("Password").fill(credentials.portal_password);
|
|
33
|
+
await page.getByRole("button", { name: "Sign in" }).click();
|
|
34
|
+
await page.getByRole("heading", { name: "Dashboard" }).waitFor();
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Continue with the signed-in workflow steps.
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Auth Profiles
|
|
44
|
+
|
|
45
|
+
Auth profiles save the signed-in browser state (cookies, localStorage, IndexedDB) so later runs can reuse a logged-in session instead of signing in from scratch. The sign-in logic still takes priority: do not add a profile until the `librettoAuthenticate` sign-in step has been verified from a signed-out browser with no profile present. If you add a profile first, validation passes on the saved session while the untested sign-in logic fails the first time that session expires.
|
|
46
|
+
|
|
47
|
+
A profile only holds whatever a signed-in session wrote into it, so it does nothing until a run has signed in at least once. With `refresh: true`, a successful run writes updated browser state back to the profile, so a fresh sign-in repairs an expired one. Local runs load `.libretto/profiles/<name>.json`; hosted runs use the provider-native profile with the same name.
|
|
48
|
+
|
|
49
|
+
Add the profile to the workflow you already verified:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
export default workflow("accountWorkflow", {
|
|
53
|
+
// Added only after the signIn step above is verified standalone.
|
|
54
|
+
authProfile: { name: "example-account", refresh: true },
|
|
55
|
+
credentials: ["portal_username", "portal_password"],
|
|
56
|
+
// ...same handler and librettoAuthenticate call as above.
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Save the current signed-in session as a named, site-scoped profile.
|
|
64
|
+
npx libretto save example-app --session login --sites app.example.com,auth.example.com
|
|
65
|
+
|
|
66
|
+
# List or delete hosted auth profile names.
|
|
67
|
+
npx libretto cloud profiles list
|
|
68
|
+
npx libretto cloud profiles delete example-app
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`save` captures cookies, localStorage, and IndexedDB only for the comma-separated `--sites` list.
|
|
72
|
+
|
|
73
|
+
To reuse an existing signed-in Chrome profile instead of signing in, use `npx libretto import-chrome-profiles`. Get the user's consent first, since attaching can close or relaunch their Chrome window.
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
connect,
|
|
8
8
|
disconnectBrowser,
|
|
9
9
|
runClose,
|
|
10
|
+
resolveWindowPosition,
|
|
10
11
|
resolveViewport,
|
|
11
12
|
} from "../core/browser.js";
|
|
12
13
|
import { parseViewportArg } from "./browser.js";
|
|
@@ -22,7 +23,10 @@ import {
|
|
|
22
23
|
type SessionState,
|
|
23
24
|
} from "../core/session.js";
|
|
24
25
|
import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
readLibrettoConfig,
|
|
28
|
+
type WindowPositionConfig,
|
|
29
|
+
} from "../core/config.js";
|
|
26
30
|
import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
|
|
27
31
|
import {
|
|
28
32
|
getProviderStartupTimeoutMs,
|
|
@@ -41,6 +45,7 @@ import {
|
|
|
41
45
|
type DaemonExecResult,
|
|
42
46
|
type DaemonToCliApi,
|
|
43
47
|
} from "../core/daemon/ipc.js";
|
|
48
|
+
import type { DaemonConfig } from "../core/daemon/config.js";
|
|
44
49
|
import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
|
|
45
50
|
import {
|
|
46
51
|
readActionLog,
|
|
@@ -66,6 +71,7 @@ type RunIntegrationCommandRequest = {
|
|
|
66
71
|
headless: boolean;
|
|
67
72
|
visualize: boolean;
|
|
68
73
|
viewport?: { width: number; height: number };
|
|
74
|
+
windowPosition?: WindowPositionConfig;
|
|
69
75
|
accessMode: SessionAccessMode;
|
|
70
76
|
providerName?: string;
|
|
71
77
|
stayOpenOnSuccess: boolean;
|
|
@@ -76,6 +82,29 @@ type ExecMode = "exec" | "readonly-exec";
|
|
|
76
82
|
|
|
77
83
|
const require = moduleBuiltin.createRequire(import.meta.url);
|
|
78
84
|
|
|
85
|
+
export function createRunBrowserConfig(args: {
|
|
86
|
+
providerName?: string;
|
|
87
|
+
headless: boolean;
|
|
88
|
+
viewport?: { width: number; height: number };
|
|
89
|
+
windowPosition?: WindowPositionConfig;
|
|
90
|
+
}): DaemonConfig["browser"] {
|
|
91
|
+
if (args.providerName) {
|
|
92
|
+
return {
|
|
93
|
+
kind: "provider",
|
|
94
|
+
providerName: args.providerName,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
kind: "launch",
|
|
100
|
+
headed: !args.headless,
|
|
101
|
+
viewport: args.viewport ?? { width: 1366, height: 768 },
|
|
102
|
+
...(!args.headless && args.windowPosition
|
|
103
|
+
? { windowPosition: args.windowPosition }
|
|
104
|
+
: {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
79
108
|
function writeDaemonExecOutput(output?: { stdout: string; stderr: string }) {
|
|
80
109
|
if (output?.stdout) {
|
|
81
110
|
process.stdout.write(output.stdout);
|
|
@@ -575,16 +604,7 @@ async function runIntegrationFromFile(
|
|
|
575
604
|
config: {
|
|
576
605
|
session: args.session,
|
|
577
606
|
experiments: args.experiments,
|
|
578
|
-
browser: args
|
|
579
|
-
? {
|
|
580
|
-
kind: "provider",
|
|
581
|
-
providerName: args.providerName,
|
|
582
|
-
}
|
|
583
|
-
: {
|
|
584
|
-
kind: "launch",
|
|
585
|
-
headed: !args.headless,
|
|
586
|
-
viewport: args.viewport ?? { width: 1366, height: 768 },
|
|
587
|
-
},
|
|
607
|
+
browser: createRunBrowserConfig(args),
|
|
588
608
|
workflow: {
|
|
589
609
|
integrationPath: absoluteIntegrationPath,
|
|
590
610
|
params: args.params,
|
|
@@ -875,15 +895,21 @@ export const runCommand = SimpleCLI.command({
|
|
|
875
895
|
console.log(`Connecting to ${providerName} browser...`);
|
|
876
896
|
}
|
|
877
897
|
|
|
898
|
+
const headless = daemonProviderName ? true : (headlessMode ?? false);
|
|
899
|
+
const windowPosition = headless
|
|
900
|
+
? undefined
|
|
901
|
+
: resolveWindowPosition(ctx.logger);
|
|
902
|
+
|
|
878
903
|
await runIntegrationFromFile(
|
|
879
904
|
{
|
|
880
905
|
integrationPath: input.integrationFile!,
|
|
881
906
|
session: ctx.session,
|
|
882
907
|
params,
|
|
883
908
|
tsconfigPath: input.tsconfig,
|
|
884
|
-
headless
|
|
909
|
+
headless,
|
|
885
910
|
visualize,
|
|
886
911
|
viewport,
|
|
912
|
+
windowPosition,
|
|
887
913
|
accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : (readLibrettoConfig().sessionMode ?? "write-access"),
|
|
888
914
|
providerName: daemonProviderName,
|
|
889
915
|
stayOpenOnSuccess: input.stayOpenOnSuccess,
|
package/src/cli/core/browser.ts
CHANGED
|
@@ -388,7 +388,7 @@ export function resolveViewport(
|
|
|
388
388
|
return DEFAULT_VIEWPORT;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
-
function resolveWindowPosition(
|
|
391
|
+
export function resolveWindowPosition(
|
|
392
392
|
logger: LoggerApi,
|
|
393
393
|
): { x: number; y: number } | undefined {
|
|
394
394
|
const config = readLibrettoConfig();
|
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
import { WorkflowController } from "../workflow-runner/runner.js";
|
|
92
92
|
import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
|
|
93
93
|
import { captureAuthProfileStorageState } from "../../../shared/workflow/auth-profile-state.js";
|
|
94
|
+
import { applyWindowPosition } from "../../../shared/run/window-position.js";
|
|
94
95
|
|
|
95
96
|
function isOperationalPage(page: Page): boolean {
|
|
96
97
|
const url = page.url();
|
|
@@ -397,6 +398,7 @@ class BrowserDaemon {
|
|
|
397
398
|
});
|
|
398
399
|
|
|
399
400
|
const page = await context.newPage();
|
|
401
|
+
await applyWindowPosition(browser, context, page, config.windowPosition);
|
|
400
402
|
page.setDefaultTimeout(30000);
|
|
401
403
|
page.setDefaultNavigationTimeout(45000);
|
|
402
404
|
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
SessionStateFileSchema,
|
|
14
14
|
} from "../state/session-state.js";
|
|
15
15
|
import { readLibrettoConfig } from "../../cli/core/config.js";
|
|
16
|
+
import {
|
|
17
|
+
applyWindowPosition,
|
|
18
|
+
type WindowPosition,
|
|
19
|
+
} from "./window-position.js";
|
|
16
20
|
|
|
17
21
|
async function pickFreePort(): Promise<number> {
|
|
18
22
|
return await new Promise((resolve, reject) => {
|
|
@@ -49,53 +53,10 @@ export type BrowserSession = {
|
|
|
49
53
|
close: () => Promise<void>;
|
|
50
54
|
};
|
|
51
55
|
|
|
52
|
-
function resolveWindowPosition():
|
|
56
|
+
function resolveWindowPosition(): WindowPosition | undefined {
|
|
53
57
|
return readLibrettoConfig().windowPosition;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
async function applyWindowPosition(
|
|
57
|
-
browser: Browser,
|
|
58
|
-
context: BrowserContext,
|
|
59
|
-
page: Page,
|
|
60
|
-
windowPosition: { x: number; y: number } | undefined,
|
|
61
|
-
): Promise<void> {
|
|
62
|
-
if (!windowPosition) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const requestedBounds = {
|
|
67
|
-
left: windowPosition.x,
|
|
68
|
-
top: windowPosition.y,
|
|
69
|
-
windowState: "normal" as const,
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const pageCdp = await context.newCDPSession(page);
|
|
73
|
-
let browserCdp:
|
|
74
|
-
| Awaited<ReturnType<Browser["newBrowserCDPSession"]>>
|
|
75
|
-
| undefined;
|
|
76
|
-
try {
|
|
77
|
-
const targetInfo = await pageCdp.send("Target.getTargetInfo");
|
|
78
|
-
const targetId = (
|
|
79
|
-
targetInfo as { targetInfo?: { targetId?: string } }
|
|
80
|
-
).targetInfo?.targetId;
|
|
81
|
-
browserCdp = await browser.newBrowserCDPSession();
|
|
82
|
-
const windowResult = await browserCdp.send(
|
|
83
|
-
"Browser.getWindowForTarget",
|
|
84
|
-
targetId ? { targetId } : {},
|
|
85
|
-
);
|
|
86
|
-
await browserCdp.send("Browser.setWindowBounds", {
|
|
87
|
-
windowId: windowResult.windowId,
|
|
88
|
-
bounds: requestedBounds,
|
|
89
|
-
});
|
|
90
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
91
|
-
} catch {
|
|
92
|
-
// Best-effort: window positioning should not prevent browser launch.
|
|
93
|
-
} finally {
|
|
94
|
-
await pageCdp.detach().catch(() => {});
|
|
95
|
-
await browserCdp?.detach().catch(() => {});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
60
|
export async function launchBrowser({
|
|
100
61
|
sessionName,
|
|
101
62
|
headless = false,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Browser, BrowserContext, Page } from "playwright";
|
|
2
|
+
|
|
3
|
+
export type WindowPosition = { x: number; y: number };
|
|
4
|
+
|
|
5
|
+
export async function applyWindowPosition(
|
|
6
|
+
browser: Browser,
|
|
7
|
+
context: BrowserContext,
|
|
8
|
+
page: Page,
|
|
9
|
+
windowPosition: WindowPosition | undefined,
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
if (!windowPosition) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const requestedBounds = {
|
|
16
|
+
left: windowPosition.x,
|
|
17
|
+
top: windowPosition.y,
|
|
18
|
+
windowState: "normal" as const,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let pageCdp:
|
|
22
|
+
| Awaited<ReturnType<BrowserContext["newCDPSession"]>>
|
|
23
|
+
| undefined;
|
|
24
|
+
let browserCdp:
|
|
25
|
+
| Awaited<ReturnType<Browser["newBrowserCDPSession"]>>
|
|
26
|
+
| undefined;
|
|
27
|
+
try {
|
|
28
|
+
pageCdp = await context.newCDPSession(page);
|
|
29
|
+
const targetInfo = await pageCdp.send("Target.getTargetInfo");
|
|
30
|
+
const targetId = (
|
|
31
|
+
targetInfo as { targetInfo?: { targetId?: string } }
|
|
32
|
+
).targetInfo?.targetId;
|
|
33
|
+
browserCdp = await browser.newBrowserCDPSession();
|
|
34
|
+
const windowResult = await browserCdp.send(
|
|
35
|
+
"Browser.getWindowForTarget",
|
|
36
|
+
targetId ? { targetId } : {},
|
|
37
|
+
);
|
|
38
|
+
await browserCdp.send("Browser.setWindowBounds", {
|
|
39
|
+
windowId: windowResult.windowId,
|
|
40
|
+
bounds: requestedBounds,
|
|
41
|
+
});
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
43
|
+
} catch {
|
|
44
|
+
// Best-effort: window positioning should not prevent browser launch.
|
|
45
|
+
} finally {
|
|
46
|
+
await pageCdp?.detach().catch(() => {});
|
|
47
|
+
await browserCdp?.detach().catch(() => {});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { LibrettoWorkflowContext } from "./workflow.js";
|
|
2
2
|
|
|
3
3
|
export type LibrettoAuthenticateOptions = {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
isSignedIn: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
|
|
5
|
+
signIn: (
|
|
6
6
|
ctx: LibrettoWorkflowContext,
|
|
7
7
|
credentials: Record<string, string>,
|
|
8
8
|
) => Promise<void> | void;
|
|
@@ -14,17 +14,17 @@ export async function librettoAuthenticate(
|
|
|
14
14
|
ctx: LibrettoWorkflowContext,
|
|
15
15
|
options: LibrettoAuthenticateOptions,
|
|
16
16
|
): Promise<{ usedProfile: boolean }> {
|
|
17
|
-
if (await options.
|
|
17
|
+
if (await options.isSignedIn(ctx)) {
|
|
18
18
|
return { usedProfile: true };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const credentials = normalizeCredentials(
|
|
22
22
|
options.credentials ?? readCredentialsFromEnv(options.envPrefix),
|
|
23
23
|
);
|
|
24
|
-
await options.
|
|
24
|
+
await options.signIn(ctx, credentials);
|
|
25
25
|
|
|
26
|
-
if (!(await options.
|
|
27
|
-
throw new Error("
|
|
26
|
+
if (!(await options.isSignedIn(ctx))) {
|
|
27
|
+
throw new Error("Sign-in completed, but the session is still not signed in.");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
return { usedProfile: false };
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# Auth Profiles
|
|
2
|
-
|
|
3
|
-
Use this reference when generating or maintaining workflows that need a logged-in website session.
|
|
4
|
-
|
|
5
|
-
## When to Use This
|
|
6
|
-
|
|
7
|
-
- The user wants to persist authentication across runs.
|
|
8
|
-
- The workflow should recover when saved login state is stale.
|
|
9
|
-
|
|
10
|
-
## Workflow
|
|
11
|
-
|
|
12
|
-
- Open the site in headed mode.
|
|
13
|
-
- Ask the user to log in manually.
|
|
14
|
-
- Save the current session as a named, site-scoped profile.
|
|
15
|
-
- Run a workflow that declares the profile and includes fallback login logic.
|
|
16
|
-
|
|
17
|
-
## Commands
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
# Save scoped auth state from the current Libretto session.
|
|
21
|
-
npx libretto save example-app --session login --sites app.example.com,auth.example.com
|
|
22
|
-
|
|
23
|
-
# List or delete hosted auth profile names.
|
|
24
|
-
npx libretto cloud profiles list
|
|
25
|
-
npx libretto cloud profiles delete example-app
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Workflow Definition
|
|
29
|
-
|
|
30
|
-
Use `authProfile` to reuse a named login profile: local runs load
|
|
31
|
-
`.libretto/profiles/<name>.json`, while hosted runs use provider-native profiles
|
|
32
|
-
that `libretto cloud deploy` registers by name without uploading local files.
|
|
33
|
-
Use `{ name, refresh: true }` when successful runs should persist updated
|
|
34
|
-
browser state back to the profile. Pair profile use with `librettoAuthenticate`
|
|
35
|
-
so stale local or hosted sessions can fall back to login with declared
|
|
36
|
-
credentials before the workflow continues.
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
import { librettoAuthenticate, workflow } from "libretto";
|
|
40
|
-
|
|
41
|
-
export default workflow("accountWorkflow", {
|
|
42
|
-
authProfile: {
|
|
43
|
-
name: "example-account",
|
|
44
|
-
refresh: true,
|
|
45
|
-
},
|
|
46
|
-
credentials: ["username", "password"],
|
|
47
|
-
async handler(ctx, input) {
|
|
48
|
-
const { page } = ctx;
|
|
49
|
-
|
|
50
|
-
await page.goto("https://app.example.com/dashboard");
|
|
51
|
-
|
|
52
|
-
await librettoAuthenticate(ctx, {
|
|
53
|
-
credentials: input.credentials,
|
|
54
|
-
validate: async ({ page }) =>
|
|
55
|
-
await page.getByRole("heading", { name: "Dashboard" })
|
|
56
|
-
.isVisible()
|
|
57
|
-
.catch(() => false),
|
|
58
|
-
fallback: async ({ page }, credentials) => {
|
|
59
|
-
await page.goto("https://app.example.com/login");
|
|
60
|
-
await page.getByLabel("Email").fill(credentials.username);
|
|
61
|
-
await page.getByLabel("Password").fill(credentials.password);
|
|
62
|
-
await page.getByRole("button", { name: "Sign in" }).click();
|
|
63
|
-
await page.getByRole("heading", { name: "Dashboard" }).waitFor();
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Continue with the signed-in workflow steps.
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## Notes
|
|
73
|
-
|
|
74
|
-
- Saving a profile captures cookies, localStorage, and IndexedDB only for the comma-separated `--sites` list.
|
|
75
|
-
- If the user explicitly wants to import from Chrome, ask which Chrome/profile
|
|
76
|
-
to launch or attach to and get consent before attaching because disconnecting
|
|
77
|
-
can close or relaunch that Chrome window. Chrome may require copying the
|
|
78
|
-
selected profile to a temporary user-data directory before running
|
|
79
|
-
`npx libretto import-chrome-profiles example-app --cdp-url http://127.0.0.1:9222 --sites app.example.com`.
|