libretto 0.6.24 → 0.6.25

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 +83 -313
  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 +105 -422
  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,13 +1,27 @@
1
1
  import {
2
2
  chromium
3
3
  } from "playwright";
4
- import { existsSync, readFileSync, unlinkSync } from "node:fs";
5
- import { mkdir, writeFile } from "node:fs/promises";
6
- import { dirname, join } from "node:path";
4
+ import {
5
+ existsSync,
6
+ readFileSync,
7
+ unlinkSync
8
+ } from "node:fs";
7
9
  import { createServer } from "node:net";
10
+ import { join } from "node:path";
8
11
  import { isWindowsNamedPipePath } from "../../shared/ipc/socket-transport.js";
9
- import { getSessionProviderClosePath, PROFILES_DIR } from "./context.js";
12
+ import { getSessionProviderClosePath } from "./context.js";
10
13
  import { readLibrettoConfig } from "./config.js";
14
+ import {
15
+ captureAuthProfileStorageState,
16
+ parseAuthProfileSites
17
+ } from "../../shared/workflow/auth-profile-state.js";
18
+ import {
19
+ formatMissingLocalAuthProfileMessage,
20
+ getProfilePath,
21
+ hasProfile,
22
+ normalizeProfileName,
23
+ writeProfile
24
+ } from "./profiles.js";
11
25
  import {
12
26
  getCloudProviderApi,
13
27
  getProviderStartupTimeoutMs
@@ -80,12 +94,6 @@ function normalizeUrl(url) {
80
94
  function normalizeDomain(url) {
81
95
  return url.hostname.replace(/^www\./, "");
82
96
  }
83
- function getProfilePath(domain) {
84
- return join(PROFILES_DIR, `${domain}.json`);
85
- }
86
- function hasProfile(domain) {
87
- return existsSync(getProfilePath(domain));
88
- }
89
97
  async function tryConnectToCDP(endpoint, logger, timeoutMs = 5e3) {
90
98
  logger.info("cdp-connect-attempt", { endpoint, timeoutMs });
91
99
  try {
@@ -304,30 +312,34 @@ async function runOpen(rawUrl, headed, session, logger, options) {
304
312
  const port = await pickFreePort();
305
313
  const runLogPath = logFileForSession(session);
306
314
  const browserMode = headed ? "headed" : "headless";
307
- const authDomain = options?.authProfileDomain ? normalizeDomain(normalizeUrl(options.authProfileDomain)) : void 0;
308
- if (authDomain) {
309
- const authProfilePath = getProfilePath(authDomain);
310
- if (!existsSync(authProfilePath)) {
315
+ const authProfileName = options?.authProfileName ? normalizeProfileName(options.authProfileName) : void 0;
316
+ if (authProfileName) {
317
+ const authProfilePath = getProfilePath(authProfileName);
318
+ if (!hasProfile(authProfileName)) {
311
319
  throw new Error(
312
- `No saved auth profile for "${authDomain}". Save one first: libretto open https://${authDomain} --headed --session <name>, log in, then run: libretto save ${authDomain} --session <name>`
320
+ formatMissingLocalAuthProfileMessage({
321
+ profileName: authProfileName,
322
+ profilePath: authProfilePath,
323
+ session
324
+ })
313
325
  );
314
326
  }
315
327
  }
316
328
  const supportsSavedProfile = parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
317
- const domain = authDomain ?? (supportsSavedProfile ? normalizeDomain(parsedUrl) : void 0);
318
- const profilePath = domain ? getProfilePath(domain) : void 0;
319
- const useProfile = domain ? hasProfile(domain) : false;
329
+ const profileName = authProfileName ?? (supportsSavedProfile ? normalizeDomain(parsedUrl) : void 0);
330
+ const profilePath = profileName ? getProfilePath(profileName) : void 0;
331
+ const useProfile = profileName ? hasProfile(profileName) : false;
320
332
  logger.info("open-launching", {
321
333
  url,
322
334
  mode: browserMode,
323
335
  session,
324
336
  port,
325
- domain,
337
+ profileName,
326
338
  useProfile,
327
339
  profilePath: useProfile ? profilePath : void 0
328
340
  });
329
341
  if (useProfile) {
330
- console.log(`Loading saved profile for ${domain}`);
342
+ console.log(`Loading saved profile ${profileName}`);
331
343
  }
332
344
  console.log(`Launching ${browserMode} browser (session: ${session})...`);
333
345
  const { pid, socketPath: daemonSocketPath, client } = await DaemonClient.spawn({
@@ -449,67 +461,70 @@ async function runOpenWithProvider(rawUrl, providerName, session, logger, access
449
461
  });
450
462
  console.log(`Browser open (${providerName}): ${url}`);
451
463
  }
452
- async function runSave(urlOrDomain, session, logger) {
453
- logger.info("save-start", { urlOrDomain, session });
454
- const { browser, context, page } = await connect(session, logger);
455
- try {
456
- await new Promise((r) => setTimeout(r, 500));
457
- const domain = normalizeDomain(normalizeUrl(urlOrDomain));
458
- const profilePath = getProfilePath(domain);
459
- const cdpSession = await context.newCDPSession(page);
460
- const { cookies: rawCookies } = await cdpSession.send(
461
- "Network.getAllCookies"
464
+ async function runSave(profileName, session, logger, options = { sites: "" }) {
465
+ const normalizedProfileName = normalizeProfileName(profileName);
466
+ const sites = parseAuthProfileSites(options.sites);
467
+ if (sites.length === 0) {
468
+ throw new Error("Pass at least one site with --sites <site[,site]>.");
469
+ }
470
+ logger.info("save-start", { profileName: normalizedProfileName, session, sites });
471
+ const state = readSessionStateOrThrow(session);
472
+ if (!state.daemonSocketPath) {
473
+ throw new Error(
474
+ `Session "${session}" has no daemon socket. Close and reopen the session, then run libretto save again.`
462
475
  );
463
- const cookies = rawCookies.map((c) => {
464
- const cookie = { ...c };
465
- if (cookie.partitionKey && typeof cookie.partitionKey === "object") {
466
- delete cookie.partitionKey;
467
- }
468
- return cookie;
469
- });
470
- await cdpSession.detach();
471
- const origins = [];
472
- for (const ctx of browser.contexts()) {
473
- for (const pg of ctx.pages()) {
474
- try {
475
- const origin = new URL(pg.url()).origin;
476
- const localStorage = await pg.evaluate(() => {
477
- const items = [];
478
- for (let i = 0; i < window.localStorage.length; i++) {
479
- const key = window.localStorage.key(i);
480
- if (key) {
481
- items.push({
482
- name: key,
483
- value: window.localStorage.getItem(key) || ""
484
- });
485
- }
486
- }
487
- return items;
488
- });
489
- if (localStorage.length > 0) {
490
- origins.push({ origin, localStorage });
491
- }
492
- } catch {
493
- }
494
- }
495
- }
496
- const state = { cookies, origins };
497
- await mkdir(dirname(profilePath), { recursive: true });
498
- await writeFile(profilePath, JSON.stringify(state, null, 2));
476
+ }
477
+ const client = await DaemonClient.connect(state.daemonSocketPath);
478
+ try {
479
+ const storageState = await client.captureAuthProfileStorageState({ sites });
480
+ const profilePath = await writeProfile(normalizedProfileName, storageState);
499
481
  logger.info("save-success", {
500
- domain,
482
+ profileName: normalizedProfileName,
483
+ sites,
501
484
  profilePath,
502
- cookieCount: cookies.length,
503
- originCount: origins.length
485
+ cookieCount: storageState.cookies?.length ?? 0,
486
+ originCount: storageState.origins?.length ?? 0
504
487
  });
505
- console.log(`Profile saved for ${domain}`);
488
+ console.log(`Profile saved: ${normalizedProfileName}`);
506
489
  console.log(` Location: ${profilePath}`);
507
- console.log(` Cookies: ${cookies.length}, Origins: ${origins.length}`);
490
+ console.log(` Sites: ${sites.join(", ")}`);
491
+ console.log(
492
+ ` Cookies: ${storageState.cookies?.length ?? 0}, Origins: ${storageState.origins?.length ?? 0}`
493
+ );
508
494
  } catch (err) {
509
- logger.error("save-error", { error: err, urlOrDomain, session });
495
+ logger.error("save-error", { error: err, profileName, session, sites });
510
496
  throw err;
511
497
  } finally {
512
- disconnectBrowser(browser, logger, session);
498
+ client.destroy();
499
+ }
500
+ }
501
+ async function runFetchChromeProfile(profileName, cdpUrl, logger, options) {
502
+ const normalizedProfileName = normalizeProfileName(profileName);
503
+ const sites = parseAuthProfileSites(options.sites);
504
+ if (sites.length === 0) {
505
+ throw new Error("Pass at least one site with --sites <site[,site]>.");
506
+ }
507
+ logger.info("fetch-chrome-profile-start", {
508
+ profileName: normalizedProfileName,
509
+ cdpUrl,
510
+ sites
511
+ });
512
+ const browser = await chromium.connectOverCDP(cdpUrl);
513
+ try {
514
+ const context = browser.contexts()[0];
515
+ if (!context) {
516
+ throw new Error("Connected Chrome instance has no browser context.");
517
+ }
518
+ const state = await captureAuthProfileStorageState(context, sites);
519
+ const profilePath = await writeProfile(normalizedProfileName, state);
520
+ console.log(`Profile fetched: ${normalizedProfileName}`);
521
+ console.log(` Location: ${profilePath}`);
522
+ console.log(` Sites: ${sites.join(", ")}`);
523
+ console.log(
524
+ ` Cookies: ${state.cookies?.length ?? 0}, Origins: ${state.origins?.length ?? 0}`
525
+ );
526
+ } finally {
527
+ disconnectBrowser(browser, logger);
513
528
  }
514
529
  }
515
530
  async function runClose(session, logger) {
@@ -1007,9 +1022,7 @@ function getScreenshotBaseName(title) {
1007
1022
  export {
1008
1023
  connect,
1009
1024
  disconnectBrowser,
1010
- getProfilePath,
1011
1025
  getScreenshotBaseName,
1012
- hasProfile,
1013
1026
  normalizeDomain,
1014
1027
  normalizeUrl,
1015
1028
  resolvePath,
@@ -1017,6 +1030,7 @@ export {
1017
1030
  runClose,
1018
1031
  runCloseAll,
1019
1032
  runConnect,
1033
+ runFetchChromeProfile,
1020
1034
  runOpen,
1021
1035
  runOpenWithProvider,
1022
1036
  runPages,
@@ -23,13 +23,13 @@ import {
23
23
  import {
24
24
  getDaemonSocketPath
25
25
  } from "./ipc.js";
26
- import { wrapPageForActionLogging } from "../telemetry.js";
26
+ import { wrapPageForActionLogging } from "../session-logs.js";
27
27
  import {
28
+ formatMissingLocalAuthProfileMessage,
28
29
  getProfilePath,
29
30
  hasProfile,
30
- normalizeDomain,
31
- normalizeUrl
32
- } from "../browser.js";
31
+ normalizeProfileName
32
+ } from "../profiles.js";
33
33
  import { handlePages } from "./pages.js";
34
34
  import { handleExec, handleReadonlyExec } from "./exec.js";
35
35
  import { DaemonExecRepl } from "./exec-repl.js";
@@ -48,6 +48,7 @@ import {
48
48
  } from "../workflow-runtime.js";
49
49
  import { WorkflowController } from "../workflow-runner/runner.js";
50
50
  import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
51
+ import { captureAuthProfileStorageState } from "../../../shared/workflow/auth-profile-state.js";
51
52
  function isOperationalPage(page) {
52
53
  const url = page.url();
53
54
  return !url.startsWith("devtools://") && !url.startsWith("chrome-error://");
@@ -68,26 +69,14 @@ class UserFacingStartupError extends Error {
68
69
  this.name = "UserFacingStartupError";
69
70
  }
70
71
  }
71
- function getMissingLocalAuthProfileError(args) {
72
- return [
73
- `Local auth profile not found for domain "${args.normalizedDomain}".`,
74
- `Expected profile file: ${args.profilePath}`,
75
- "To create it:",
76
- ` 1. libretto open https://${args.normalizedDomain} --headed --session ${args.session}`,
77
- " 2. Log in manually in the browser window.",
78
- ` 3. libretto save ${args.normalizedDomain} --session ${args.session}`
79
- ].join("\n");
80
- }
81
72
  function resolveAuthProfileStorageStatePath(args) {
82
- if (!args.authProfileDomain) return void 0;
83
- const normalizedDomain = normalizeDomain(
84
- normalizeUrl(args.authProfileDomain)
85
- );
86
- const profilePath = getProfilePath(normalizedDomain);
87
- if (!hasProfile(normalizedDomain)) {
73
+ if (!args.authProfileName) return void 0;
74
+ const profileName = normalizeProfileName(args.authProfileName);
75
+ const profilePath = getProfilePath(profileName);
76
+ if (!hasProfile(profileName)) {
88
77
  throw new UserFacingStartupError(
89
- getMissingLocalAuthProfileError({
90
- normalizedDomain,
78
+ formatMissingLocalAuthProfileMessage({
79
+ profileName,
91
80
  profilePath,
92
81
  session: args.session
93
82
  })
@@ -253,6 +242,10 @@ class BrowserDaemon {
253
242
  // ── Launch mode ────────────────────────────────────────────────────
254
243
  static async launchBrowser(args) {
255
244
  const { session, browser: config } = args;
245
+ const storageStatePath = config.storageStatePath ?? resolveAuthProfileStorageStatePath({
246
+ authProfileName: args.workflow?.authProfileName,
247
+ session
248
+ });
256
249
  const windowPositionArg = config.windowPosition ? `--window-position=${config.windowPosition.x},${config.windowPosition.y}` : void 0;
257
250
  const browser = await chromium.launch({
258
251
  headless: !config.headed,
@@ -264,10 +257,6 @@ class BrowserDaemon {
264
257
  ...windowPositionArg ? [windowPositionArg] : []
265
258
  ]
266
259
  });
267
- const storageStatePath = config.storageStatePath ?? resolveAuthProfileStorageStatePath({
268
- authProfileDomain: args.workflow?.authProfileDomain,
269
- session
270
- });
271
260
  const context = await browser.newContext({
272
261
  ...storageStatePath ? { storageState: storageStatePath } : {},
273
262
  viewport: {
@@ -331,7 +320,10 @@ class BrowserDaemon {
331
320
  getProviderSession: () => providerSession
332
321
  });
333
322
  try {
334
- providerSession = await provider.createSession();
323
+ providerSession = await provider.createSession({
324
+ authProfileName: config.authProfileName,
325
+ authProfilePersist: config.authProfilePersist
326
+ });
335
327
  const browser = await chromium.connectOverCDP(
336
328
  providerSession.cdpEndpoint
337
329
  );
@@ -471,6 +463,9 @@ class BrowserDaemon {
471
463
  pages: () => this.withRequestTimeout(() => handlePages(this.pageById, this.page)),
472
464
  exec: (args) => this.runExec(args),
473
465
  readonlyExec: (args) => this.runReadonlyExec(args),
466
+ captureAuthProfileStorageState: (args) => this.withRequestTimeout(
467
+ () => captureAuthProfileStorageState(this.context, args.sites)
468
+ ),
474
469
  snapshot: (args) => this.runSnapshot(args),
475
470
  getWorkflowStatus: () => this.getWorkflowStatus(),
476
471
  resumeWorkflow: () => this.resumeWorkflow(),
@@ -584,6 +579,7 @@ class BrowserDaemon {
584
579
  page: this.page,
585
580
  context: this.context,
586
581
  logger: this.logger,
582
+ refreshLocalAuthProfiles: !this.externallyManaged,
587
583
  onLog: (event) => {
588
584
  void this.broadcast("workflowOutput", event);
589
585
  },
@@ -680,36 +676,52 @@ async function main() {
680
676
  const config = JSON.parse(process.argv[2]);
681
677
  const headed = config.browser.kind === "launch" ? config.browser.headed : false;
682
678
  let loadedWorkflow;
679
+ let workflowConfig = config.workflow;
680
+ let browserConfig = config.browser;
683
681
  if (config.workflow) {
684
682
  try {
685
683
  loadedWorkflow = await loadDefaultWorkflow(
686
684
  getAbsoluteIntegrationPath(config.workflow.integrationPath)
687
685
  );
688
686
  validateWorkflowInput(loadedWorkflow, config.workflow.params ?? {});
687
+ const authProfileName = loadedWorkflow.authProfileName;
688
+ const authProfilePersist = loadedWorkflow.authProfileRefresh === true;
689
+ workflowConfig = {
690
+ ...config.workflow,
691
+ authProfileName,
692
+ authProfilePersist
693
+ };
694
+ if (config.browser.kind === "provider") {
695
+ browserConfig = {
696
+ ...config.browser,
697
+ authProfileName,
698
+ authProfilePersist
699
+ };
700
+ }
689
701
  } catch (error) {
690
702
  throw new UserFacingStartupError(
691
703
  error instanceof Error ? error.message : String(error)
692
704
  );
693
705
  }
694
706
  }
695
- const daemon = config.browser.kind === "provider" ? await BrowserDaemon.connectToProvider({
707
+ const daemon = browserConfig.kind === "provider" ? await BrowserDaemon.connectToProvider({
696
708
  session: config.session,
697
709
  experiments: config.experiments,
698
- browser: config.browser
699
- }) : config.browser.kind === "connect" ? await BrowserDaemon.connectToEndpoint({
710
+ browser: browserConfig
711
+ }) : browserConfig.kind === "connect" ? await BrowserDaemon.connectToEndpoint({
700
712
  session: config.session,
701
713
  experiments: config.experiments,
702
- browser: config.browser
714
+ browser: browserConfig
703
715
  }) : await BrowserDaemon.launchBrowser({
704
716
  session: config.session,
705
717
  experiments: config.experiments,
706
- browser: config.browser,
707
- workflow: config.workflow
718
+ browser: browserConfig,
719
+ workflow: workflowConfig
708
720
  });
709
- if (config.workflow) {
721
+ if (workflowConfig) {
710
722
  void waitForSessionState(config.session).then(
711
723
  () => daemon.startWorkflow({
712
- workflow: config.workflow,
724
+ workflow: workflowConfig,
713
725
  headed,
714
726
  loadedWorkflow
715
727
  })
@@ -232,6 +232,9 @@ class DaemonClient {
232
232
  async readonlyExec(args) {
233
233
  return this.ipc.call.readonlyExec(args);
234
234
  }
235
+ async captureAuthProfileStorageState(args) {
236
+ return this.ipc.call.captureAuthProfileStorageState(args);
237
+ }
235
238
  async snapshot(args = {}) {
236
239
  return this.ipc.call.snapshot(args);
237
240
  }
@@ -20,6 +20,7 @@ import {
20
20
  getWorkflowsFromModuleExports,
21
21
  LIBRETTO_WORKFLOW_BRAND
22
22
  } from "../../shared/workflow/workflow.js";
23
+ import { normalizeCredentialNames } from "../../shared/workflow/credentials.js";
23
24
  const DEFAULT_RUNTIME_EXTERNALS = [
24
25
  "libretto",
25
26
  "playwright",
@@ -27,6 +28,7 @@ const DEFAULT_RUNTIME_EXTERNALS = [
27
28
  "chromium-bidi"
28
29
  ];
29
30
  const BUILT_IN_MANIFEST_DEPENDENCIES = ["libretto"];
31
+ const DEPLOY_METADATA_FILENAME = ".libretto-workflows.json";
30
32
  const SOURCE_FILE_EXTENSIONS = [
31
33
  "",
32
34
  ".ts",
@@ -430,6 +432,12 @@ function writeDeployManifest(args) {
430
432
  ) + "\n"
431
433
  );
432
434
  }
435
+ function writeDeployMetadata(args) {
436
+ writeFileSync(
437
+ join(args.outputDir, DEPLOY_METADATA_FILENAME),
438
+ JSON.stringify({ workflows: args.workflows }, null, 2) + "\n"
439
+ );
440
+ }
433
441
  function shouldVendorCurrentLibretto(versionSpec) {
434
442
  return versionSpec.startsWith("file:") || versionSpec.startsWith("link:") || versionSpec.startsWith("workspace:") || versionSpec.startsWith("portal:") || versionSpec.includes("&path:");
435
443
  }
@@ -498,11 +506,15 @@ function createExternalDiscoveryStub() {
498
506
  }
499
507
  });
500
508
  }
501
- function createDiscoveryLibrettoModule(workflowNames) {
509
+ function createDiscoveryLibrettoModule(workflowsByName) {
502
510
  const moduleShape = {
503
511
  LIBRETTO_WORKFLOW_BRAND,
504
- workflow: (name) => {
505
- workflowNames.add(name);
512
+ workflow: (name, definitionOrHandler) => {
513
+ workflowsByName.set(name, {
514
+ name,
515
+ ...extractDiscoveryCredentialMetadata(definitionOrHandler),
516
+ ...extractDiscoveryAuthProfileMetadata(definitionOrHandler)
517
+ });
506
518
  return {
507
519
  [LIBRETTO_WORKFLOW_BRAND]: true,
508
520
  name,
@@ -524,14 +536,37 @@ function createDiscoveryLibrettoModule(workflowNames) {
524
536
  }
525
537
  });
526
538
  }
527
- function discoverBundledWorkflowNames(args) {
539
+ function extractDiscoveryCredentialMetadata(definitionOrHandler) {
540
+ if (!definitionOrHandler || typeof definitionOrHandler !== "object" || !("credentials" in definitionOrHandler)) {
541
+ return { credentialNames: [] };
542
+ }
543
+ const rawCredentials = definitionOrHandler.credentials;
544
+ return {
545
+ credentialNames: Array.isArray(rawCredentials) ? normalizeCredentialNames(rawCredentials) : []
546
+ };
547
+ }
548
+ function extractDiscoveryAuthProfileMetadata(definitionOrHandler) {
549
+ if (!definitionOrHandler || typeof definitionOrHandler !== "object" || !("authProfile" in definitionOrHandler)) {
550
+ return {};
551
+ }
552
+ const authProfile = definitionOrHandler.authProfile;
553
+ if (typeof authProfile === "string") return { authProfileName: authProfile };
554
+ if (!authProfile || typeof authProfile !== "object") return {};
555
+ const record = authProfile;
556
+ if (typeof record.name !== "string") return {};
557
+ return {
558
+ authProfileName: record.name,
559
+ ...typeof record.refresh === "boolean" ? { authProfileRefresh: record.refresh } : {}
560
+ };
561
+ }
562
+ function discoverBundledWorkflows(args) {
528
563
  const discoveryPath = join(
529
564
  args.absSourceDir,
530
565
  `.libretto-deploy-discovery-${process.pid}-${Date.now()}.cjs`
531
566
  );
532
567
  const originalRequire = Module.prototype.require;
533
- const workflowNames = /* @__PURE__ */ new Set();
534
- const discoveryLibrettoModule = createDiscoveryLibrettoModule(workflowNames);
568
+ const workflowsByName = /* @__PURE__ */ new Map();
569
+ const discoveryLibrettoModule = createDiscoveryLibrettoModule(workflowsByName);
535
570
  let loadedModuleExports = null;
536
571
  try {
537
572
  writeFileSync(discoveryPath, args.bundleBuffer);
@@ -556,10 +591,10 @@ ${formatBuildError(error)}`
556
591
  delete require2.cache?.[discoveryPath];
557
592
  rmSync(discoveryPath, { force: true });
558
593
  }
559
- const discoveredWorkflowNames = [...workflowNames].sort(
560
- (left, right) => left.localeCompare(right)
594
+ const discoveredWorkflows = [...workflowsByName.values()].sort(
595
+ (left, right) => left.name.localeCompare(right.name)
561
596
  );
562
- if (discoveredWorkflowNames.length === 0) {
597
+ if (discoveredWorkflows.length === 0) {
563
598
  throw new Error(
564
599
  `No workflows were found in ${args.absEntryPoint}. Import the workflow files you want to deploy from the entry point, or export a workflow directly from it.`
565
600
  );
@@ -569,15 +604,15 @@ ${formatBuildError(error)}`
569
604
  (workflow) => workflow.name
570
605
  )
571
606
  );
572
- const nonExportedWorkflowNames = discoveredWorkflowNames.filter(
573
- (name) => !exportedWorkflowNames.has(name)
607
+ const nonExportedWorkflowNames = discoveredWorkflows.filter(
608
+ (workflow) => !exportedWorkflowNames.has(workflow.name)
574
609
  );
575
610
  if (nonExportedWorkflowNames.length > 0) {
576
611
  throw new Error(
577
- `Workflows discovered in ${args.absEntryPoint} must be exported from the deploy entry point. Re-export them from the entry point or export them through a \`workflows\` object. Non-exported workflows: ${nonExportedWorkflowNames.join(", ")}`
612
+ `Workflows discovered in ${args.absEntryPoint} must be exported from the deploy entry point. Re-export them from the entry point or export them through a \`workflows\` object. Non-exported workflows: ${nonExportedWorkflowNames.map((workflow) => workflow.name).join(", ")}`
578
613
  );
579
614
  }
580
- return discoveredWorkflowNames;
615
+ return discoveredWorkflows;
581
616
  }
582
617
  function createBootstrapSource(args) {
583
618
  const bundleHash = createHash("sha256").update(args.bundleBuffer).digest("hex").slice(0, 16);
@@ -585,8 +620,12 @@ function createBootstrapSource(args) {
585
620
  "base64"
586
621
  );
587
622
  const outputPrefix = `${normalizePackageName(args.deploymentName)}-`;
588
- const exportLines = args.workflowNames.map(
589
- (name, index) => `export const ${getGeneratedWorkflowExportName(index)} = createWorkflowProxy(${JSON.stringify(name)});`
623
+ const exportLines = args.workflows.map(
624
+ (workflow, index) => `export const ${getGeneratedWorkflowExportName(index)} = createWorkflowProxy(${JSON.stringify(workflow.name)}, ${JSON.stringify({
625
+ credentialNames: workflow.credentialNames,
626
+ authProfileName: workflow.authProfileName,
627
+ authProfileRefresh: workflow.authProfileRefresh
628
+ })});`
590
629
  ).join("\n");
591
630
  return `import { createRequire } from "node:module";
592
631
  import { existsSync, writeFileSync } from "node:fs";
@@ -616,8 +655,8 @@ function ensureBundleFile() {
616
655
  return BUNDLE_FILENAME;
617
656
  }
618
657
 
619
- function createWorkflowProxy(workflowName) {
620
- return workflow(workflowName, async (ctx, input) => {
658
+ function createWorkflowProxy(workflowName, metadata) {
659
+ const handler = async (ctx, input) => {
621
660
  const impl = nativeRequire(ensureBundleFile());
622
661
  const target = getWorkflowFromModuleExports(impl, workflowName);
623
662
  if (!target || typeof target.run !== "function") {
@@ -626,6 +665,26 @@ function createWorkflowProxy(workflowName) {
626
665
  );
627
666
  }
628
667
  return await target.run(ctx, input);
668
+ };
669
+
670
+ if (!metadata?.authProfileName) {
671
+ return workflow(workflowName, {
672
+ credentials: Array.isArray(metadata?.credentialNames)
673
+ ? metadata.credentialNames
674
+ : [],
675
+ handler,
676
+ });
677
+ }
678
+
679
+ return workflow(workflowName, {
680
+ credentials: Array.isArray(metadata.credentialNames)
681
+ ? metadata.credentialNames
682
+ : [],
683
+ authProfile: {
684
+ name: metadata.authProfileName,
685
+ ...(typeof metadata.authProfileRefresh === "boolean" ? { refresh: metadata.authProfileRefresh } : {}),
686
+ },
687
+ handler,
629
688
  });
630
689
  }
631
690
 
@@ -657,7 +716,7 @@ async function writeBundledDeployEntrypoint(args) {
657
716
  "Bundler did not produce a deployment implementation file."
658
717
  );
659
718
  }
660
- const workflowNames = discoverBundledWorkflowNames({
719
+ const workflows = discoverBundledWorkflows({
661
720
  absEntryPoint: args.absEntryPoint,
662
721
  absSourceDir: args.absSourceDir,
663
722
  bundleBuffer: Buffer.from(bundledImplementation.contents),
@@ -668,9 +727,10 @@ async function writeBundledDeployEntrypoint(args) {
668
727
  createBootstrapSource({
669
728
  bundleBuffer: Buffer.from(bundledImplementation.contents),
670
729
  deploymentName: args.deploymentName,
671
- workflowNames
730
+ workflows
672
731
  })
673
732
  );
733
+ return workflows;
674
734
  } catch (error) {
675
735
  throw new Error(
676
736
  `Failed to bundle deploy entry point ${args.absEntryPoint}.
@@ -694,7 +754,7 @@ async function createHostedDeployPackage(args) {
694
754
  const workspacePackages = discoverWorkspacePackages(absSourceDir);
695
755
  let callerOwnsTempRoot = false;
696
756
  try {
697
- await writeBundledDeployEntrypoint({
757
+ const workflows = await writeBundledDeployEntrypoint({
698
758
  absEntryPoint,
699
759
  absSourceDir,
700
760
  deploymentName: args.deploymentName,
@@ -712,13 +772,15 @@ async function createHostedDeployPackage(args) {
712
772
  outputDir,
713
773
  sourceDir: absSourceDir
714
774
  });
775
+ writeDeployMetadata({ outputDir, workflows });
715
776
  callerOwnsTempRoot = true;
716
777
  return {
717
778
  cleanup: () => {
718
779
  rmSync(tempRoot, { force: true, recursive: true });
719
780
  },
720
781
  entryPoint: "index.js",
721
- outputDir
782
+ outputDir,
783
+ workflows
722
784
  };
723
785
  } finally {
724
786
  if (!callerOwnsTempRoot) {
@@ -735,7 +797,8 @@ async function buildHostedDeployTarball(args) {
735
797
  });
736
798
  return {
737
799
  entryPoint: deployPackage.entryPoint,
738
- source: readFileSync(tarPath).toString("base64")
800
+ source: readFileSync(tarPath).toString("base64"),
801
+ workflows: deployPackage.workflows
739
802
  };
740
803
  } finally {
741
804
  deployPackage.cleanup();