Skip to content

feat(cas): add helper method for authenticated citation sources#599

Open
scottcmg wants to merge 6 commits into
mainfrom
task/add-pdf-fetch-method
Open

feat(cas): add helper method for authenticated citation sources#599
scottcmg wants to merge 6 commits into
mainfrom
task/add-pdf-fetch-method

Conversation

@scottcmg

Copy link
Copy Markdown
Contributor

Details

Customers have reported issues with the generated PDF citations that are returned as part of the conversational agent response where clicking the inline citation or the source in the source list returns a 401 in a new tab. This is because these citations require an authorized fetch from ECS to display.

This PR is to add a helper method that customers can use to add support for these citation links so they do not have to be responsible for independently determining the wiring needed for support. It adds some security hardening against (1) inline HTML execution and (2) hijacked download requests sending user credentials to a non-UiPath-origin source.

@scottcmg scottcmg requested a review from a team July 12, 2026 19:20
Comment thread src/services/conversational-agent/conversational-agent.ts Outdated
Comment thread src/services/conversational-agent/conversational-agent.ts Outdated
Comment thread tests/unit/services/conversational-agent/conversational-agent.test.ts Outdated
Comment thread src/services/conversational-agent/conversational-agent.ts
@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review findings

New inline comments posted this run:

  1. Telemetry key casing (conversational-agent.ts line 171) — key uses camelCase 'downloadCitationSource' while existing keys use PascalCase (GetAll, GetById). Should be 'ConversationalAgent.DownloadCitationSource'.

  2. Security: origin guard fails open when baseUrl is absent (conversational-agent.ts line 191) — the guard is silently skipped when baseUrl is not configured, so the bearer token would be sent to any absolute URL. Suggest changing to fail closed.

  3. any type in test factory (conversational-agent.test.ts line 364) — violates the no-any convention; should be Partial.

  4. Missing test branches (conversational-agent.test.ts after line 438) — no tests for (a) fetch() throwing a network error and (b) a malformed downloadUrl failing new URL() parsing.

  5. Missing integration test and oauth-scopes.md entry (conversational-agent.ts line 172) — post-implementation checklist: integration test in tests/integration/shared/agents/ and a scope row for downloadCitationSource() in docs/oauth-scopes.md.

Fail-closed origin check, PascalCase telemetry key, typed test param,
network/malformed-URL branch tests, and the correct (scope-less) oauth
entry per the ECS reference endpoint's org-member policy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@scottcmg

Copy link
Copy Markdown
Contributor Author

Pushed fixes for the review 👇

  • Telemetry key → DownloadCitationSource (PascalCase)
  • Origin check now fails closed (!base || ...)
  • Test param typed Partial<CitationSourceMedia> (no more any)
  • Added network-failure + malformed-URL branch tests (fwiw 'not a url :://' actually resolves against the base, so used 'https://[')
  • oauth-scopes: checked the ECS ReferenceController — it's just ORG_MEMBER_OR_S2S, no specific scope, so the entry says "none (authenticated org-member token)"

Skipped the integration test for now — conv-agent has no integration suite at all yet, so a one-off here felt out of place. Can do it as a separate pass if we want to stand that up.

Comment thread src/models/conversational-agent/conversational-agent.models.ts Outdated
Comment thread src/services/conversational-agent/conversational-agent.ts Outdated
@@ -0,0 +1,52 @@
import { describe, it, expect } from 'vitest';

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.

The test file path does not mirror the src/ structure. Per the architecture docs: "tests/unit/ mirrors src/ structure".

  • Source: src/services/conversational-agent/helpers/citation.ts
  • Expected test path: tests/unit/services/conversational-agent/helpers/citation.test.ts
  • Actual path: tests/unit/helpers/conversational-agent/citation.test.ts

The file should be moved to match the source tree.

@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review findings

New inline comments posted this run:

  1. {@link} in @param description (models.ts:158) — ({@link CitationSourceMedia}) inside a @param description violates the convention; TypeDoc auto-links param types, making this redundant noise. Fix: use backtick notation or drop the inline link.

  2. Service class JSDoc not in sync with model (conversational-agent.ts:146–170) — Convention requires the service class and ServiceModel JSDoc to be identical. Three divergences: missing 4-sentence description paragraph, mismatched @param format, and @example title/comment differences.

  3. Test file not mirroring src/ structure (citation.test.ts:1) — tests/unit/ must mirror src/ structure. The helper source is at src/services/conversational-agent/helpers/citation.ts, so the test belongs at tests/unit/services/conversational-agent/helpers/citation.test.ts.

