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.
Files changed (63) hide show
  1. package/README.md +9 -1
  2. package/README.template.md +9 -1
  3. package/dist/cli/commands/browser.js +17 -10
  4. package/dist/cli/commands/cloud-credentials.js +70 -0
  5. package/dist/cli/commands/deploy.js +24 -2
  6. package/dist/cli/commands/execution.js +9 -30
  7. package/dist/cli/commands/import-chrome-profiles.js +46 -0
  8. package/dist/cli/commands/profiles.js +71 -0
  9. package/dist/cli/commands/shared.js +1 -3
  10. package/dist/cli/core/browser.js +89 -75
  11. package/dist/cli/core/daemon/daemon.js +47 -35
  12. package/dist/cli/core/daemon/ipc.js +3 -0
  13. package/dist/cli/core/deploy-artifact.js +85 -22
  14. package/dist/cli/core/profiles.js +47 -0
  15. package/dist/cli/core/prompt.js +9 -0
  16. package/dist/cli/core/providers/libretto-cloud.js +6 -2
  17. package/dist/cli/core/session-logs.js +325 -0
  18. package/dist/cli/core/telemetry.js +110 -311
  19. package/dist/cli/core/workflow-runner/runner.js +65 -0
  20. package/dist/cli/router.js +9 -1
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +12 -0
  23. package/dist/shared/workflow/auth-profile-name.d.ts +3 -0
  24. package/dist/shared/workflow/auth-profile-name.js +29 -0
  25. package/dist/shared/workflow/auth-profile-state.d.ts +20 -0
  26. package/dist/shared/workflow/auth-profile-state.js +105 -0
  27. package/dist/shared/workflow/authenticate.d.ts +17 -0
  28. package/dist/shared/workflow/authenticate.js +37 -0
  29. package/dist/shared/workflow/credentials.d.ts +5 -0
  30. package/dist/shared/workflow/credentials.js +68 -0
  31. package/dist/shared/workflow/workflow.d.ts +16 -1
  32. package/dist/shared/workflow/workflow.js +56 -4
  33. package/package.json +1 -1
  34. package/skills/libretto/SKILL.md +3 -4
  35. package/skills/libretto/references/auth-profiles.md +61 -11
  36. package/skills/libretto/references/code-generation-rules.md +31 -1
  37. package/skills/libretto-readonly/SKILL.md +1 -1
  38. package/src/cli/commands/browser.ts +19 -11
  39. package/src/cli/commands/cloud-credentials.ts +82 -0
  40. package/src/cli/commands/deploy.ts +41 -2
  41. package/src/cli/commands/execution.ts +10 -31
  42. package/src/cli/commands/import-chrome-profiles.ts +46 -0
  43. package/src/cli/commands/profiles.ts +90 -0
  44. package/src/cli/commands/shared.ts +4 -8
  45. package/src/cli/core/browser.ts +102 -91
  46. package/src/cli/core/daemon/config.ts +4 -1
  47. package/src/cli/core/daemon/daemon.ts +52 -44
  48. package/src/cli/core/daemon/ipc.ts +15 -0
  49. package/src/cli/core/deploy-artifact.ts +131 -32
  50. package/src/cli/core/profiles.ts +53 -0
  51. package/src/cli/core/prompt.ts +15 -0
  52. package/src/cli/core/providers/libretto-cloud.ts +6 -2
  53. package/src/cli/core/providers/types.ts +4 -1
  54. package/src/cli/core/session-logs.ts +445 -0
  55. package/src/cli/core/telemetry.ts +142 -413
  56. package/src/cli/core/workflow-runner/runner.ts +86 -1
  57. package/src/cli/router.ts +8 -0
  58. package/src/index.ts +10 -0
  59. package/src/shared/workflow/auth-profile-name.ts +27 -0
  60. package/src/shared/workflow/auth-profile-state.ts +144 -0
  61. package/src/shared/workflow/authenticate.ts +63 -0
  62. package/src/shared/workflow/credentials.ts +91 -0
  63. package/src/shared/workflow/workflow.ts +89 -4
