Skip to content

fix(core): reject a config carrying both auth methods at construction [APPS-37257] - #722

Open
amrit-agarwal-1 wants to merge 2 commits into
mainfrom
fix/sdk-conflicting-auth-config-throw
Open

fix(core): reject a config carrying both auth methods at construction [APPS-37257]#722
amrit-agarwal-1 wants to merge 2 commits into
mainfrom
fix/sdk-conflicting-auth-config-throw

Conversation

@amrit-agarwal-1

@amrit-agarwal-1 amrit-agarwal-1 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes APPS-37257.

Re-scoped: the ticket's literal case already ships

The ticket reports a pure secret config silently deferring in a coded app whose dev plugin injected
OAuth meta tags. That case was fixed by #675#mergeConfigSources elects the auth method and
drops the loser, so a caller naming only secret works. The reporter is on 1.6.1, which predates it;
the 1.6.x remedy is to upgrade to >= 1.7.0, and this PR does not touch that path.

What is still broken on main

When the caller names a secret and at least one OAuth field, the election XOR
(namesSecret !== namesOAuth) is false, so nothing is dropped. The merged config then carries both
methods, isCompleteConfig's XOR fails, and the constructor silently stores #partialConfig without
calling SDKInternalsRegistry.set. The caller first learns about it from an unrelated service
constructor (Invalid SDK instance...), or from initialize() reporting a configuration that was
never "not found".

The method's own comment claimed this case was "left intact for validateConfig() to reject". It was
not. validateConfig's only call site is inside #initializeWithConfig, which runs only once
isCompleteConfig has returned true — precisely what a both-methods config makes false. The
function was unreachable dead code and is removed rather than resurrected.

Changes

  • Guard #mergeConfigSources, placed after feat(core): zero-config authentication from the execution context #675's drop block so cross-source resolution is
    preserved. Only a caller contradicting themselves throws. Inside the merge rather than the
    constructor body, so #loadConfig gets the same accurate error and the throw lands before
    telemetryClient.initialize and trackEvent('Sdk.Auth').
  • conflictingAuthMessage names every auth field and the layer that supplied it — the constructor
    argument, <meta name="uipath:...">, or the env var — and says which fields to remove. It never
    interpolates a field value: secret is a bearer token and this string reaches browser consoles and
    log pipelines. Pinned by a test.
  • missingConfigMessage(found?) now describes a config that was found but is short, instead of
    claiming none was found. The zero-argument text is byte-identical, so existing assertions hold.
  • Export PartialUiPathConfig, BaseConfig, OAuthFields from /core. The constructor's declared
    parameter type was unreachable from that entry point, which is what pushed the reporter to
    as unknown as ConstructorParameters<typeof UiPath>[0]. Same four names the root barrel exports — no
    entry-point mixing, which would duplicate SDKInternalsRegistry.
  • Dropped scope from the secret-auth JSDoc example (src/core/index.ts). Pasted into a coded app
    it inherits the other two OAuth fields from the injected meta tags and lands on the new guard. It was
    the only artifact in the repo the guard breaks, it never typechecked against UiPathSDKConfig, and
    scope was never load-bearing there since hasOAuthConfig requires all three fields.
  • Docs: the precedence admonition in docs/authentication.md and the redirect-URI tip in
    docs/coded-apps/getting-started.md now say that precedence is per field, so omitted fields are
    inherited. Note the ticket has this backwards — constructor config merges over meta tags, not under.
  • .claude/skills/onboard-api/references/e2e-testing.md quoted the old error string verbatim for the
    scopes-plural mistake; updated so it stays accurate.

Verification

npm run typecheck clean · npm run lint 0 warnings 0 errors · 2760/2760 unit tests ·
npm run build clean, with all four config type names present in dist/core/index.d.ts.

End to end against the built dist, with the dev plugin's meta tags faked:

Input (meta tags carry full OAuth) Result
{...base, secret} — the ticket / #675 constructs, isInitialized=true, new Cases(sdk) OK
{...base, secret, scope}the residue throws at construction, naming each field's source
{...base, secret, clientId: undefined, ...} constructs — the reporter's workaround stays legal
new UiPath() — documented coded-app default constructs, new Cases(sdk) OK
secret value present in the message? no

