Skip to content

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

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

amrit-agarwal-1 wants to merge 3 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-21 07:13 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 thread src/core/config/config-utils.ts Outdated
@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.

@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.

amrit-agarwal-1 added a commit that referenced this pull request Sep 10, 2026
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.

amrit-agarwal-1 added a commit that referenced this pull request Sep 11, 2026
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>
@amrit-agarwal-1
amrit-agarwal-1 force-pushed the fix/sdk-conflicting-auth-config-throw branch from deb8207 to e31c7c0 Compare September 11, 2026 10:11
@claude

claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

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

amrit-agarwal-1 added a commit that referenced this pull request Sep 16, 2026
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>
@amrit-agarwal-1
amrit-agarwal-1 force-pushed the fix/sdk-conflicting-auth-config-throw branch from e31c7c0 to 242ee6f Compare September 16, 2026 07:16
@claude

claude Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

One previously-resolved thread has been re-opened:

src/core/config/config-utils.ts — describeGaps inline static arrays

The requested change was not made. The describeGaps function still constructs ['clientId', 'redirectUri', 'scope'] as const inline twice (and ['baseUrl', 'orgName', 'tenantName'] as const once), while AUTH_FIELDS shows the pattern for how these should be handled at module scope. Per the project convention: NEVER define static lookup tables inside method bodies — move them to module-level constants.

amrit-agarwal-1 added a commit that referenced this pull request Sep 16, 2026
Review feedback on #722: describeGaps built ['clientId', 'redirectUri', 'scope']
inline twice and ['baseUrl', 'orgName', 'tenantName'] once, which
agent_docs/conventions.md forbids — static lookup tables belong at module scope.

BASE_FIELDS and OAUTH_FIELDS now sit beside AUTH_FIELDS, and AUTH_FIELDS derives
from OAUTH_FIELDS so the OAuth triple is declared once. The spread keeps the
literal tuple, so AuthField is still "secret" | "clientId" | "redirectUri" |
"scope" and AUTH_FIELD_SOURCES' Record stays exhaustive.

No behaviour change: every message string is byte-identical, pinned by the
existing tests.

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

claude Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

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

amrit-agarwal-1 and others added 3 commits September 21, 2026 12:39
… [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>
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>
Review feedback on #722: describeGaps built ['clientId', 'redirectUri', 'scope']
inline twice and ['baseUrl', 'orgName', 'tenantName'] once, which
agent_docs/conventions.md forbids — static lookup tables belong at module scope.

BASE_FIELDS and OAUTH_FIELDS now sit beside AUTH_FIELDS, and AUTH_FIELDS derives
from OAUTH_FIELDS so the OAuth triple is declared once. The spread keeps the
literal tuple, so AuthField is still "secret" | "clientId" | "redirectUri" |
"scope" and AUTH_FIELD_SOURCES' Record stays exhaustive.

No behaviour change: every message string is byte-identical, pinned by the
existing tests.

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 b603231 to 0c24496 Compare September 21, 2026 07:11
@claude

claude Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

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

This branch has not been deployed

No deployments
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.

3 participants