Make the service JSDoc identical to the ServiceModel and use backtick
notation for the param type per convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

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

Comment thread docs/oauth-scopes.md Outdated
scottcmg and others added 2 commits July 13, 2026 16:32
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…thod

# Conflicts:
#	src/services/conversational-agent/conversational-agent.ts
@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review findings

One previously-resolved thread was unresolved this run:

  • Test file path not mirroring src/ structure (citation.test.ts:1) — The test file is still at tests/unit/helpers/conversational-agent/citation.test.ts. The fix (moving it to tests/unit/services/conversational-agent/helpers/citation.test.ts to mirror the source at src/services/conversational-agent/helpers/citation.ts) was not included in the pushed fixes. Unresolved the thread.

@scottcmg scottcmg requested a review from vnaren23 July 14, 2026 04:52
Comment thread docs/oauth-scopes.md Outdated
Comment thread src/services/conversational-agent/helpers/citation.ts
@claude

claude Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review findings

New inline comment posted this run:

  • Inline regex literal inside function body (citation.ts:60) — /\.[^.]*$/ is defined inside selectMimeType's body, recreated on every call. Convention requires static regex literals to be module-level constants (e.g., const FILE_EXTENSION_REGEX = /\.[^.]*$/;). All other previous findings have been addressed.

@sonarqubecloud

Copy link
Copy Markdown

Comment on lines +50 to +51
/** Configured SDK origin, used to keep citation downloads on the tenant's host. */
private readonly baseUrl?: string;

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.

instance.config.baseUrl is typed string on BaseConfig, so this can drop the ? (and then we could remove the !base check in line 114)

Comment on lines +108 to +110
throw new ValidationError({
message: `Invalid citation downloadUrl`
});

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.

nit : there's no interpolation, could be plain single quotes around the message. Same for line 116.
Also shouldn't both be ServerError instead?

Comment on lines +45 to +46
text: async () => statusText,
headers: { get: () => null },

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.

are these two read anywhere?

it('throws a ValidationError when the source has no downloadUrl', async () => {
await expect(
conversationalAgent.downloadCitationSource(mediaSource({ downloadUrl: undefined })),
).rejects.toThrow(/downloadUrl/);

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.

Every other error test in this block uses .rejects.toBeInstanceOf(ValidationError). This one uses toThrow(/downloadUrl/). Any particular reason?

Comment on lines +160 to +163
* @throws ValidationError if the source has no `downloadUrl`
* @throws A typed HTTP error for error responses — e.g. `AuthenticationError`
* (401), `AuthorizationError` (403), `NotFoundError` (404) — or
* `NetworkError` on a connection failure, matching other SDK calls

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.

Does any method document the errors with the @throw tag? Could you verify.
wherever it is used, I don't think it is in the docs surface.

Comment on lines +145 to +156
* Downloads the document behind a media citation as an authenticated `Blob`.
*
* Media citation sources expose a `downloadUrl` pointing at an auth-gated
* UiPath endpoint; opening it directly sends no token and fails with `401`.
* This performs the request with the SDK's access token and returns the bytes,
* with the `Blob` type resolved from the source `mimeType` (falling back to the
* response Content-Type, then the title's file extension). Use `source.title`
* as the file name.
*
* HTML content is intentionally returned as `application/octet-stream` (a
* download) rather than `text/html`, so previewing the blob inline can't
* execute citation markup in your app's origin.

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.

could this description be made a little concise?

throw ErrorFactory.createFromHttpStatus(response.status, errorInfo);
}

const blob = await response.blob();

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.

should this be inside try-catch as well?
could we extend the existing try/catch to cover the body read too

something like

try {
    const response = await fetch(target.href, {
      headers: { Authorization: `Bearer ${token}` }
    });

    if (!response.ok) {
      const errorInfo = await errorResponseParser.parse(response);
      throw ErrorFactory.createFromHttpStatus(response.status, errorInfo);
    }

    blob = await response.blob();
  } catch (error: any) {
    if (error.type && error.type.includes('Error')) throw error;
    throw ErrorFactory.createNetworkError(error);
  }

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