15 new/rewritten tests. Reviewers mutation-tested them: 11 fail on a full revert of the source change,
and the 4 that pass either way are the deliberate guardrails — each fails under the specific prohibited
mutation it exists to catch (predicate widened to any-OAuth-field, key-presence instead of truthiness,
guard moved before the drop block).

Deliberately out of scope

Filed separately rather than smuggled in:

  • Incompleteness still defers (missing base fields, half-filled OAuth, folder-key-only pages). This PR
    improves the diagnosis for those; the deferral itself is load-bearing for the documented
    no-argument coded-app pattern.
  • A host-auth coded app deployed with no client id gets <meta name="uipath:client-id" content="">, so
    even new UiPath() never registers. That is a backend-side defect this change only re-labels.
  • Telemetry attributes on the existing Sdk.Auth event to measure how often the cross-source
    resolution fires. Explicitly not a console.warn — that would fire on every page load of every
    deployed secret-mode coded app, a pattern docs/authentication.md sanctions.
  • Relaxing UiPathSDKConfig's ?: never to ?: undefined. The reporter's cast is caused by
    exactOptionalPropertyTypes, which no tsconfig here sets; that never is the compile-time half of
    this guard. Wants their compilerOptions first.

Related: STUD-81240 and
AGVSOL-3239 are two more reports of the same
Invalid SDK instance string from different causes — module-scope service construction and a deployed
blank page. #675 already improved that message; neither is touched here.

🤖 Generated with Claude Code

@amrit-agarwal-1
amrit-agarwal-1 requested a review from a team September 7, 2026 16:13
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://UiPath.github.io/uipath-typescript/pr-preview/pr-722/

Built to branch gh-pages at 2026-09-10 10:24 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ No issues found. Checked for bugs and CLAUDE.md compliance.