@@ -1,10 +1,15 @@
1
- import type { BrowserContext, Page } from "playwright";
1
+ import type { BrowserContext, Frame, Page } from "playwright";
2
2
  import type { LoggerApi } from "../../../shared/logger/index.js";
3
3
  import { installPauseHandler } from "../../../shared/debug/pause-handler.js";
4
+ import {
5
+ mergeAuthProfileStorageState,
6
+ normalizeAuthProfileSite,
7
+ } from "../../../shared/workflow/auth-profile-state.js";
4
8
  import type {
5
9
  ExportedLibrettoWorkflow,
6
10
  LibrettoWorkflowContext,
7
11
  } from "../../../shared/workflow/workflow.js";
12
+ import { readProfile, writeProfile } from "../profiles.js";
8
13
  import {
9
14
  getAbsoluteIntegrationPath,
10
15
  installHeadedWorkflowVisualization,
@@ -44,6 +49,7 @@ export type WorkflowControllerConfig = {
44
49
  page: Page;
45
50
  context: BrowserContext;
46
51
  logger: LoggerApi;
52
+ refreshLocalAuthProfiles?: boolean;
47
53
  onLog?: (event: WorkflowLogEvent) => void;
48
54
  onOutcome?: (outcome: WorkflowOutcome) => void;
49
55
  };
@@ -155,6 +161,7 @@ export class WorkflowController {
155
161
  session: this.config.session,
156
162
  page: this.config.page,
157
163
  };
164
+ const visitedSites = createVisitedSiteTracker(this.config.context);
158
165
 
159
166
  const uninstallPauseHandler = installPauseHandler((pauseArgs) =>
160
167
  this.pause({
@@ -164,6 +171,12 @@ export class WorkflowController {
164
171
  );
165
172
  try {
166
173
  await workflow.run(workflowContext, workflowConfig.params ?? {});
174
+ await refreshLocalAuthProfileIfEnabled({
175
+ context: this.config.context,
176
+ enabled: this.config.refreshLocalAuthProfiles === true,
177
+ sites: visitedSites.sites(),
178
+ workflow,
179
+ });
167
180
  } catch (error) {
168
181
  this.emitOutcome({
169
182
  state: "finished",
@@ -174,6 +187,7 @@ export class WorkflowController {
174
187
  return;
175
188
  } finally {
176
189
  uninstallPauseHandler();
190
+ visitedSites.dispose();
177
191
  }
178
192
 
179
193
  this.emitOutcome({
@@ -232,6 +246,77 @@ export class WorkflowController {
232
246
  }
233
247
  }
234
248
 
249
+ async function refreshLocalAuthProfileIfEnabled(
250
+ args: {
251
+ context: BrowserContext;
252
+ enabled: boolean;
253
+ sites: readonly string[];
254
+ workflow: ExportedLibrettoWorkflow;
255
+ },
256
+ ): Promise<void> {
257
+ const { context, enabled, sites, workflow } = args;
258
+ if (!workflow.authProfileName || workflow.authProfileRefresh !== true) {
259
+ return;
260
+ }
261
+ if (!enabled) return;
262
+ if (sites.length === 0) {
263
+ console.warn(
264
+ `Auth profile refresh skipped for "${workflow.authProfileName}": workflow did not visit any http(s) sites.`,
265
+ );
266
+ return;
267
+ }
268
+ const existing = readProfile(workflow.authProfileName);
269
+ const latest = await context.storageState({ indexedDB: true });
270
+ const state = mergeAuthProfileStorageState(existing, latest, sites);
271
+ await writeProfile(workflow.authProfileName, state);
272
+ console.warn(`Auth profile refreshed: ${workflow.authProfileName}`);
273
+ }
274
+
275
+ function createVisitedSiteTracker(context: BrowserContext): {
276
+ sites: () => string[];
277
+ dispose: () => void;
278
+ } {
279
+ const sites = new Set<string>();
280
+ const pageListeners = new Map<Page, (frame: Frame) => void>();
281
+
282
+ const recordUrl = (url: string): void => {
283
+ if (!url.startsWith("http://") && !url.startsWith("https://")) return;
284
+ const site = normalizeAuthProfileSite(url);
285
+ if (site) sites.add(site);
286
+ };
287
+
288
+ const trackPage = (page: Page): void => {
289
+ if (pageListeners.has(page)) return;
290
+ recordUrl(page.url());
291
+
292
+ const onFrameNavigated = (frame: Frame): void => {
293
+ if (frame === page.mainFrame()) {
294
+ recordUrl(frame.url());
295
+ }
296
+ };
297
+
298
+ pageListeners.set(page, onFrameNavigated);
299
+ page.on("framenavigated", onFrameNavigated);
300
+ };
301
+
302
+ for (const page of context.pages()) {
303
+ trackPage(page);
304
+ }
305
+
306
+ context.on("page", trackPage);
307
+
308
+ return {
309
+ sites: () => [...sites],
310
+ dispose: () => {
311
+ context.off("page", trackPage);
312
+ for (const [page, listener] of pageListeners) {
313
+ page.off("framenavigated", listener);
314
+ }
315
+ pageListeners.clear();
316
+ },
317
+ };
318
+ }
319
+
235
320
  function chunkToString(chunk: unknown): string {
236
321
  return Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
237
322
  }
package/src/cli/router.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  import { authCommands } from "./commands/auth.js";
2
2
  import { billingCommands } from "./commands/billing.js";
3
3
  import { browserCommands } from "./commands/browser.js";
4
+ import { cloudCredentialCommands } from "./commands/cloud-credentials.js";
4
5
  import { deployCommand } from "./commands/deploy.js";
5
6
  import { executionCommands } from "./commands/execution.js";
6
7
  import { experimentsCommand } from "./commands/experiments.js";
8
+ import { importChromeProfilesCommand } from "./commands/import-chrome-profiles.js";
9
+ import { profileCommands } from "./commands/profiles.js";
7
10
  import { setupCommand } from "./commands/setup.js";
8
11
  import { statusCommand } from "./commands/status.js";
9
12
  import { snapshotCommand } from "./commands/snapshot.js";
10
13
  import { searchCommand } from "./commands/search.js";
14
+ import { telemetryMiddleware } from "./core/telemetry.js";
11
15
  import { updateCommand } from "./commands/update.js";
12
16
  import { SimpleCLI } from "affordance";
13
17
 
@@ -19,9 +23,12 @@ export const cliRoutes = {
19
23
  deploy: deployCommand,
20
24
  auth: authCommands,
21
25
  billing: billingCommands,
26
+ credentials: cloudCredentialCommands,
27
+ profiles: profileCommands,
22
28
  },
23
29
  }),
24
30
  experiments: experimentsCommand,
31
+ "import-chrome-profiles": importChromeProfilesCommand,
25
32
  ...executionCommands,
26
33
  search: searchCommand,
27
34
  setup: setupCommand,
@@ -32,6 +39,7 @@ export const cliRoutes = {
32
39
 
33
40
  export function createCLIApp() {
34
41
  return SimpleCLI.define("libretto", cliRoutes, {
42
+ middlewares: [telemetryMiddleware],
35
43
  appendHelpText: [
36
44
  "Options:",
37
45
  " --session <name> Required for session-scoped commands",
package/src/index.ts CHANGED
@@ -112,6 +112,10 @@ export {
112
112
  } from "./shared/run/api.js";
113
113
 
114
114
  // Workflow helpers
115
+ export {
116
+ librettoAuthenticate,
117
+ type LibrettoAuthenticateOptions,
118
+ } from "./shared/workflow/authenticate.js";
115
119
  export {
116
120
  getDefaultWorkflowFromModuleExports,
117
121
  getWorkflowFromModuleExports,
@@ -128,6 +132,12 @@ export {
128
132
  type LibrettoWorkflowOptions,
129
133
  type WorkflowInputValidator,
130
134
  } from "./shared/workflow/workflow.js";
135
+ export {
136
+ captureAuthProfileStorageState,
137
+ normalizeAuthProfileSite,
138
+ parseAuthProfileSites,
139
+ type AuthProfileStorageState,
140
+ } from "./shared/workflow/auth-profile-state.js";
131
141
  const isDirectExecution = (): boolean => {
132
142
  const entryArg = process.argv[1];
133
143
  if (!entryArg) {
@@ -0,0 +1,27 @@
1
+ export function normalizeProfileName(name: string): string {
2
+ const trimmed = name.trim();
3
+ if (!trimmed) {
4
+ throw new Error("Profile name is required.");
5
+ }
6
+ if (!isValidProfileName(trimmed)) {
7
+ throw new Error(
8
+ `Invalid profile name "${name}". Use letters, numbers, dots, underscores, and dashes only.`,
9
+ );
10
+ }
11
+ return trimmed;
12
+ }
13
+
14
+ function isValidProfileName(name: string): boolean {
15
+ if (name === "." || name === "..") return false;
16
+ for (let index = 0; index < name.length; index += 1) {
17
+ const code = name.charCodeAt(index);
18
+ const isUppercase = code >= 65 && code <= 90;
19
+ const isLowercase = code >= 97 && code <= 122;
20
+ const isDigit = code >= 48 && code <= 57;
21
+ const isAllowedPunctuation = code === 45 || code === 46 || code === 95;
22
+ if (!isUppercase && !isLowercase && !isDigit && !isAllowedPunctuation) {
23
+ return false;
24
+ }
25
+ }
26
+ return true;
27
+ }
@@ -0,0 +1,144 @@
1
+ import type { BrowserContext } from "playwright";
2
+
3
+ export type AuthProfileStorageState = {
4
+ sites?: string[];
5
+ cookies?: unknown[];
6
+ origins?: Array<{
7
+ origin: string;
8
+ localStorage: Array<{ name: string; value: string }>;
9
+ indexedDB?: unknown;
10
+ }>;
11
+ };
12
+
13
+ export function parseAuthProfileSites(value: string): string[] {
14
+ const sites = value
15
+ .split(",")
16
+ .map((entry) => normalizeAuthProfileSite(entry))
17
+ .filter((entry): entry is string => Boolean(entry));
18
+ return [...new Set(sites)];
19
+ }
20
+
21
+ export function normalizeAuthProfileSite(value: string): string | null {
22
+ const trimmed = value.trim();
23
+ if (!trimmed) return null;
24
+ try {
25
+ const url = trimmed.includes("://")
26
+ ? new URL(trimmed)
27
+ : new URL(`https://${trimmed}`);
28
+ return normalizeHost(url.hostname);
29
+ } catch {
30
+ const normalized = normalizeHost(trimmed);
31
+ return normalized || null;
32
+ }
33
+ }
34
+
35
+ export async function captureAuthProfileStorageState(
36
+ context: BrowserContext,
37
+ sites: readonly string[],
38
+ ): Promise<AuthProfileStorageState> {
39
+ const normalizedSites = [...new Set(
40
+ sites
41
+ .map((site) => normalizeAuthProfileSite(site))
42
+ .filter((site): site is string => Boolean(site)),
43
+ )];
44
+ if (normalizedSites.length === 0) {
45
+ throw new Error("At least one auth profile site is required.");
46
+ }
47
+
48
+ const state = await context.storageState({ indexedDB: true });
49
+ return {
50
+ sites: normalizedSites,
51
+ cookies: state.cookies.filter((cookie) =>
52
+ cookieDomainMatchesSites(cookie.domain, normalizedSites),
53
+ ),
54
+ origins: state.origins.filter((origin) =>
55
+ originMatchesSites(origin.origin, normalizedSites),
56
+ ),
57
+ };
58
+ }
59
+
60
+ export function mergeAuthProfileStorageState(
61
+ existing: AuthProfileStorageState,
62
+ latest: AuthProfileStorageState,
63
+ sites: readonly string[],
64
+ ): AuthProfileStorageState {
65
+ const normalizedSites = [...new Set(
66
+ sites
67
+ .map((site) => normalizeAuthProfileSite(site))
68
+ .filter((site): site is string => Boolean(site)),
69
+ )];
70
+ if (normalizedSites.length === 0) {
71
+ return existing;
72
+ }
73
+
74
+ const existingCookies = existing.cookies ?? [];
75
+ const latestCookies = latest.cookies ?? [];
76
+ const existingOrigins = existing.origins ?? [];
77
+ const latestOrigins = latest.origins ?? [];
78
+
79
+ const mergedSites = [
80
+ ...new Set([
81
+ ...(existing.sites ?? []),
82
+ ...normalizedSites,
83
+ ]),
84
+ ];
85
+
86
+ return {
87
+ ...existing,
88
+ sites: mergedSites,
89
+ cookies: [
90
+ ...existingCookies.filter(
91
+ (cookie) => !cookieMatchesSites(cookie, normalizedSites),
92
+ ),
93
+ ...latestCookies.filter((cookie) =>
94
+ cookieMatchesSites(cookie, normalizedSites),
95
+ ),
96
+ ],
97
+ origins: [
98
+ ...existingOrigins.filter(
99
+ (origin) => !originMatchesSites(origin.origin, normalizedSites),
100
+ ),
101
+ ...latestOrigins.filter((origin) =>
102
+ originMatchesSites(origin.origin, normalizedSites),
103
+ ),
104
+ ],
105
+ };
106
+ }
107
+
108
+ function normalizeHost(value: string): string {
109
+ let host = value.trim().toLowerCase();
110
+ while (host.startsWith(".")) host = host.slice(1);
111
+ while (host.endsWith(".")) host = host.slice(0, -1);
112
+ return host;
113
+ }
114
+
115
+ function cookieMatchesSites(cookie: unknown, sites: readonly string[]): boolean {
116
+ if (!cookie || typeof cookie !== "object" || !("domain" in cookie)) {
117
+ return false;
118
+ }
119
+ const domain = (cookie as { domain?: unknown }).domain;
120
+ return typeof domain === "string" && cookieDomainMatchesSites(domain, sites);
121
+ }
122
+
123
+ function cookieDomainMatchesSites(
124
+ cookieDomain: string,
125
+ sites: readonly string[],
126
+ ): boolean {
127
+ const domain = normalizeHost(cookieDomain);
128
+ if (!domain) return false;
129
+ return sites.some(
130
+ (site) =>
131
+ domain === site ||
132
+ domain.endsWith(`.${site}`) ||
133
+ site.endsWith(`.${domain}`),
134
+ );
135
+ }
136
+
137
+ function originMatchesSites(origin: string, sites: readonly string[]): boolean {
138
+ try {
139
+ const host = normalizeHost(new URL(origin).hostname);
140
+ return sites.some((site) => host === site || host.endsWith(`.${site}`));
141
+ } catch {
142
+ return false;
143
+ }
144
+ }
@@ -0,0 +1,63 @@
1
+ import type { LibrettoWorkflowContext } from "./workflow.js";
2
+
3
+ export type LibrettoAuthenticateOptions = {
4
+ validate: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
5
+ fallback: (
6
+ ctx: LibrettoWorkflowContext,
7
+ credentials: Record<string, string>,
8
+ ) => Promise<void> | void;
9
+ credentials?: Record<string, unknown>;
10
+ envPrefix?: string;
11
+ };
12
+
13
+ export async function librettoAuthenticate(
14
+ ctx: LibrettoWorkflowContext,
15
+ options: LibrettoAuthenticateOptions,
16
+ ): Promise<{ usedProfile: boolean }> {
17
+ if (await options.validate(ctx)) {
18
+ return { usedProfile: true };
19
+ }
20
+
21
+ const credentials = normalizeCredentials(
22
+ options.credentials ?? readCredentialsFromEnv(options.envPrefix),
23
+ );
24
+ await options.fallback(ctx, credentials);
25
+
26
+ if (!(await options.validate(ctx))) {
27
+ throw new Error("Authentication fallback completed, but validation still failed.");
28
+ }
29
+
30
+ return { usedProfile: false };
31
+ }
32
+
33
+ function normalizeCredentials(
34
+ credentials: Record<string, unknown>,
35
+ ): Record<string, string> {
36
+ const normalized: Record<string, string> = {};
37
+ for (const [key, value] of Object.entries(credentials)) {
38
+ if (typeof value === "string") normalized[key] = value;
39
+ }
40
+ return normalized;
41
+ }
42
+
43
+ function readCredentialsFromEnv(envPrefix = "LIBRETTO_"): Record<string, string> {
44
+ const credentials: Record<string, string> = {};
45
+ for (const [key, value] of Object.entries(process.env)) {
46
+ if (key.startsWith(envPrefix) && value !== undefined) {
47
+ const credentialName = key.slice(envPrefix.length).toLowerCase();
48
+ if (isLibrettoControlCredential(envPrefix, credentialName)) continue;
49
+ credentials[credentialName] = value;
50
+ }
51
+ }
52
+ return credentials;
53
+ }
54
+
55
+ function isLibrettoControlCredential(
56
+ envPrefix: string,
57
+ credentialName: string,
58
+ ): boolean {
59
+ return (
60
+ envPrefix === "LIBRETTO_" &&
61
+ ["api_key", "api_url", "timeout_seconds"].includes(credentialName)
62
+ );
63
+ }
@@ -0,0 +1,91 @@
1
+ function asStringMap(value: unknown): Record<string, string> {
2
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3
+ return {};
4
+ }
5
+
6
+ const result: Record<string, string> = {};
7
+ for (const [key, rawValue] of Object.entries(value)) {
8
+ if (typeof rawValue === "string") result[key] = rawValue;
9
+ }
10
+ return result;
11
+ }
12
+
13
+ function readHostedCredentials(input: unknown): Record<string, string> {
14
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
15
+ return {};
16
+ }
17
+
18
+ const record = input as Record<string, unknown>;
19
+ return asStringMap(record.credentials);
20
+ }
21
+
22
+ export function readCredentialInputsFromEnv(
23
+ env: NodeJS.ProcessEnv = process.env,
24
+ ): Record<string, string> {
25
+ const credentials: Record<string, string> = {};
26
+ for (const [key, value] of Object.entries(env)) {
27
+ if (!key.startsWith("LIBRETTO_CLOUD_") || value === undefined) continue;
28
+ if (value.trim().length === 0) continue;
29
+ const name = key.slice("LIBRETTO_CLOUD_".length).toLowerCase();
30
+ if (!name || name === "api_key") continue;
31
+ credentials[name] = value;
32
+ }
33
+ return credentials;
34
+ }
35
+
36
+ export function normalizeCredentialNames(
37
+ names: readonly string[] | undefined,
38
+ ): string[] {
39
+ if (!names) return [];
40
+ const normalized = new Set<string>();
41
+ for (const name of names) {
42
+ const value = name.trim().toLowerCase();
43
+ if (value.length > 0) normalized.add(value);
44
+ }
45
+ return [...normalized];
46
+ }
47
+
48
+ function filterCredentialMap(
49
+ credentials: Record<string, string>,
50
+ names: readonly string[],
51
+ ): Record<string, string> {
52
+ const result: Record<string, string> = {};
53
+ for (const name of names) {
54
+ if (credentials[name] !== undefined) result[name] = credentials[name];
55
+ }
56
+ return result;
57
+ }
58
+
59
+ function shouldReadCredentialInputsFromEnv(env: NodeJS.ProcessEnv): boolean {
60
+ return (env.LIBRETTO_HOSTED_RUNTIME?.trim().length ?? 0) === 0;
61
+ }
62
+
63
+ export function mergeCredentialsIntoInput(
64
+ input: unknown,
65
+ credentialNames?: readonly string[],
66
+ env: NodeJS.ProcessEnv = process.env,
67
+ ): unknown {
68
+ const normalizedNames = normalizeCredentialNames(credentialNames);
69
+ if (normalizedNames.length === 0) return input;
70
+
71
+ const existingCredentials = readHostedCredentials(input);
72
+ const envCredentials = shouldReadCredentialInputsFromEnv(env)
73
+ ? readCredentialInputsFromEnv(env)
74
+ : {};
75
+ const mergedCredentials = {
76
+ ...filterCredentialMap(envCredentials, normalizedNames),
77
+ ...filterCredentialMap(existingCredentials, normalizedNames),
78
+ };
79
+
80
+ if (Object.keys(mergedCredentials).length === 0) return input;
81
+
82
+ const base =
83
+ input && typeof input === "object" && !Array.isArray(input)
84
+ ? { ...(input as Record<string, unknown>) }
85
+ : {};
86
+
87
+ return {
88
+ ...base,
89
+ credentials: mergedCredentials,
90
+ };
91
+ }
@@ -4,6 +4,11 @@ import {
4
4
  createRecoveryPage,
5
5
  type RecoveryAction,
6
6
  } from "../../runtime/recovery/page-fallbacks.js";
7
+ import { normalizeProfileName } from "./auth-profile-name.js";
8
+ import {
9
+ mergeCredentialsIntoInput,
10
+ normalizeCredentialNames,
11
+ } from "./credentials.js";
7
12
 
8
13
  export const LIBRETTO_WORKFLOW_BRAND = Symbol.for("libretto.workflow");
9
14
 
@@ -17,12 +22,21 @@ export type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (
17
22
  input: Input,
18
23
  ) => Promise<Output>;
19
24
 
25
+ export type LibrettoWorkflowAuthProfile =
26
+ | string
27
+ | {
28
+ name: string;
29
+ refresh?: boolean;
30
+ };
31
+
20
32
  export type LibrettoWorkflowDefinition<
21
33
  InputSchema extends z.ZodType = z.ZodType<unknown>,
22
34
  OutputSchema extends z.ZodType = z.ZodType<unknown>,
23
35
  > = {
24
36
  input?: InputSchema;
25
37
  output?: OutputSchema;
38
+ credentials?: readonly string[];
39
+ authProfile?: LibrettoWorkflowAuthProfile;
26
40
  recoveryAction?: RecoveryAction;
27
41
  };
28
42
 
@@ -72,10 +86,44 @@ function parseWorkflowInput<InputSchema extends z.ZodType>(
72
86
  if (!inputSchema) return input as z.infer<InputSchema>;
73
87
 
74
88
  const result = inputSchema.safeParse(input);
75
- if (!result.success) {
76
- throw new LibrettoWorkflowInputError(workflowName, result.error);
89
+ if (result.success) {
90
+ return reattachCredentialInput(result.data, input) as z.infer<InputSchema>;
91
+ }
92
+
93
+ const stripped = stripCredentialInput(input);
94
+ if (stripped !== input) {
95
+ const strippedResult = inputSchema.safeParse(stripped);
96
+ if (strippedResult.success) {
97
+ return reattachCredentialInput(strippedResult.data, input) as z.infer<InputSchema>;
98
+ }
77
99
  }
78
- return result.data;
100
+
101
+ throw new LibrettoWorkflowInputError(workflowName, result.error);
102
+ }
103
+
104
+ function stripCredentialInput(input: unknown): unknown {
105
+ if (!input || typeof input !== "object" || Array.isArray(input)) return input;
106
+ const { credentials: _credentials, ...rest } = input as Record<string, unknown>;
107
+ if (!("credentials" in (input as Record<string, unknown>))) return input;
108
+ return rest;
109
+ }
110
+
111
+ function reattachCredentialInput(parsed: unknown, raw: unknown): unknown {
112
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return parsed;
113
+ const rawRecord = raw as Record<string, unknown>;
114
+ const rawCredentials =
115
+ rawRecord.credentials && typeof rawRecord.credentials === "object"
116
+ ? rawRecord.credentials
117
+ : undefined;
118
+ if (!rawCredentials) return parsed;
119
+ const parsedRecord =
120
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
121
+ ? { ...(parsed as Record<string, unknown>) }
122
+ : {};
123
+ return {
124
+ ...parsedRecord,
125
+ credentials: rawCredentials,
126
+ };
79
127
  }
80
128
 
81
129
  export type WorkflowInputValidator = {
@@ -104,6 +152,9 @@ export class LibrettoWorkflow<
104
152
  // this schema to JSON Schema at build time and exposes it via
105
153
  // /v1/workflows/get so API consumers know the workflow's output shape.
106
154
  public readonly outputSchema?: OutputSchema;
155
+ public readonly credentialNames: readonly string[];
156
+ public readonly authProfileName?: string;
157
+ public readonly authProfileRefresh?: boolean;
107
158
  public readonly recoveryAction?: RecoveryAction;
108
159
  private readonly handler: LibrettoWorkflowHandler<
109
160
  z.infer<InputSchema>,
@@ -116,6 +167,9 @@ export class LibrettoWorkflow<
116
167
  | {
117
168
  inputSchema?: InputSchema;
118
169
  outputSchema?: OutputSchema;
170
+ credentialNames?: readonly string[];
171
+ authProfileName?: string;
172
+ authProfileRefresh?: boolean;
119
173
  recoveryAction?: RecoveryAction;
120
174
  }
121
175
  | undefined,
@@ -127,6 +181,9 @@ export class LibrettoWorkflow<
127
181
  this.name = name;
128
182
  this.inputSchema = options?.inputSchema;
129
183
  this.outputSchema = options?.outputSchema;
184
+ this.credentialNames = options?.credentialNames ?? [];
185
+ this.authProfileName = options?.authProfileName;
186
+ this.authProfileRefresh = options?.authProfileRefresh;
130
187
  this.recoveryAction = options?.recoveryAction;
131
188
  this.handler = handler;
132
189
  }
@@ -135,7 +192,11 @@ export class LibrettoWorkflow<
135
192
  ctx: LibrettoWorkflowContext,
136
193
  input: unknown,
137
194
  ): Promise<z.infer<OutputSchema>> {
138
- const parsed = parseWorkflowInput(this.name, this.inputSchema, input);
195
+ const parsed = parseWorkflowInput(
196
+ this.name,
197
+ this.inputSchema,
198
+ mergeCredentialsIntoInput(input, this.credentialNames),
199
+ );
139
200
  const workflowContext =
140
201
  !this.recoveryAction
141
202
  ? ctx
@@ -154,6 +215,9 @@ export type ExportedLibrettoWorkflow = {
154
215
  readonly name: string;
155
216
  readonly inputSchema?: z.ZodType;
156
217
  readonly outputSchema?: z.ZodType;
218
+ readonly credentialNames: readonly string[];
219
+ readonly authProfileName?: string;
220
+ readonly authProfileRefresh?: boolean;
157
221
  readonly recoveryAction?: RecoveryAction;
158
222
  run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
159
223
  };
@@ -255,11 +319,18 @@ function getWorkflowConstructorOptions<
255
319
  ): {
256
320
  inputSchema?: InputSchema;
257
321
  outputSchema?: OutputSchema;
322
+ credentialNames: readonly string[];
323
+ authProfileName?: string;
324
+ authProfileRefresh?: boolean;
258
325
  recoveryAction?: RecoveryAction;
259
326
  } {
327
+ const authProfile = normalizeWorkflowAuthProfile(options.authProfile);
260
328
  return {
261
329
  inputSchema: options.input,
262
330
  outputSchema: options.output,
331
+ credentialNames: normalizeCredentialNames(options.credentials),
332
+ authProfileName: authProfile?.name,
333
+ authProfileRefresh: authProfile?.refresh,
263
334
  recoveryAction: options.recoveryAction,
264
335
  };
265
336
  }
@@ -320,3 +391,17 @@ export function workflow(
320
391
  maybeHandler,
321
392
  );
322
393
  }
394
+
395
+ function normalizeWorkflowAuthProfile(
396
+ value: LibrettoWorkflowAuthProfile | undefined,
397
+ ): { name: string; refresh?: boolean } | undefined {
398
+ if (!value) return undefined;
399
+ if (typeof value === "string") return { name: normalizeProfileName(value) };
400
+ const name = normalizeProfileName(value.name);
401
+ return {
402
+ name,
403
+ ...(value.refresh === undefined
404
+ ? {}
405
+ : { refresh: value.refresh }),
406
+ };
407
+ }