@mcp-abap-adt/auth-broker 1.0.8 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,15 +60,15 @@ var __importStar = (this && this.__importStar) || (function () {
60
60
  };
61
61
  })();
62
62
  Object.defineProperty(exports, "__esModule", { value: true });
63
- const node_readline_1 = require("node:readline");
64
63
  const fs = __importStar(require("fs"));
65
64
  const path = __importStar(require("path"));
66
65
  // Use require for CommonJS dist files with absolute path
67
66
  const distPath = path.resolve(__dirname, '..', 'index.js');
68
67
  const { AuthBroker } = require(distPath);
69
- const logger_1 = require("@mcp-abap-adt/logger");
70
68
  const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
71
69
  const auth_stores_1 = require("@mcp-abap-adt/auth-stores");
70
+ const logger_1 = require("@mcp-abap-adt/logger");
71
+ const mcpSsoConfig_1 = require("./mcpSsoConfig");
72
72
  function getVersion() {
73
73
  try {
74
74
  const candidates = [
@@ -112,7 +112,7 @@ function showHelp() {
112
112
  console.log(' --service-url <url> Service URL (ABAP: SAP URL, XSUAA: MCP URL)');
113
113
  console.log(' --config <path> JSON config file (SSO provider config)');
114
114
  console.log(' --browser <browser> Browser: auto|none|system|chrome|edge|firefox');
115
- console.log(' --redirect-port <port> Redirect port for browser flows (default: 3001)');
115
+ console.log(' --redirect-port <port> Redirect port for browser flows (default: from auth-providers, currently 61001)');
116
116
  console.log(' --redirect-uri <uri> Custom redirect URI (OOB/manual code flows)');
117
117
  console.log('');
118
118
  console.log('OIDC Options:');
@@ -158,18 +158,6 @@ function parseScopes(value) {
158
158
  .filter(Boolean);
159
159
  return parts.length > 0 ? parts : undefined;
160
160
  }
161
- function readManualInput(prompt) {
162
- const rl = (0, node_readline_1.createInterface)({
163
- input: process.stdin,
164
- output: process.stdout,
165
- });
166
- return new Promise((resolve) => {
167
- rl.question(prompt, (answer) => {
168
- rl.close();
169
- resolve(answer.trim());
170
- });
171
- });
172
- }
173
161
  function createCliLogger(prefix = 'SSO') {
174
162
  const isEnabled = () => {
175
163
  if (process.env.DEBUG_SSO === 'false' ||
@@ -520,190 +508,6 @@ function resolveSamlTokenAlias(metadataXml) {
520
508
  const match = metadataXml.match(regex);
521
509
  return match?.[1];
522
510
  }
523
- function normalizeProviderConfig(raw) {
524
- if (!raw || typeof raw !== 'object') {
525
- return null;
526
- }
527
- if (raw.provider) {
528
- return raw.provider;
529
- }
530
- if (raw.protocol && raw.flow) {
531
- const { protocol, flow, config, ...rest } = raw;
532
- return {
533
- protocol,
534
- flow,
535
- config: config ?? rest,
536
- };
537
- }
538
- return null;
539
- }
540
- function mergeConfig(target, source) {
541
- const result = { ...target };
542
- for (const [key, value] of Object.entries(source)) {
543
- if (value !== undefined) {
544
- result[key] = value;
545
- }
546
- }
547
- return result;
548
- }
549
- function buildOidcConfig(options) {
550
- const scopeList = options.scopes;
551
- const tokenEndpoint = options.tokenEndpoint ||
552
- (options.uaaUrl
553
- ? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
554
- : undefined);
555
- const passcode = options.passcode;
556
- const username = options.username || (passcode ? 'passcode' : undefined);
557
- const password = options.password || passcode;
558
- return {
559
- issuerUrl: options.issuerUrl,
560
- authorizationEndpoint: options.authorizationEndpoint,
561
- tokenEndpoint,
562
- deviceAuthorizationEndpoint: options.deviceAuthorizationEndpoint,
563
- clientId: options.clientId,
564
- clientSecret: options.clientSecret,
565
- scopes: scopeList,
566
- scope: options.scope,
567
- authorizationCode: options.code,
568
- username,
569
- password,
570
- subjectToken: options.subjectToken,
571
- subjectTokenType: options.subjectTokenType ||
572
- 'urn:ietf:params:oauth:token-type:access_token',
573
- audience: options.audience,
574
- actorToken: options.actorToken,
575
- actorTokenType: options.actorTokenType,
576
- browser: options.browser,
577
- redirectPort: options.redirectPort,
578
- redirectUri: options.redirectUri,
579
- };
580
- }
581
- function buildSamlConfig(options) {
582
- const assertionFlow = options.assertionFlow || (options.assertion ? 'assertion' : 'browser');
583
- const tokenUrl = options.tokenEndpoint ||
584
- (options.uaaUrl
585
- ? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
586
- : undefined);
587
- const assertionProvider = options.assertion
588
- ? async () => options.assertion
589
- : assertionFlow === 'assertion'
590
- ? async () => readManualInput('Paste SAMLResponse: ')
591
- : undefined;
592
- const cookieProvider = async (samlResponse) => {
593
- if (options.cookie) {
594
- return options.cookie;
595
- }
596
- if (assertionFlow === 'assertion') {
597
- return `SAMLResponse=${samlResponse}`;
598
- }
599
- return readManualInput('Paste session cookies: ');
600
- };
601
- return {
602
- idpSsoUrl: options.idpSsoUrl,
603
- spEntityId: options.spEntityId,
604
- acsUrl: options.acsUrl,
605
- relayState: options.relayState,
606
- assertionFlow,
607
- assertionProvider,
608
- tokenUrl,
609
- uaaUrl: options.uaaUrl,
610
- clientId: options.clientId,
611
- clientSecret: options.clientSecret,
612
- browser: options.browser,
613
- redirectPort: options.redirectPort,
614
- cookieProvider,
615
- };
616
- }
617
- function buildProviderConfig(options, existingAuth, existingConn, fileConfig) {
618
- let configFromCli = null;
619
- if (options.protocol && options.flow) {
620
- if (options.protocol === 'oidc') {
621
- switch (options.flow) {
622
- case 'browser':
623
- configFromCli = {
624
- protocol: 'oidc',
625
- flow: 'browser',
626
- config: buildOidcConfig(options),
627
- };
628
- break;
629
- case 'device':
630
- configFromCli = {
631
- protocol: 'oidc',
632
- flow: 'device',
633
- config: buildOidcConfig(options),
634
- };
635
- break;
636
- case 'password':
637
- configFromCli = {
638
- protocol: 'oidc',
639
- flow: 'password',
640
- config: buildOidcConfig(options),
641
- };
642
- break;
643
- case 'token_exchange':
644
- configFromCli = {
645
- protocol: 'oidc',
646
- flow: 'token_exchange',
647
- config: buildOidcConfig(options),
648
- };
649
- break;
650
- default:
651
- throw new Error(`Unsupported OIDC flow: ${options.flow}`);
652
- }
653
- }
654
- else if (options.protocol === 'saml2') {
655
- switch (options.flow) {
656
- case 'bearer':
657
- configFromCli = {
658
- protocol: 'saml2',
659
- flow: 'bearer',
660
- config: buildSamlConfig(options),
661
- };
662
- break;
663
- case 'pure':
664
- configFromCli = {
665
- protocol: 'saml2',
666
- flow: 'pure',
667
- config: buildSamlConfig(options),
668
- };
669
- break;
670
- default:
671
- throw new Error(`Unsupported SAML flow: ${options.flow}`);
672
- }
673
- }
674
- }
675
- const base = fileConfig ?? configFromCli;
676
- if (!base) {
677
- throw new Error('Provider config is missing. Use --config or --protocol/--flow options.');
678
- }
679
- let result = base;
680
- if (options.protocol) {
681
- result = { ...result, protocol: options.protocol };
682
- }
683
- if (options.flow) {
684
- result = { ...result, flow: options.flow };
685
- }
686
- if (configFromCli) {
687
- result = {
688
- ...result,
689
- config: mergeConfig(result.config || {}, configFromCli.config || {}),
690
- };
691
- }
692
- const accessToken = existingConn?.authorizationToken;
693
- const refreshToken = existingAuth?.refreshToken;
694
- if ((result.protocol === 'oidc' ||
695
- (result.protocol === 'saml2' && result.flow === 'bearer')) &&
696
- (accessToken || refreshToken)) {
697
- result = {
698
- ...result,
699
- config: mergeConfig(result.config || {}, {
700
- accessToken,
701
- refreshToken,
702
- }),
703
- };
704
- }
705
- return result;
706
- }
707
511
  async function main() {
708
512
  const options = parseArgs();
709
513
  if (!options) {
@@ -768,12 +572,21 @@ async function main() {
768
572
  process.exit(1);
769
573
  }
770
574
  const raw = JSON.parse(fs.readFileSync(resolvedConfigPath, 'utf8'));
771
- providerConfigFromFile = normalizeProviderConfig(raw);
575
+ providerConfigFromFile = (0, mcpSsoConfig_1.normalizeProviderConfig)(raw);
772
576
  if (!providerConfigFromFile) {
773
577
  console.error(`❌ Config file does not contain provider config`);
774
578
  process.exit(1);
775
579
  }
776
580
  }
581
+ // Merge the file into `options` *before* anything downstream reads
582
+ // options.protocol/flow or builds a strategy from them — a run driven by
583
+ // --config alone must reach exactly the same validation and
584
+ // strategy-building code a --protocol/--flow run does, or a field the file
585
+ // carries (including a legacy one 2.0.0 removed) can reach the provider
586
+ // untouched with no error and no warning. CLI flags already parsed above
587
+ // are left alone; the file only fills what they didn't set. A no-op when
588
+ // --config wasn't given.
589
+ (0, mcpSsoConfig_1.applyFileConfig)(options, providerConfigFromFile);
777
590
  if (options.serviceKeyPath) {
778
591
  const resolvedServiceKeyPath = path.resolve(options.serviceKeyPath);
779
592
  const serviceKeyDir = path.dirname(resolvedServiceKeyPath);
@@ -836,7 +649,7 @@ async function main() {
836
649
  }
837
650
  if (options.protocol === 'oidc' && options.flow === 'password') {
838
651
  if (!options.passcode && !options.password) {
839
- options.passcode = await readManualInput('Paste passcode: ');
652
+ options.passcode = await (0, mcpSsoConfig_1.readManualInput)('Paste passcode: ');
840
653
  }
841
654
  }
842
655
  const tempSessionDir = path.join(path.dirname(resolvedOutputPath), '.tmp');
@@ -893,7 +706,7 @@ async function main() {
893
706
  refreshToken: existingAuth?.refreshToken,
894
707
  });
895
708
  }
896
- const providerConfig = buildProviderConfig(options, existingAuth, existingConn, providerConfigFromFile);
709
+ const providerConfig = (0, mcpSsoConfig_1.buildProviderConfig)(options, existingAuth, existingConn);
897
710
  const providerConfigWithLogger = providerConfig.config
898
711
  ? {
899
712
  ...providerConfig,
@@ -0,0 +1,398 @@
1
+ "use strict";
2
+ /**
3
+ * Pure config-building logic for `mcp-sso`, split out from bin/mcp-sso.ts so
4
+ * it can be imported by tests directly.
5
+ *
6
+ * bin/mcp-sso.ts itself cannot be `import`ed safely: it `require()`s the
7
+ * compiled `dist/index.js` at module load time (assuming it is always run
8
+ * from `dist/bin/`) and calls `main()` at the bottom of the file. Neither is
9
+ * true when a test loads the TypeScript source directly, so anything that
10
+ * needs unit coverage — building an `SsoProviderConfig` from CLI options and
11
+ * an optional `--config` file, and merging the two — lives here instead.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.INTERACTIVE_LOGIN_TIMEOUT_MS = void 0;
15
+ exports.readManualInput = readManualInput;
16
+ exports.normalizeProviderConfig = normalizeProviderConfig;
17
+ exports.applyFileConfig = applyFileConfig;
18
+ exports.buildOidcBrowserConfig = buildOidcBrowserConfig;
19
+ exports.buildOidcDeviceConfig = buildOidcDeviceConfig;
20
+ exports.buildOidcPasswordConfig = buildOidcPasswordConfig;
21
+ exports.buildOidcTokenExchangeConfig = buildOidcTokenExchangeConfig;
22
+ exports.buildSamlBearerConfig = buildSamlBearerConfig;
23
+ exports.buildSamlPureConfig = buildSamlPureConfig;
24
+ exports.buildProviderConfig = buildProviderConfig;
25
+ const node_readline_1 = require("node:readline");
26
+ const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
27
+ /**
28
+ * A person completes these logins at a browser; the library's own default
29
+ * (30s) is sized for an unattended caller instead.
30
+ */
31
+ exports.INTERACTIVE_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
32
+ function readManualInput(prompt) {
33
+ const rl = (0, node_readline_1.createInterface)({
34
+ input: process.stdin,
35
+ output: process.stdout,
36
+ });
37
+ return new Promise((resolve) => {
38
+ rl.question(prompt, (answer) => {
39
+ rl.close();
40
+ resolve(answer.trim());
41
+ });
42
+ });
43
+ }
44
+ function normalizeProviderConfig(raw) {
45
+ if (!raw || typeof raw !== 'object') {
46
+ return null;
47
+ }
48
+ if (raw.provider) {
49
+ return raw.provider;
50
+ }
51
+ if (raw.protocol && raw.flow) {
52
+ const { protocol, flow, config, ...rest } = raw;
53
+ return {
54
+ protocol,
55
+ flow,
56
+ config: config ?? rest,
57
+ };
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Pre-2.0.0 provider config fields that named a JavaScript function
63
+ * (`() => Promise<string>` or similar) rather than a value. JSON cannot
64
+ * carry a function, so a `--config` file can never legitimately supply one —
65
+ * if a key like this shows up, the file is stale in a way no conversion can
66
+ * fix. Refuse rather than silently drop it, and say what to write instead.
67
+ */
68
+ const UNSUPPORTED_LEGACY_CONFIG_FIELDS = {
69
+ authorizationCodeProvider: "'authorizationCodeProvider' (a function) cannot be expressed in a --config file. Use --code for a value you already hold, or drive the OIDC browser flow interactively without it.",
70
+ assertionProvider: "'assertionProvider' (a function) cannot be expressed in a --config file. Use --assertion for a value you already hold, or --assertion-flow to choose how it is obtained.",
71
+ manualInput: "'manualInput' (a function) cannot be expressed in a --config file. Use --assertion-flow manual to prompt on this CLI's own stdin instead.",
72
+ };
73
+ /**
74
+ * `--config` file fields that map 1:1 onto a `McpSsoOptions` field of the
75
+ * same name — every pre-2.0.0 config field the strategies still need, other
76
+ * than `authorizationCode` (see `applyFileConfig`), lands here.
77
+ */
78
+ const CONFIG_BACKFILL_FIELDS = [
79
+ 'browser',
80
+ 'redirectPort',
81
+ 'redirectUri',
82
+ 'issuerUrl',
83
+ 'authorizationEndpoint',
84
+ 'tokenEndpoint',
85
+ 'deviceAuthorizationEndpoint',
86
+ 'clientId',
87
+ 'clientSecret',
88
+ 'scopes',
89
+ 'scope',
90
+ 'username',
91
+ 'password',
92
+ 'passcode',
93
+ 'subjectToken',
94
+ 'subjectTokenType',
95
+ 'audience',
96
+ 'actorToken',
97
+ 'actorTokenType',
98
+ 'idpSsoUrl',
99
+ 'spEntityId',
100
+ 'acsUrl',
101
+ 'relayState',
102
+ 'assertionFlow',
103
+ 'assertion',
104
+ 'cookie',
105
+ 'uaaUrl',
106
+ ];
107
+ /**
108
+ * Backfills `options` in place from a `--config` file's raw `config` object.
109
+ * CLI flags always win — a field already set on `options` is left alone; the
110
+ * file only fills gaps.
111
+ *
112
+ * This is what makes a config-file-only run (no --protocol/--flow on the
113
+ * CLI) reach the same strategy-building code as a CLI-flag run: without it,
114
+ * the file's fields — including a `browser`/`redirectPort` a 2.0.0 provider
115
+ * no longer accepts directly — would reach `SsoProviderFactory.create()`
116
+ * untouched and be silently ignored.
117
+ *
118
+ * Every field a pre-2.0.0 config could carry either lands on its
119
+ * `McpSsoOptions` equivalent here (which the strategy built from `options`
120
+ * then consumes) or the run is refused via `process.exit(1)` naming what to
121
+ * write instead. Nothing may reach the provider without one of those two
122
+ * happening.
123
+ *
124
+ * Also backfills `options.protocol`/`options.flow` from the file when the
125
+ * CLI didn't set them — a `--config`-only run (no `--protocol`/`--flow`
126
+ * flags at all) must still resolve to a real flow, or nothing downstream
127
+ * ever calls a builder at all.
128
+ *
129
+ * A no-op when `fileConfig` is `null` (no `--config` was given), so callers
130
+ * can invoke this unconditionally.
131
+ */
132
+ function applyFileConfig(options, fileConfig) {
133
+ if (!fileConfig) {
134
+ return;
135
+ }
136
+ options.protocol = options.protocol ?? fileConfig.protocol;
137
+ options.flow = options.flow ?? fileConfig.flow;
138
+ const fields = (fileConfig.config ?? {});
139
+ for (const [field, guidance] of Object.entries(UNSUPPORTED_LEGACY_CONFIG_FIELDS)) {
140
+ if (fields[field] !== undefined) {
141
+ console.error(`❌ --config: ${guidance}`);
142
+ process.exit(1);
143
+ }
144
+ }
145
+ // Pre-2.0.0 field name for the OIDC manual/OOB code paste path; maps onto
146
+ // the same slot --code fills.
147
+ if (options.code === undefined &&
148
+ typeof fields.authorizationCode === 'string') {
149
+ options.code = fields.authorizationCode;
150
+ }
151
+ for (const field of CONFIG_BACKFILL_FIELDS) {
152
+ if (options[field] === undefined && fields[field] !== undefined) {
153
+ options[field] = fields[field];
154
+ }
155
+ }
156
+ }
157
+ function requireOption(value, flagName) {
158
+ if (!value) {
159
+ console.error(`❌ ${flagName} is required for this flow.`);
160
+ process.exit(1);
161
+ }
162
+ return value;
163
+ }
164
+ function resolveOidcTokenEndpoint(options) {
165
+ return (options.tokenEndpoint ||
166
+ (options.uaaUrl
167
+ ? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
168
+ : undefined));
169
+ }
170
+ /**
171
+ * Only the OIDC 'browser' flow opens a browser; routes `--browser`,
172
+ * `--redirect-port` and manual/OOB code paste into the strategy that
173
+ * replaces them.
174
+ */
175
+ function buildOidcBrowserAuthorization(options) {
176
+ if (options.code) {
177
+ // The consumer already holds the code (manual paste / OOB redirect
178
+ // URI); no callback server is opened at all.
179
+ return (0, auth_providers_1.asOidcResult)((0, auth_providers_1.staticCodeStrategy)({
180
+ redirectUri: options.redirectUri,
181
+ payload: options.code,
182
+ }));
183
+ }
184
+ // No fallback: an omitted --redirect-port lets the strategy bind its own
185
+ // default port rather than this CLI pinning a number it doesn't own.
186
+ return (0, auth_providers_1.oidcCallbackStrategy)({
187
+ port: options.redirectPort,
188
+ browser: options.browser,
189
+ timeoutMs: exports.INTERACTIVE_LOGIN_TIMEOUT_MS,
190
+ });
191
+ }
192
+ function buildOidcCommon(options) {
193
+ return {
194
+ issuerUrl: options.issuerUrl,
195
+ clientId: requireOption(options.clientId, '--client-id'),
196
+ clientSecret: options.clientSecret,
197
+ };
198
+ }
199
+ function buildOidcBrowserConfig(options) {
200
+ return {
201
+ ...buildOidcCommon(options),
202
+ authorizationEndpoint: options.authorizationEndpoint,
203
+ tokenEndpoint: resolveOidcTokenEndpoint(options),
204
+ scopes: options.scopes,
205
+ authorization: buildOidcBrowserAuthorization(options),
206
+ };
207
+ }
208
+ function buildOidcDeviceConfig(options) {
209
+ // Device flow never opens a browser from this process; --browser and
210
+ // --redirect-port have nothing to attach to here.
211
+ return {
212
+ ...buildOidcCommon(options),
213
+ deviceAuthorizationEndpoint: options.deviceAuthorizationEndpoint,
214
+ tokenEndpoint: resolveOidcTokenEndpoint(options),
215
+ scopes: options.scopes,
216
+ };
217
+ }
218
+ function buildOidcPasswordConfig(options) {
219
+ // Password flow never opens a browser; --browser and --redirect-port are
220
+ // not meaningful here either.
221
+ const passcode = options.passcode;
222
+ const username = options.username || (passcode ? 'passcode' : undefined);
223
+ const password = options.password || passcode;
224
+ return {
225
+ ...buildOidcCommon(options),
226
+ username: requireOption(username, '--username (or --passcode)'),
227
+ password: requireOption(password, '--password (or --passcode)'),
228
+ tokenEndpoint: resolveOidcTokenEndpoint(options),
229
+ scopes: options.scopes,
230
+ };
231
+ }
232
+ function buildOidcTokenExchangeConfig(options) {
233
+ // Token exchange never opens a browser; --browser and --redirect-port are
234
+ // not meaningful here either.
235
+ return {
236
+ ...buildOidcCommon(options),
237
+ subjectToken: requireOption(options.subjectToken, '--subject-token'),
238
+ subjectTokenType: options.subjectTokenType ||
239
+ 'urn:ietf:params:oauth:token-type:access_token',
240
+ scope: options.scope,
241
+ audience: options.audience,
242
+ actorToken: options.actorToken,
243
+ actorTokenType: options.actorTokenType,
244
+ tokenEndpoint: resolveOidcTokenEndpoint(options),
245
+ };
246
+ }
247
+ /**
248
+ * Both SAML flows (bearer, pure) can open a browser; routes `--browser`,
249
+ * `--redirect-port` and the manual/static assertion options into the
250
+ * strategy that replaces them.
251
+ */
252
+ function buildSamlAuthorization(options) {
253
+ if (options.assertion) {
254
+ // The consumer already holds the assertion; nothing is opened or asked.
255
+ return (0, auth_providers_1.staticCodeStrategy)({
256
+ redirectUri: options.acsUrl,
257
+ payload: options.assertion,
258
+ });
259
+ }
260
+ const assertionFlow = options.assertionFlow || 'browser';
261
+ if (assertionFlow !== 'browser') {
262
+ // 'manual', and an 'assertion' flow given no value, both need a human to
263
+ // lift the SAMLResponse out of the POST body by hand.
264
+ return (0, auth_providers_1.manualSamlResponseStrategy)({
265
+ redirectUri: options.acsUrl,
266
+ read: readManualInput,
267
+ });
268
+ }
269
+ // No fallback: an omitted --redirect-port lets the strategy bind its own
270
+ // default port rather than this CLI pinning a number it doesn't own.
271
+ return (0, auth_providers_1.samlCallbackStrategy)({
272
+ port: options.redirectPort,
273
+ browser: options.browser,
274
+ timeoutMs: exports.INTERACTIVE_LOGIN_TIMEOUT_MS,
275
+ });
276
+ }
277
+ function buildSamlCookieProvider(options) {
278
+ const assertionFlow = options.assertionFlow || (options.assertion ? 'assertion' : 'browser');
279
+ return async (samlResponse) => {
280
+ if (options.cookie) {
281
+ return options.cookie;
282
+ }
283
+ if (assertionFlow === 'assertion') {
284
+ return `SAMLResponse=${samlResponse}`;
285
+ }
286
+ return readManualInput('Paste session cookies: ');
287
+ };
288
+ }
289
+ function buildSamlBearerConfig(options) {
290
+ return {
291
+ idpSsoUrl: requireOption(options.idpSsoUrl, '--idp-sso-url'),
292
+ spEntityId: requireOption(options.spEntityId, '--sp-entity-id'),
293
+ acsUrl: options.acsUrl,
294
+ relayState: options.relayState,
295
+ tokenUrl: options.tokenEndpoint,
296
+ uaaUrl: options.uaaUrl,
297
+ clientId: options.clientId,
298
+ clientSecret: options.clientSecret,
299
+ authorization: buildSamlAuthorization(options),
300
+ };
301
+ }
302
+ function buildSamlPureConfig(options) {
303
+ return {
304
+ idpSsoUrl: requireOption(options.idpSsoUrl, '--idp-sso-url'),
305
+ spEntityId: requireOption(options.spEntityId, '--sp-entity-id'),
306
+ acsUrl: options.acsUrl,
307
+ relayState: options.relayState,
308
+ authorization: buildSamlAuthorization(options),
309
+ cookieProvider: buildSamlCookieProvider(options),
310
+ };
311
+ }
312
+ /**
313
+ * Builds the effective `SsoProviderConfig` from `options` alone.
314
+ *
315
+ * `options` must already be the *merged* view — `--config` file fields
316
+ * backfilled beneath whatever the CLI flags set, via `applyFileConfig` — so
317
+ * this never needs a separate file/CLI merge step, and required-field
318
+ * validation (`requireOption`, inside each builder above) sees the same
319
+ * merged data regardless of whether a value came from the file or a flag.
320
+ */
321
+ function buildProviderConfig(options, existingAuth, existingConn) {
322
+ if (!options.protocol || !options.flow) {
323
+ throw new Error('Provider config is missing. Use --config or --protocol/--flow options.');
324
+ }
325
+ let result;
326
+ if (options.protocol === 'oidc') {
327
+ switch (options.flow) {
328
+ case 'browser':
329
+ result = {
330
+ protocol: 'oidc',
331
+ flow: 'browser',
332
+ config: buildOidcBrowserConfig(options),
333
+ };
334
+ break;
335
+ case 'device':
336
+ result = {
337
+ protocol: 'oidc',
338
+ flow: 'device',
339
+ config: buildOidcDeviceConfig(options),
340
+ };
341
+ break;
342
+ case 'password':
343
+ result = {
344
+ protocol: 'oidc',
345
+ flow: 'password',
346
+ config: buildOidcPasswordConfig(options),
347
+ };
348
+ break;
349
+ case 'token_exchange':
350
+ result = {
351
+ protocol: 'oidc',
352
+ flow: 'token_exchange',
353
+ config: buildOidcTokenExchangeConfig(options),
354
+ };
355
+ break;
356
+ default:
357
+ throw new Error(`Unsupported OIDC flow: ${options.flow}`);
358
+ }
359
+ }
360
+ else if (options.protocol === 'saml2') {
361
+ switch (options.flow) {
362
+ case 'bearer':
363
+ result = {
364
+ protocol: 'saml2',
365
+ flow: 'bearer',
366
+ config: buildSamlBearerConfig(options),
367
+ };
368
+ break;
369
+ case 'pure':
370
+ result = {
371
+ protocol: 'saml2',
372
+ flow: 'pure',
373
+ config: buildSamlPureConfig(options),
374
+ };
375
+ break;
376
+ default:
377
+ throw new Error(`Unsupported SAML flow: ${options.flow}`);
378
+ }
379
+ }
380
+ else {
381
+ throw new Error(`Unsupported protocol: ${options.protocol}`);
382
+ }
383
+ const accessToken = existingConn?.authorizationToken;
384
+ const refreshToken = existingAuth?.refreshToken;
385
+ if ((result.protocol === 'oidc' ||
386
+ (result.protocol === 'saml2' && result.flow === 'bearer')) &&
387
+ (accessToken || refreshToken)) {
388
+ result = {
389
+ ...result,
390
+ config: {
391
+ ...result.config,
392
+ accessToken,
393
+ refreshToken,
394
+ },
395
+ };
396
+ }
397
+ return result;
398
+ }