libretto 0.6.28 → 0.6.30-experimental-runtime-libretto.0
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/cli/core/deploy-artifact.js +130 -10
- 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 +1 -1
- package/package.json +19 -18
- package/skills/libretto/SKILL.md +10 -4
- package/skills/libretto/references/code-generation-rules.md +2 -9
- 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/cli/core/deploy-artifact.ts +145 -10
- 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
|
@@ -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
|
|
|
@@ -71,7 +71,6 @@ type BuildHostedDeployTarballArgs = {
|
|
|
71
71
|
type CreateHostedDeployPackageArgs = BuildHostedDeployTarballArgs;
|
|
72
72
|
|
|
73
73
|
const DEFAULT_RUNTIME_EXTERNALS = [
|
|
74
|
-
"libretto",
|
|
75
74
|
"playwright",
|
|
76
75
|
"playwright-core",
|
|
77
76
|
"chromium-bidi",
|
|
@@ -580,22 +579,79 @@ function resolveDependencyVersion(
|
|
|
580
579
|
);
|
|
581
580
|
}
|
|
582
581
|
|
|
582
|
+
function resolveLibrettoDependencyVersion(
|
|
583
|
+
sourceDir: string,
|
|
584
|
+
packageName: string,
|
|
585
|
+
): string | null {
|
|
586
|
+
try {
|
|
587
|
+
const librettoManifestPath = require.resolve("libretto/package.json", {
|
|
588
|
+
paths: [sourceDir],
|
|
589
|
+
});
|
|
590
|
+
const version = readDependencyVersionFromManifest(
|
|
591
|
+
readPackageManifest(librettoManifestPath),
|
|
592
|
+
packageName,
|
|
593
|
+
);
|
|
594
|
+
if (version) {
|
|
595
|
+
return version;
|
|
596
|
+
}
|
|
597
|
+
} catch {
|
|
598
|
+
// Fall through to the current CLI install. Source installs are optional in
|
|
599
|
+
// tests that only exercise manifest construction.
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const version = readDependencyVersionFromManifest(
|
|
603
|
+
readPackageManifest(join(CURRENT_LIBRETTO_PACKAGE_DIR, "package.json")),
|
|
604
|
+
packageName,
|
|
605
|
+
);
|
|
606
|
+
if (version) {
|
|
607
|
+
return version;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
const manifestPath = require.resolve(`${packageName}/package.json`, {
|
|
612
|
+
paths: [CURRENT_LIBRETTO_PACKAGE_DIR],
|
|
613
|
+
});
|
|
614
|
+
return readPackageManifest(manifestPath).version ?? null;
|
|
615
|
+
} catch {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
583
620
|
function writeDeployManifest(args: {
|
|
584
621
|
additionalExternals: readonly string[];
|
|
585
622
|
deploymentName: string;
|
|
586
623
|
librettoDependency: string;
|
|
587
624
|
outputDir: string;
|
|
625
|
+
runtimeExternals: readonly string[];
|
|
588
626
|
sourceDir: string;
|
|
589
627
|
}): void {
|
|
628
|
+
const dependencyPackages = [
|
|
629
|
+
...new Set([
|
|
630
|
+
...BUILT_IN_MANIFEST_DEPENDENCIES,
|
|
631
|
+
...args.runtimeExternals.filter((packageName) =>
|
|
632
|
+
resolveLibrettoDependencyVersion(args.sourceDir, packageName),
|
|
633
|
+
),
|
|
634
|
+
...args.additionalExternals,
|
|
635
|
+
]),
|
|
636
|
+
];
|
|
590
637
|
const dependencies = Object.fromEntries(
|
|
591
|
-
|
|
592
|
-
|
|
638
|
+
dependencyPackages.map((packageName) => {
|
|
639
|
+
const fallbackVersion =
|
|
640
|
+
packageName === "libretto"
|
|
641
|
+
? args.librettoDependency
|
|
642
|
+
: (resolveLibrettoDependencyVersion(args.sourceDir, packageName) ??
|
|
643
|
+
undefined);
|
|
644
|
+
return [
|
|
593
645
|
packageName,
|
|
594
646
|
packageName === "libretto"
|
|
595
647
|
? args.librettoDependency
|
|
596
|
-
: resolveDependencyVersion(
|
|
597
|
-
|
|
598
|
-
|
|
648
|
+
: resolveDependencyVersion(
|
|
649
|
+
args.sourceDir,
|
|
650
|
+
packageName,
|
|
651
|
+
fallbackVersion,
|
|
652
|
+
),
|
|
653
|
+
];
|
|
654
|
+
}),
|
|
599
655
|
);
|
|
600
656
|
|
|
601
657
|
writeFileSync(
|
|
@@ -930,7 +986,7 @@ function createBootstrapSource(args: {
|
|
|
930
986
|
// to discover workflow exports. The implementation bundle stays embedded in
|
|
931
987
|
// the file, while external packages are resolved from node_modules when the
|
|
932
988
|
// deployed code loads them.
|
|
933
|
-
return `import { createRequire } from "node:module";
|
|
989
|
+
return `import { createRequire, Module } from "node:module";
|
|
934
990
|
import { existsSync, writeFileSync } from "node:fs";
|
|
935
991
|
import { tmpdir } from "node:os";
|
|
936
992
|
import { join } from "node:path";
|
|
@@ -946,6 +1002,45 @@ const BUNDLE_FILENAME = join(
|
|
|
946
1002
|
const nativeRequire = createRequire(
|
|
947
1003
|
join(tmpdir(), ${JSON.stringify("libretto-deploy-bootstrap.cjs")}),
|
|
948
1004
|
);
|
|
1005
|
+
const packageRequire = createRequire(import.meta.url || __filename);
|
|
1006
|
+
|
|
1007
|
+
function isBarePackageSpecifier(specifier) {
|
|
1008
|
+
return (
|
|
1009
|
+
!specifier.startsWith(".") &&
|
|
1010
|
+
!specifier.startsWith("/") &&
|
|
1011
|
+
!specifier.startsWith("node:") &&
|
|
1012
|
+
!/^[A-Za-z]:[\\\\/]/.test(specifier)
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function loadImplementation() {
|
|
1017
|
+
const originalRequire = Module.prototype.require;
|
|
1018
|
+
function patchedRequire(specifier) {
|
|
1019
|
+
try {
|
|
1020
|
+
return originalRequire.call(this, specifier);
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
if (
|
|
1023
|
+
error?.code === "MODULE_NOT_FOUND" &&
|
|
1024
|
+
isBarePackageSpecifier(specifier)
|
|
1025
|
+
) {
|
|
1026
|
+
Module.prototype.require = originalRequire;
|
|
1027
|
+
try {
|
|
1028
|
+
return packageRequire(specifier);
|
|
1029
|
+
} finally {
|
|
1030
|
+
Module.prototype.require = patchedRequire;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
throw error;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
Module.prototype.require = patchedRequire;
|
|
1037
|
+
|
|
1038
|
+
try {
|
|
1039
|
+
return nativeRequire(ensureBundleFile());
|
|
1040
|
+
} finally {
|
|
1041
|
+
Module.prototype.require = originalRequire;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
949
1044
|
|
|
950
1045
|
function ensureBundleFile() {
|
|
951
1046
|
if (!existsSync(BUNDLE_FILENAME)) {
|
|
@@ -960,7 +1055,7 @@ function ensureBundleFile() {
|
|
|
960
1055
|
|
|
961
1056
|
function createWorkflowProxy(workflowName, metadata) {
|
|
962
1057
|
const handler = async (ctx, input) => {
|
|
963
|
-
const impl =
|
|
1058
|
+
const impl = loadImplementation();
|
|
964
1059
|
const target = getWorkflowFromModuleExports(impl, workflowName);
|
|
965
1060
|
if (!target || typeof target.run !== "function") {
|
|
966
1061
|
throw new Error(
|
|
@@ -1008,6 +1103,11 @@ async function writeBundledDeployEntrypoint(args: {
|
|
|
1008
1103
|
}> {
|
|
1009
1104
|
try {
|
|
1010
1105
|
const unresolvedWorkspaceImports = new Map<string, string>();
|
|
1106
|
+
const discoveryExternalPackages = new Set<string>([
|
|
1107
|
+
...args.externalPackages,
|
|
1108
|
+
"libretto",
|
|
1109
|
+
]);
|
|
1110
|
+
const unresolvedDiscoveryWorkspaceImports = new Map<string, string>();
|
|
1011
1111
|
// The implementation bundle is CommonJS so the bootstrap can load it lazily
|
|
1012
1112
|
// with createRequire() after workflow discovery, while external packages
|
|
1013
1113
|
// continue to load through normal Node module resolution.
|
|
@@ -1031,6 +1131,27 @@ async function writeBundledDeployEntrypoint(args: {
|
|
|
1031
1131
|
target: "node20",
|
|
1032
1132
|
write: false,
|
|
1033
1133
|
});
|
|
1134
|
+
// Discovery intentionally keeps libretto external so the local discovery
|
|
1135
|
+
// shim can observe workflow(...) calls without executing the full library.
|
|
1136
|
+
const discoveryBuild = await build({
|
|
1137
|
+
absWorkingDir: args.absSourceDir,
|
|
1138
|
+
bundle: true,
|
|
1139
|
+
entryPoints: [args.absEntryPoint],
|
|
1140
|
+
external: [...discoveryExternalPackages],
|
|
1141
|
+
format: "cjs",
|
|
1142
|
+
outfile: "prebundled-discovery.cjs",
|
|
1143
|
+
platform: "node",
|
|
1144
|
+
plugins: [
|
|
1145
|
+
workspaceSourcePlugin(
|
|
1146
|
+
args.workspacePackages,
|
|
1147
|
+
discoveryExternalPackages,
|
|
1148
|
+
unresolvedDiscoveryWorkspaceImports,
|
|
1149
|
+
),
|
|
1150
|
+
],
|
|
1151
|
+
splitting: false,
|
|
1152
|
+
target: "node20",
|
|
1153
|
+
write: false,
|
|
1154
|
+
});
|
|
1034
1155
|
const [unresolvedImport] = unresolvedWorkspaceImports;
|
|
1035
1156
|
if (unresolvedImport) {
|
|
1036
1157
|
const [importPath, packageDir] = unresolvedImport;
|
|
@@ -1038,6 +1159,13 @@ async function writeBundledDeployEntrypoint(args: {
|
|
|
1038
1159
|
`Unable to resolve workspace import "${importPath}" from ${packageDir}.`,
|
|
1039
1160
|
);
|
|
1040
1161
|
}
|
|
1162
|
+
const [unresolvedDiscoveryImport] = unresolvedDiscoveryWorkspaceImports;
|
|
1163
|
+
if (unresolvedDiscoveryImport) {
|
|
1164
|
+
const [importPath, packageDir] = unresolvedDiscoveryImport;
|
|
1165
|
+
throw new Error(
|
|
1166
|
+
`Unable to resolve workspace import "${importPath}" from ${packageDir}.`,
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1041
1169
|
|
|
1042
1170
|
const bundledImplementation = implementationBuild.outputFiles?.find(
|
|
1043
1171
|
(file) => file.path.endsWith("prebundled.cjs"),
|
|
@@ -1047,12 +1175,18 @@ async function writeBundledDeployEntrypoint(args: {
|
|
|
1047
1175
|
"Bundler did not produce a deployment implementation file.",
|
|
1048
1176
|
);
|
|
1049
1177
|
}
|
|
1178
|
+
const bundledDiscovery = discoveryBuild.outputFiles?.find((file) =>
|
|
1179
|
+
file.path.endsWith("prebundled-discovery.cjs"),
|
|
1180
|
+
);
|
|
1181
|
+
if (!bundledDiscovery) {
|
|
1182
|
+
throw new Error("Bundler did not produce a deployment discovery file.");
|
|
1183
|
+
}
|
|
1050
1184
|
|
|
1051
1185
|
const workflows = discoverBundledWorkflows({
|
|
1052
1186
|
absEntryPoint: args.absEntryPoint,
|
|
1053
1187
|
absSourceDir: args.absSourceDir,
|
|
1054
|
-
bundleBuffer: Buffer.from(
|
|
1055
|
-
externalPackages:
|
|
1188
|
+
bundleBuffer: Buffer.from(bundledDiscovery.contents),
|
|
1189
|
+
externalPackages: discoveryExternalPackages,
|
|
1056
1190
|
});
|
|
1057
1191
|
|
|
1058
1192
|
writeFileSync(
|
|
@@ -1145,6 +1279,7 @@ export async function createHostedDeployPackage(
|
|
|
1145
1279
|
deploymentName: args.deploymentName,
|
|
1146
1280
|
librettoDependency,
|
|
1147
1281
|
outputDir,
|
|
1282
|
+
runtimeExternals: [...DEFAULT_RUNTIME_EXTERNALS],
|
|
1148
1283
|
sourceDir: absSourceDir,
|
|
1149
1284
|
});
|
|
1150
1285
|
writeDeployMetadata({ outputDir, workflows: workflowsWithShareableSource });
|
|
@@ -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`.
|