@amrit-agarwal-1
amrit-agarwal-1 marked this pull request as draft September 7, 2026 21:07
@amrit-agarwal-1
amrit-agarwal-1 force-pushed the fix/sdk-conflicting-auth-config-throw branch from 57e5de0 to 91dc8b8 Compare September 10, 2026 05:11
@amrit-agarwal-1
amrit-agarwal-1 marked this pull request as ready for review September 10, 2026 05:11
Comment on lines +63 to +66
const named = (['clientId', 'redirectUri', 'scope'] as const).filter((field) => found[field]);
gaps.push(named.length > 0
? `the OAuth configuration is incomplete — ${named.join(', ')} set, ` +
`${(['clientId', 'redirectUri', 'scope'] as const).filter((f) => !found[f]).join(', ')} missing`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

['clientId', 'redirectUri', 'scope'] as const is constructed twice in four lines, and AUTH_FIELDS already lives at module level. Per the conventions, static arrays that don't change between calls belong at module scope — repeated inline literals hide structure and get rebuilt on every invocation.

Extract a module-level constant alongside AUTH_FIELDS:

Suggested change
const named = (['clientId', 'redirectUri', 'scope'] as const).filter((field) => found[field]);
gaps.push(named.length > 0
? `the OAuth configuration is incomplete — ${named.join(', ')} set, ` +
`${(['clientId', 'redirectUri', 'scope'] as const).filter((f) => !found[f]).join(', ')} missing`
const OAUTH_AUTH_FIELDS = ['clientId', 'redirectUri', 'scope'] as const;
type OAuthAuthField = typeof OAUTH_AUTH_FIELDS[number];

Then describeGaps becomes:

const named = OAUTH_AUTH_FIELDS.filter((field) => found[field]);
gaps.push(named.length > 0
  ? `the OAuth configuration is incomplete — ${named.join(', ')} set, ` +
    `${OAUTH_AUTH_FIELDS.filter((f) => !found[f]).join(', ')} missing`
  : 'no authentication method set (needs `secret`, or clientId, redirectUri and scope)');

(Line 51's ['baseUrl', 'orgName', 'tenantName'] as const has the same issue and can be extracted similarly as BASE_CONFIG_FIELDS.)

@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

✅ No issues found. Checked for bugs and CLAUDE.md compliance.

@Raina451 Raina451 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fix catches the conflict at runtime right? basically in browser, when user do npm run dev, it wont fail at that time , this error will be visible in browser console at runtime.
can we fix this at build time itself? something with type check or so?

@amrit-agarwal-1

Copy link
Copy Markdown
Contributor Author

this fix catches the conflict at runtime right? basically in browser, when user do npm run dev, it wont fail at that time , this error will be visible in browser console at runtime. can we fix this at build time itself? something with type check or so?

This ticket specifically can't be caught at build time because the reporter's argument new UiPath({ baseUrl, orgName, tenantName, secret }) is valid on its own, and is the documented way to use secret auth; no type could reject it without rejecting correct code. The conflict only came into existence once that was merged with the OAuth tags on the page, and those are written into index.html at deploy time, rewritten later if the app's client id changes, with no rebuild - so the same bundle is valid in one tenant and invalid in another. Tightening the parameter type also overshoots the runtime rule: it rejects {secret, scope} and {secret, clientId}, which work today because hasOAuthConfig requires all three fields.

… [APPS-37257]

`#mergeConfigSources` elects an auth method only when the caller names exactly
one of them. When the caller names a secret *and* at least one OAuth field, the
election XOR is false, nothing is dropped, and the merged config carries both
methods — so `isCompleteConfig`'s XOR fails and the constructor silently stores
`#partialConfig` without registering internals. The caller first hears about it
from an unrelated service constructor ("Invalid SDK instance"), or from
`initialize()` reporting a configuration that was never "not found".

The method's own comment claimed the case was "left intact for validateConfig()
to reject". It was not: `validateConfig`'s only call site sits inside
`#initializeWithConfig`, which runs only once `isCompleteConfig` has returned
true — precisely what a both-methods config makes false. The function was
unreachable dead code and is removed.

- Guard the merge, after the existing drop block, so PR #675's cross-source
  resolution is preserved: a pure secret over injected OAuth meta tags still
  works. Only a caller contradicting themselves throws.
- `conflictingAuthMessage` names every auth field and the layer that supplied it
  (constructor argument / `<meta name="uipath:...">` / env var), and states which
  fields to remove. It never interpolates a field value — `secret` is a bearer
  token and this string reaches browser consoles.
- `missingConfigMessage` now describes a config that was found but is short,
  instead of claiming none was found.
- Export `PartialUiPathConfig`, `BaseConfig` and `OAuthFields` from `/core`. The
  constructor's declared parameter type was unreachable from that entry point,
  which is what pushed callers to `as unknown as ConstructorParameters<...>[0]`.
- Drop `scope` from the secret-auth JSDoc example: pasted into a coded app it
  inherits the other two OAuth fields from the meta tags and hits the new guard.

Every input that now throws already failed — later, and somewhere else.

Verified: typecheck, oxlint, 2760/2760 unit tests, build; and end to end against
the built dist with the dev plugin's meta tags faked — the reporter's scenario,
their explicit-undefined workaround, and `new UiPath()` with no config all still
construct and register.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@amrit-agarwal-1
amrit-agarwal-1 force-pushed the fix/sdk-conflicting-auth-config-throw branch from 91dc8b8 to d258ff2 Compare September 10, 2026 10:08
Comment thread docs/authentication.md Outdated
Comment thread tests/unit/core/config/config-utils.test.ts Outdated
@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

One new finding posted this run: misleading test description in tests/unit/core/config/config-utils.test.ts:77 — the description says "no layer named an OAuth field" but metaConfig: { ...OAUTH } is present in the test body. The condition under test is that the constructor argument (sources.config) named no OAuth field; "layer" implies all sources, which is incorrect and would produce a confusing failure message.

Review feedback on #722:

- The precedence admonition in docs/authentication.md said the same thing three
  ways over thirteen lines. Four lines, same facts.
- `falls back to generic guidance when no layer named an OAuth field` was wrong:
  the test's own `metaConfig` names all three OAuth fields. The condition is that
  the *constructor argument* named none — `namedOAuth` filters `sources.config`
  only — so a failure would have read as if no layer had OAuth at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

✅ No issues found. Checked for bugs and CLAUDE.md compliance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants