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.
@@ -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.
@@ -4,7 +4,7 @@ description: "Read-only Libretto workflow for diagnosing live browser state with
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.28"
7
+ version: "0.6.29"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works
@@ -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 { readLibrettoConfig } from "../core/config.js";
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.providerName
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: daemonProviderName ? true : (headlessMode ?? false),
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,
@@ -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
- [...BUILT_IN_MANIFEST_DEPENDENCIES, ...args.additionalExternals].map(
592
- (packageName) => [
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(args.sourceDir, packageName),
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 = nativeRequire(ensureBundleFile());
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(bundledImplementation.contents),
1055
- externalPackages: args.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(): { x: number; y: number } | undefined {
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
- validate: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
5
- fallback: (
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.validate(ctx)) {
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.fallback(ctx, credentials);
24
+ await options.signIn(ctx, credentials);
25
25
 
26
- if (!(await options.validate(ctx))) {
27
- throw new Error("Authentication fallback completed, but validation still failed.");
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`.