libretto 0.6.24 → 0.6.26
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/README.md +9 -1
- package/README.template.md +9 -1
- package/dist/cli/commands/browser.js +17 -10
- package/dist/cli/commands/cloud-credentials.js +70 -0
- package/dist/cli/commands/deploy.js +24 -2
- package/dist/cli/commands/execution.js +9 -30
- package/dist/cli/commands/import-chrome-profiles.js +46 -0
- package/dist/cli/commands/profiles.js +71 -0
- package/dist/cli/commands/shared.js +1 -3
- package/dist/cli/core/browser.js +89 -75
- package/dist/cli/core/daemon/daemon.js +47 -35
- package/dist/cli/core/daemon/ipc.js +3 -0
- package/dist/cli/core/deploy-artifact.js +85 -22
- package/dist/cli/core/profiles.js +47 -0
- package/dist/cli/core/prompt.js +9 -0
- package/dist/cli/core/providers/libretto-cloud.js +6 -2
- package/dist/cli/core/session-logs.js +325 -0
- package/dist/cli/core/telemetry.js +110 -311
- package/dist/cli/core/workflow-runner/runner.js +65 -0
- package/dist/cli/router.js +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/dist/shared/workflow/auth-profile-name.d.ts +3 -0
- package/dist/shared/workflow/auth-profile-name.js +29 -0
- package/dist/shared/workflow/auth-profile-state.d.ts +20 -0
- package/dist/shared/workflow/auth-profile-state.js +105 -0
- package/dist/shared/workflow/authenticate.d.ts +17 -0
- package/dist/shared/workflow/authenticate.js +37 -0
- package/dist/shared/workflow/credentials.d.ts +5 -0
- package/dist/shared/workflow/credentials.js +68 -0
- package/dist/shared/workflow/workflow.d.ts +16 -1
- package/dist/shared/workflow/workflow.js +56 -4
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +3 -4
- package/skills/libretto/references/auth-profiles.md +61 -11
- package/skills/libretto/references/code-generation-rules.md +31 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/browser.ts +19 -11
- package/src/cli/commands/cloud-credentials.ts +82 -0
- package/src/cli/commands/deploy.ts +41 -2
- package/src/cli/commands/execution.ts +10 -31
- package/src/cli/commands/import-chrome-profiles.ts +46 -0
- package/src/cli/commands/profiles.ts +90 -0
- package/src/cli/commands/shared.ts +4 -8
- package/src/cli/core/browser.ts +102 -91
- package/src/cli/core/daemon/config.ts +4 -1
- package/src/cli/core/daemon/daemon.ts +52 -44
- package/src/cli/core/daemon/ipc.ts +15 -0
- package/src/cli/core/deploy-artifact.ts +131 -32
- package/src/cli/core/profiles.ts +53 -0
- package/src/cli/core/prompt.ts +15 -0
- package/src/cli/core/providers/libretto-cloud.ts +6 -2
- package/src/cli/core/providers/types.ts +4 -1
- package/src/cli/core/session-logs.ts +445 -0
- package/src/cli/core/telemetry.ts +142 -413
- package/src/cli/core/workflow-runner/runner.ts +86 -1
- package/src/cli/router.ts +8 -0
- package/src/index.ts +10 -0
- package/src/shared/workflow/auth-profile-name.ts +27 -0
- package/src/shared/workflow/auth-profile-state.ts +144 -0
- package/src/shared/workflow/authenticate.ts +63 -0
- package/src/shared/workflow/credentials.ts +91 -0
- package/src/shared/workflow/workflow.ts +89 -4
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
function parseAuthProfileSites(value) {
|
|
2
|
+
const sites = value.split(",").map((entry) => normalizeAuthProfileSite(entry)).filter((entry) => Boolean(entry));
|
|
3
|
+
return [...new Set(sites)];
|
|
4
|
+
}
|
|
5
|
+
function normalizeAuthProfileSite(value) {
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
if (!trimmed) return null;
|
|
8
|
+
try {
|
|
9
|
+
const url = trimmed.includes("://") ? new URL(trimmed) : new URL(`https://${trimmed}`);
|
|
10
|
+
return normalizeHost(url.hostname);
|
|
11
|
+
} catch {
|
|
12
|
+
const normalized = normalizeHost(trimmed);
|
|
13
|
+
return normalized || null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function captureAuthProfileStorageState(context, sites) {
|
|
17
|
+
const normalizedSites = [...new Set(
|
|
18
|
+
sites.map((site) => normalizeAuthProfileSite(site)).filter((site) => Boolean(site))
|
|
19
|
+
)];
|
|
20
|
+
if (normalizedSites.length === 0) {
|
|
21
|
+
throw new Error("At least one auth profile site is required.");
|
|
22
|
+
}
|
|
23
|
+
const state = await context.storageState({ indexedDB: true });
|
|
24
|
+
return {
|
|
25
|
+
sites: normalizedSites,
|
|
26
|
+
cookies: state.cookies.filter(
|
|
27
|
+
(cookie) => cookieDomainMatchesSites(cookie.domain, normalizedSites)
|
|
28
|
+
),
|
|
29
|
+
origins: state.origins.filter(
|
|
30
|
+
(origin) => originMatchesSites(origin.origin, normalizedSites)
|
|
31
|
+
)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function mergeAuthProfileStorageState(existing, latest, sites) {
|
|
35
|
+
const normalizedSites = [...new Set(
|
|
36
|
+
sites.map((site) => normalizeAuthProfileSite(site)).filter((site) => Boolean(site))
|
|
37
|
+
)];
|
|
38
|
+
if (normalizedSites.length === 0) {
|
|
39
|
+
return existing;
|
|
40
|
+
}
|
|
41
|
+
const existingCookies = existing.cookies ?? [];
|
|
42
|
+
const latestCookies = latest.cookies ?? [];
|
|
43
|
+
const existingOrigins = existing.origins ?? [];
|
|
44
|
+
const latestOrigins = latest.origins ?? [];
|
|
45
|
+
const mergedSites = [
|
|
46
|
+
.../* @__PURE__ */ new Set([
|
|
47
|
+
...existing.sites ?? [],
|
|
48
|
+
...normalizedSites
|
|
49
|
+
])
|
|
50
|
+
];
|
|
51
|
+
return {
|
|
52
|
+
...existing,
|
|
53
|
+
sites: mergedSites,
|
|
54
|
+
cookies: [
|
|
55
|
+
...existingCookies.filter(
|
|
56
|
+
(cookie) => !cookieMatchesSites(cookie, normalizedSites)
|
|
57
|
+
),
|
|
58
|
+
...latestCookies.filter(
|
|
59
|
+
(cookie) => cookieMatchesSites(cookie, normalizedSites)
|
|
60
|
+
)
|
|
61
|
+
],
|
|
62
|
+
origins: [
|
|
63
|
+
...existingOrigins.filter(
|
|
64
|
+
(origin) => !originMatchesSites(origin.origin, normalizedSites)
|
|
65
|
+
),
|
|
66
|
+
...latestOrigins.filter(
|
|
67
|
+
(origin) => originMatchesSites(origin.origin, normalizedSites)
|
|
68
|
+
)
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function normalizeHost(value) {
|
|
73
|
+
let host = value.trim().toLowerCase();
|
|
74
|
+
while (host.startsWith(".")) host = host.slice(1);
|
|
75
|
+
while (host.endsWith(".")) host = host.slice(0, -1);
|
|
76
|
+
return host;
|
|
77
|
+
}
|
|
78
|
+
function cookieMatchesSites(cookie, sites) {
|
|
79
|
+
if (!cookie || typeof cookie !== "object" || !("domain" in cookie)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const domain = cookie.domain;
|
|
83
|
+
return typeof domain === "string" && cookieDomainMatchesSites(domain, sites);
|
|
84
|
+
}
|
|
85
|
+
function cookieDomainMatchesSites(cookieDomain, sites) {
|
|
86
|
+
const domain = normalizeHost(cookieDomain);
|
|
87
|
+
if (!domain) return false;
|
|
88
|
+
return sites.some(
|
|
89
|
+
(site) => domain === site || domain.endsWith(`.${site}`) || site.endsWith(`.${domain}`)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
function originMatchesSites(origin, sites) {
|
|
93
|
+
try {
|
|
94
|
+
const host = normalizeHost(new URL(origin).hostname);
|
|
95
|
+
return sites.some((site) => host === site || host.endsWith(`.${site}`));
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
captureAuthProfileStorageState,
|
|
102
|
+
mergeAuthProfileStorageState,
|
|
103
|
+
normalizeAuthProfileSite,
|
|
104
|
+
parseAuthProfileSites
|
|
105
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LibrettoWorkflowContext } from './workflow.js';
|
|
2
|
+
import 'playwright';
|
|
3
|
+
import 'zod';
|
|
4
|
+
import '../../runtime/recovery/page-fallbacks.js';
|
|
5
|
+
import 'ai';
|
|
6
|
+
|
|
7
|
+
type LibrettoAuthenticateOptions = {
|
|
8
|
+
validate: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
|
|
9
|
+
fallback: (ctx: LibrettoWorkflowContext, credentials: Record<string, string>) => Promise<void> | void;
|
|
10
|
+
credentials?: Record<string, unknown>;
|
|
11
|
+
envPrefix?: string;
|
|
12
|
+
};
|
|
13
|
+
declare function librettoAuthenticate(ctx: LibrettoWorkflowContext, options: LibrettoAuthenticateOptions): Promise<{
|
|
14
|
+
usedProfile: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
export { type LibrettoAuthenticateOptions, librettoAuthenticate };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
async function librettoAuthenticate(ctx, options) {
|
|
2
|
+
if (await options.validate(ctx)) {
|
|
3
|
+
return { usedProfile: true };
|
|
4
|
+
}
|
|
5
|
+
const credentials = normalizeCredentials(
|
|
6
|
+
options.credentials ?? readCredentialsFromEnv(options.envPrefix)
|
|
7
|
+
);
|
|
8
|
+
await options.fallback(ctx, credentials);
|
|
9
|
+
if (!await options.validate(ctx)) {
|
|
10
|
+
throw new Error("Authentication fallback completed, but validation still failed.");
|
|
11
|
+
}
|
|
12
|
+
return { usedProfile: false };
|
|
13
|
+
}
|
|
14
|
+
function normalizeCredentials(credentials) {
|
|
15
|
+
const normalized = {};
|
|
16
|
+
for (const [key, value] of Object.entries(credentials)) {
|
|
17
|
+
if (typeof value === "string") normalized[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
function readCredentialsFromEnv(envPrefix = "LIBRETTO_") {
|
|
22
|
+
const credentials = {};
|
|
23
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
24
|
+
if (key.startsWith(envPrefix) && value !== void 0) {
|
|
25
|
+
const credentialName = key.slice(envPrefix.length).toLowerCase();
|
|
26
|
+
if (isLibrettoControlCredential(envPrefix, credentialName)) continue;
|
|
27
|
+
credentials[credentialName] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return credentials;
|
|
31
|
+
}
|
|
32
|
+
function isLibrettoControlCredential(envPrefix, credentialName) {
|
|
33
|
+
return envPrefix === "LIBRETTO_" && ["api_key", "api_url", "timeout_seconds"].includes(credentialName);
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
librettoAuthenticate
|
|
37
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
declare function readCredentialInputsFromEnv(env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
2
|
+
declare function normalizeCredentialNames(names: readonly string[] | undefined): string[];
|
|
3
|
+
declare function mergeCredentialsIntoInput(input: unknown, credentialNames?: readonly string[], env?: NodeJS.ProcessEnv): unknown;
|
|
4
|
+
|
|
5
|
+
export { mergeCredentialsIntoInput, normalizeCredentialNames, readCredentialInputsFromEnv };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function asStringMap(value) {
|
|
2
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3
|
+
return {};
|
|
4
|
+
}
|
|
5
|
+
const result = {};
|
|
6
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
7
|
+
if (typeof rawValue === "string") result[key] = rawValue;
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
function readHostedCredentials(input) {
|
|
12
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
const record = input;
|
|
16
|
+
return asStringMap(record.credentials);
|
|
17
|
+
}
|
|
18
|
+
function readCredentialInputsFromEnv(env = process.env) {
|
|
19
|
+
const credentials = {};
|
|
20
|
+
for (const [key, value] of Object.entries(env)) {
|
|
21
|
+
if (!key.startsWith("LIBRETTO_CLOUD_") || value === void 0) continue;
|
|
22
|
+
if (value.trim().length === 0) continue;
|
|
23
|
+
const name = key.slice("LIBRETTO_CLOUD_".length).toLowerCase();
|
|
24
|
+
if (!name || name === "api_key") continue;
|
|
25
|
+
credentials[name] = value;
|
|
26
|
+
}
|
|
27
|
+
return credentials;
|
|
28
|
+
}
|
|
29
|
+
function normalizeCredentialNames(names) {
|
|
30
|
+
if (!names) return [];
|
|
31
|
+
const normalized = /* @__PURE__ */ new Set();
|
|
32
|
+
for (const name of names) {
|
|
33
|
+
const value = name.trim().toLowerCase();
|
|
34
|
+
if (value.length > 0) normalized.add(value);
|
|
35
|
+
}
|
|
36
|
+
return [...normalized];
|
|
37
|
+
}
|
|
38
|
+
function filterCredentialMap(credentials, names) {
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
if (credentials[name] !== void 0) result[name] = credentials[name];
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function shouldReadCredentialInputsFromEnv(env) {
|
|
46
|
+
return (env.LIBRETTO_HOSTED_RUNTIME?.trim().length ?? 0) === 0;
|
|
47
|
+
}
|
|
48
|
+
function mergeCredentialsIntoInput(input, credentialNames, env = process.env) {
|
|
49
|
+
const normalizedNames = normalizeCredentialNames(credentialNames);
|
|
50
|
+
if (normalizedNames.length === 0) return input;
|
|
51
|
+
const existingCredentials = readHostedCredentials(input);
|
|
52
|
+
const envCredentials = shouldReadCredentialInputsFromEnv(env) ? readCredentialInputsFromEnv(env) : {};
|
|
53
|
+
const mergedCredentials = {
|
|
54
|
+
...filterCredentialMap(envCredentials, normalizedNames),
|
|
55
|
+
...filterCredentialMap(existingCredentials, normalizedNames)
|
|
56
|
+
};
|
|
57
|
+
if (Object.keys(mergedCredentials).length === 0) return input;
|
|
58
|
+
const base = input && typeof input === "object" && !Array.isArray(input) ? { ...input } : {};
|
|
59
|
+
return {
|
|
60
|
+
...base,
|
|
61
|
+
credentials: mergedCredentials
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
mergeCredentialsIntoInput,
|
|
66
|
+
normalizeCredentialNames,
|
|
67
|
+
readCredentialInputsFromEnv
|
|
68
|
+
};
|
|
@@ -9,9 +9,15 @@ type LibrettoWorkflowContext = {
|
|
|
9
9
|
page: Page;
|
|
10
10
|
};
|
|
11
11
|
type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (ctx: LibrettoWorkflowContext, input: Input) => Promise<Output>;
|
|
12
|
+
type LibrettoWorkflowAuthProfile = string | {
|
|
13
|
+
name: string;
|
|
14
|
+
refresh?: boolean;
|
|
15
|
+
};
|
|
12
16
|
type LibrettoWorkflowDefinition<InputSchema extends z.ZodType = z.ZodType<unknown>, OutputSchema extends z.ZodType = z.ZodType<unknown>> = {
|
|
13
17
|
input?: InputSchema;
|
|
14
18
|
output?: OutputSchema;
|
|
19
|
+
credentials?: readonly string[];
|
|
20
|
+
authProfile?: LibrettoWorkflowAuthProfile;
|
|
15
21
|
recoveryAction?: RecoveryAction;
|
|
16
22
|
};
|
|
17
23
|
type LibrettoWorkflowOptions<InputSchema extends z.ZodType = z.ZodType<unknown>, OutputSchema extends z.ZodType = z.ZodType<unknown>> = LibrettoWorkflowDefinition<InputSchema, OutputSchema> & {
|
|
@@ -32,11 +38,17 @@ declare class LibrettoWorkflow<InputSchema extends z.ZodType = z.ZodType<unknown
|
|
|
32
38
|
readonly name: string;
|
|
33
39
|
readonly inputSchema?: InputSchema;
|
|
34
40
|
readonly outputSchema?: OutputSchema;
|
|
41
|
+
readonly credentialNames: readonly string[];
|
|
42
|
+
readonly authProfileName?: string;
|
|
43
|
+
readonly authProfileRefresh?: boolean;
|
|
35
44
|
readonly recoveryAction?: RecoveryAction;
|
|
36
45
|
private readonly handler;
|
|
37
46
|
constructor(name: string, options: {
|
|
38
47
|
inputSchema?: InputSchema;
|
|
39
48
|
outputSchema?: OutputSchema;
|
|
49
|
+
credentialNames?: readonly string[];
|
|
50
|
+
authProfileName?: string;
|
|
51
|
+
authProfileRefresh?: boolean;
|
|
40
52
|
recoveryAction?: RecoveryAction;
|
|
41
53
|
} | undefined, handler: LibrettoWorkflowHandler<z.infer<InputSchema>, z.infer<OutputSchema>>);
|
|
42
54
|
run(ctx: LibrettoWorkflowContext, input: unknown): Promise<z.infer<OutputSchema>>;
|
|
@@ -46,6 +58,9 @@ type ExportedLibrettoWorkflow = {
|
|
|
46
58
|
readonly name: string;
|
|
47
59
|
readonly inputSchema?: z.ZodType;
|
|
48
60
|
readonly outputSchema?: z.ZodType;
|
|
61
|
+
readonly credentialNames: readonly string[];
|
|
62
|
+
readonly authProfileName?: string;
|
|
63
|
+
readonly authProfileRefresh?: boolean;
|
|
49
64
|
readonly recoveryAction?: RecoveryAction;
|
|
50
65
|
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
51
66
|
};
|
|
@@ -58,4 +73,4 @@ declare function workflow<InputSchema extends z.ZodType = z.ZodType<unknown>, Ou
|
|
|
58
73
|
declare function workflow<InputSchema extends z.ZodType = z.ZodType<unknown>, OutputSchema extends z.ZodType = z.ZodType<unknown>>(name: string, options: LibrettoWorkflowOptions<InputSchema, OutputSchema>): LibrettoWorkflow<InputSchema, OutputSchema>;
|
|
59
74
|
declare function workflow<Input = unknown, Output = unknown>(name: string, handler: LibrettoWorkflowHandler<Input, Output>): LibrettoWorkflow<z.ZodType<Input>, z.ZodType<Output>>;
|
|
60
75
|
|
|
61
|
-
export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowDefinition, type LibrettoWorkflowHandler, LibrettoWorkflowInputError, type LibrettoWorkflowOptions, type WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow };
|
|
76
|
+
export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowAuthProfile, type LibrettoWorkflowContext, type LibrettoWorkflowDefinition, type LibrettoWorkflowHandler, LibrettoWorkflowInputError, type LibrettoWorkflowOptions, type WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow };
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRecoveryPage
|
|
3
3
|
} from "../../runtime/recovery/page-fallbacks.js";
|
|
4
|
+
import { normalizeProfileName } from "./auth-profile-name.js";
|
|
5
|
+
import {
|
|
6
|
+
mergeCredentialsIntoInput,
|
|
7
|
+
normalizeCredentialNames
|
|
8
|
+
} from "./credentials.js";
|
|
4
9
|
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
5
10
|
class LibrettoWorkflowInputError extends Error {
|
|
6
11
|
workflowName;
|
|
@@ -25,10 +30,34 @@ function formatZodErrorMessage(workflowName, zodError) {
|
|
|
25
30
|
function parseWorkflowInput(workflowName, inputSchema, input) {
|
|
26
31
|
if (!inputSchema) return input;
|
|
27
32
|
const result = inputSchema.safeParse(input);
|
|
28
|
-
if (
|
|
29
|
-
|
|
33
|
+
if (result.success) {
|
|
34
|
+
return reattachCredentialInput(result.data, input);
|
|
35
|
+
}
|
|
36
|
+
const stripped = stripCredentialInput(input);
|
|
37
|
+
if (stripped !== input) {
|
|
38
|
+
const strippedResult = inputSchema.safeParse(stripped);
|
|
39
|
+
if (strippedResult.success) {
|
|
40
|
+
return reattachCredentialInput(strippedResult.data, input);
|
|
41
|
+
}
|
|
30
42
|
}
|
|
31
|
-
|
|
43
|
+
throw new LibrettoWorkflowInputError(workflowName, result.error);
|
|
44
|
+
}
|
|
45
|
+
function stripCredentialInput(input) {
|
|
46
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return input;
|
|
47
|
+
const { credentials: _credentials, ...rest } = input;
|
|
48
|
+
if (!("credentials" in input)) return input;
|
|
49
|
+
return rest;
|
|
50
|
+
}
|
|
51
|
+
function reattachCredentialInput(parsed, raw) {
|
|
52
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return parsed;
|
|
53
|
+
const rawRecord = raw;
|
|
54
|
+
const rawCredentials = rawRecord.credentials && typeof rawRecord.credentials === "object" ? rawRecord.credentials : void 0;
|
|
55
|
+
if (!rawCredentials) return parsed;
|
|
56
|
+
const parsedRecord = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { ...parsed } : {};
|
|
57
|
+
return {
|
|
58
|
+
...parsedRecord,
|
|
59
|
+
credentials: rawCredentials
|
|
60
|
+
};
|
|
32
61
|
}
|
|
33
62
|
function validateWorkflowInput(workflow2, input) {
|
|
34
63
|
parseWorkflowInput(workflow2.name, workflow2.inputSchema, input);
|
|
@@ -44,17 +73,27 @@ class LibrettoWorkflow {
|
|
|
44
73
|
// this schema to JSON Schema at build time and exposes it via
|
|
45
74
|
// /v1/workflows/get so API consumers know the workflow's output shape.
|
|
46
75
|
outputSchema;
|
|
76
|
+
credentialNames;
|
|
77
|
+
authProfileName;
|
|
78
|
+
authProfileRefresh;
|
|
47
79
|
recoveryAction;
|
|
48
80
|
handler;
|
|
49
81
|
constructor(name, options, handler) {
|
|
50
82
|
this.name = name;
|
|
51
83
|
this.inputSchema = options?.inputSchema;
|
|
52
84
|
this.outputSchema = options?.outputSchema;
|
|
85
|
+
this.credentialNames = options?.credentialNames ?? [];
|
|
86
|
+
this.authProfileName = options?.authProfileName;
|
|
87
|
+
this.authProfileRefresh = options?.authProfileRefresh;
|
|
53
88
|
this.recoveryAction = options?.recoveryAction;
|
|
54
89
|
this.handler = handler;
|
|
55
90
|
}
|
|
56
91
|
async run(ctx, input) {
|
|
57
|
-
const parsed = parseWorkflowInput(
|
|
92
|
+
const parsed = parseWorkflowInput(
|
|
93
|
+
this.name,
|
|
94
|
+
this.inputSchema,
|
|
95
|
+
mergeCredentialsIntoInput(input, this.credentialNames)
|
|
96
|
+
);
|
|
58
97
|
const workflowContext = !this.recoveryAction ? ctx : {
|
|
59
98
|
...ctx,
|
|
60
99
|
page: createRecoveryPage(ctx.page, {
|
|
@@ -113,9 +152,13 @@ function getWorkflowFromModuleExports(moduleExports, workflowName) {
|
|
|
113
152
|
return null;
|
|
114
153
|
}
|
|
115
154
|
function getWorkflowConstructorOptions(options) {
|
|
155
|
+
const authProfile = normalizeWorkflowAuthProfile(options.authProfile);
|
|
116
156
|
return {
|
|
117
157
|
inputSchema: options.input,
|
|
118
158
|
outputSchema: options.output,
|
|
159
|
+
credentialNames: normalizeCredentialNames(options.credentials),
|
|
160
|
+
authProfileName: authProfile?.name,
|
|
161
|
+
authProfileRefresh: authProfile?.refresh,
|
|
119
162
|
recoveryAction: options.recoveryAction
|
|
120
163
|
};
|
|
121
164
|
}
|
|
@@ -141,6 +184,15 @@ function workflow(name, definitionOrHandler, maybeHandler) {
|
|
|
141
184
|
maybeHandler
|
|
142
185
|
);
|
|
143
186
|
}
|
|
187
|
+
function normalizeWorkflowAuthProfile(value) {
|
|
188
|
+
if (!value) return void 0;
|
|
189
|
+
if (typeof value === "string") return { name: normalizeProfileName(value) };
|
|
190
|
+
const name = normalizeProfileName(value.name);
|
|
191
|
+
return {
|
|
192
|
+
name,
|
|
193
|
+
...value.refresh === void 0 ? {} : { refresh: value.refresh }
|
|
194
|
+
};
|
|
195
|
+
}
|
|
144
196
|
export {
|
|
145
197
|
LIBRETTO_WORKFLOW_BRAND,
|
|
146
198
|
LibrettoWorkflow,
|
package/package.json
CHANGED
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.26"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## How Libretto Works
|
|
@@ -23,7 +23,7 @@ Full documentation is published at [libretto.sh](https://libretto.sh). Available
|
|
|
23
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
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
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), [
|
|
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
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
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)
|
|
29
29
|
|
|
@@ -61,7 +61,7 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
|
|
|
61
61
|
- Defer repo/code review until you begin generating code, unless the user explicitly asks for it earlier.
|
|
62
62
|
- Read and follow guidelines in `references/code-generation-rules.md` before generating or editing production workflow code.
|
|
63
63
|
- Validation requires a successful clean `run` 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
|
-
- 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`,
|
|
64
|
+
- 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
65
|
- Treat exploration sessions as disposable unless the user explicitly wants one kept open.
|
|
66
66
|
- Get explicit user confirmation before mutating actions or replaying network requests that may have side effects.
|
|
67
67
|
- Never run multiple `exec` commands at the same time.
|
|
@@ -164,7 +164,6 @@ npx libretto exec --session debug-example --page <page-id> "await page.url()"
|
|
|
164
164
|
npx libretto run ./integration.ts --params '{"status":"open"}'
|
|
165
165
|
npx libretto run ./integration.ts --read-only
|
|
166
166
|
npx libretto run ./integration.ts --stay-open-on-success
|
|
167
|
-
npx libretto run ./integration.ts --auth-profile app.example.com
|
|
168
167
|
```
|
|
169
168
|
|
|
170
169
|
### `resume`
|
|
@@ -1,29 +1,79 @@
|
|
|
1
1
|
# Auth Profiles
|
|
2
2
|
|
|
3
|
-
Use this reference
|
|
3
|
+
Use this reference when generating or maintaining workflows that need a logged-in website session.
|
|
4
4
|
|
|
5
5
|
## When to Use This
|
|
6
6
|
|
|
7
|
-
- The
|
|
8
|
-
- The
|
|
7
|
+
- The user wants to persist authentication across runs.
|
|
8
|
+
- The workflow should recover when saved login state is stale.
|
|
9
9
|
|
|
10
10
|
## Workflow
|
|
11
11
|
|
|
12
12
|
- Open the site in headed mode.
|
|
13
13
|
- Ask the user to log in manually.
|
|
14
|
-
- Save the current session as a profile.
|
|
15
|
-
-
|
|
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
16
|
|
|
17
17
|
## Commands
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
|
|
21
|
-
npx libretto save app.example.com
|
|
22
|
-
|
|
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
|
+
});
|
|
23
70
|
```
|
|
24
71
|
|
|
25
72
|
## Notes
|
|
26
73
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
|
|
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`.
|
|
@@ -40,7 +40,7 @@ export default workflow("myWorkflow", {
|
|
|
40
40
|
|
|
41
41
|
Key points:
|
|
42
42
|
|
|
43
|
-
- `workflow(name, { input, output, recoveryAction, handler })` takes a unique workflow name, optional Zod input/output schemas, an optional recovery action, and the async handler. The handler's `input` parameter is inferred from the input schema — do not redeclare it with a separate `type Input = ...`.
|
|
43
|
+
- `workflow(name, { input, output, credentials, authProfile, recoveryAction, handler })` takes a unique workflow name, optional Zod input/output schemas, optional declared credential names, an optional auth profile, an optional recovery action, and the async handler. The handler's `input` parameter is inferred from the input schema — do not redeclare it with a separate `type Input = ...`.
|
|
44
44
|
- At run time, Libretto validates `input` against `inputSchema` before calling the handler. Invalid input throws a clear error listing each failing field; the workflow handler never sees malformed input.
|
|
45
45
|
- `npx libretto run ./file.ts` executes the file's default-exported workflow, so always use `export default workflow(...)`.
|
|
46
46
|
- `ctx` provides `session` and `page`. Use `console.log`/`console.warn`/`console.error` for logging — the runtime wraps these with structured metadata automatically.
|
|
@@ -49,6 +49,36 @@ 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 Auth Profiles
|
|
53
|
+
|
|
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
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
export default workflow("sentimentWorkflow", {
|
|
58
|
+
credentials: ["openai_api_key"],
|
|
59
|
+
input: z.object({
|
|
60
|
+
pageUrl: z.string().url(),
|
|
61
|
+
}),
|
|
62
|
+
output: z.object({
|
|
63
|
+
sentiment: z.string(),
|
|
64
|
+
}),
|
|
65
|
+
async handler(ctx, input) {
|
|
66
|
+
const apiKey = input.credentials.openai_api_key;
|
|
67
|
+
// Use apiKey for server-side API calls.
|
|
68
|
+
return { sentiment: "neutral" };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For logged-in website workflows, unless the user says otherwise, generate both:
|
|
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.
|
|
81
|
+
|
|
52
82
|
## Workflow Error Handling
|
|
53
83
|
|
|
54
84
|
Do not add runtime recovery by default. Add `recoveryAction` only when exploration shows nondeterministic blockers such as popups, cookie banners, modals, or overlays.
|