Skip to content

Commit 74beea5

Browse files
fix(auth): address PR review findings
- Add TokenManager.destroy() to prevent EmbeddedTokenManager listener leak - Restore global.document in afterEach to fix runtime.test.ts isolation - Move UIP.* protocol types to src/core/auth/uip-embedded-protocol.ts (were incorrectly placed in Action Center models domain) - Remove unused UipEmbeddedRefreshTokenPayload type - Add @param JSDoc to EmbeddedTokenManager constructor Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6d94cc2 commit 74beea5

7 files changed

Lines changed: 80 additions & 33 deletions

File tree

src/core/auth/embedded-token-manager.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UipEmbeddedEventNames, UipEmbeddedInitPayload, UipEmbeddedTokenRefreshedPayload } from '../../models/action-center/tasks.internal-types';
1+
import { UipEmbeddedEventNames, UipEmbeddedInitPayload, UipEmbeddedTokenRefreshedPayload } from './uip-embedded-protocol';
22
import { TokenInfo } from './types';
33
import { AuthenticationError, HttpStatus } from '../errors';
44
import { HostTokenResponse, isTokenExpired, requestHostToken } from './host-token-request';
@@ -49,6 +49,10 @@ export class EmbeddedTokenManager {
4949
private cancelRefresh: (() => void) | null = null;
5050
private readonly initListener: (event: MessageEvent) => void;
5151

52+
/**
53+
* @param onTokenRefreshed Called whenever a token is received or refreshed,
54+
* allowing the caller to persist the new token in the execution context.
55+
*/
5256
constructor(private readonly onTokenRefreshed: (tokenInfo: TokenInfo) => void) {
5357
this.initListener = this.handleInit.bind(this);
5458
window.addEventListener('message', this.initListener);

src/core/auth/token-manager.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,14 @@ export class TokenManager {
237237
return true;
238238
}
239239

240+
/**
241+
* Releases resources held by this instance.
242+
* Must be called when the TokenManager is no longer needed to prevent listener leaks.
243+
*/
244+
destroy(): void {
245+
this.embeddedTokenManager?.destroy();
246+
}
247+
240248
/**
241249
* Clears the current token
242250
*/
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Event names and payload types for the UIP.* postMessage protocol used
3+
* when a coded app is embedded inside a first-party UiPath host.
4+
*
5+
* Flow:
6+
* Host → App: UIP.init (delivers initial access token)
7+
* App → Host: UIP.refreshToken (requests a new token on expiry)
8+
* Host → App: UIP.tokenRefreshed (delivers refreshed access token)
9+
*/
10+
11+
export enum UipEmbeddedEventNames {
12+
INIT = 'UIP.init',
13+
REFRESH_TOKEN = 'UIP.refreshToken',
14+
TOKEN_REFRESHED = 'UIP.tokenRefreshed',
15+
}
16+
17+
export type UipEmbeddedInitPayload = {
18+
eventType: UipEmbeddedEventNames.INIT,
19+
content: {
20+
token: {
21+
accessToken: string,
22+
expiresAt: string, // ISO 8601
23+
},
24+
},
25+
};
26+
27+
export type UipEmbeddedTokenRefreshedPayload = {
28+
eventType: UipEmbeddedEventNames.TOKEN_REFRESHED,
29+
content: {
30+
token: {
31+
accessToken: string,
32+
expiresAt: string, // ISO 8601
33+
},
34+
},
35+
};

src/models/action-center/tasks.internal-types.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,36 +30,6 @@ export type ActionCenterEventResponsePayload = {
3030
content: ActionCenterTokenData,
3131
}
3232

33-
export enum UipEmbeddedEventNames {
34-
INIT = 'UIP.init',
35-
REFRESH_TOKEN = 'UIP.refreshToken',
36-
TOKEN_REFRESHED = 'UIP.tokenRefreshed',
37-
}
38-
39-
export type UipEmbeddedInitPayload = {
40-
eventType: UipEmbeddedEventNames.INIT,
41-
content: {
42-
token: {
43-
accessToken: string,
44-
expiresAt: string, // ISO 8601
45-
},
46-
},
47-
};
48-
49-
export type UipEmbeddedTokenRefreshedPayload = {
50-
eventType: UipEmbeddedEventNames.TOKEN_REFRESHED,
51-
content: {
52-
token: {
53-
accessToken: string,
54-
expiresAt: string, // ISO 8601
55-
},
56-
},
57-
};
58-
59-
export type UipEmbeddedRefreshTokenPayload = {
60-
eventType: UipEmbeddedEventNames.REFRESH_TOKEN,
61-
};
62-
6333
export interface TasksAssignOptions {
6434
taskAssignments: TaskAssignmentOptions[];
6535
}

tests/unit/core/auth/embedded-token-manager.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
22
import { EmbeddedTokenManager } from '../../../../src/core/auth/embedded-token-manager';
3-
import { UipEmbeddedEventNames } from '../../../../src/models/action-center/tasks.internal-types';
3+
import { UipEmbeddedEventNames } from '../../../../src/core/auth/uip-embedded-protocol';
44
import { AuthenticationError } from '../../../../src/core/errors';
55
import type { TokenInfo } from '../../../../src/core/auth/types';
66

tests/unit/core/auth/token-manager-embedded.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,30 @@ describe('TokenManager — EmbeddedTokenManager wiring', () => {
133133
expect(result).toBe('my-secret');
134134
expect(mockEmbeddedRefresh).not.toHaveBeenCalled();
135135
});
136+
137+
// ---- destroy ----
138+
139+
it('destroy() delegates to EmbeddedTokenManager', () => {
140+
vi.mocked(platform).isBrowser = true;
141+
vi.mocked(platform).isInActionCenter = false;
142+
const mockDestroy = vi.fn();
143+
vi.mocked(EmbeddedTokenManager).mockImplementationOnce(() => ({
144+
isActive: mockEmbeddedIsActive,
145+
refreshAccessToken: mockEmbeddedRefresh,
146+
destroy: mockDestroy,
147+
}));
148+
149+
const manager = makeManager(true);
150+
manager.destroy();
151+
152+
expect(mockDestroy).toHaveBeenCalledOnce();
153+
});
154+
155+
it('destroy() is a no-op when EmbeddedTokenManager was not instantiated', () => {
156+
vi.mocked(platform).isBrowser = false;
157+
vi.mocked(platform).isInActionCenter = false;
158+
const manager = makeManager();
159+
160+
expect(() => manager.destroy()).not.toThrow();
161+
});
136162
});

tests/unit/core/config/runtime.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, vi, beforeEach } from 'vitest';
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
22

33
// Must mock platform before importing runtime
44
vi.mock('../../../../src/utils/platform', () => ({ isBrowser: true }));
@@ -27,6 +27,10 @@ beforeEach(() => {
2727
global.document = { querySelector: mockQuerySelector } as unknown as Document;
2828
});
2929

30+
afterEach(() => {
31+
delete (global as unknown as Record<string, unknown>).document;
32+
});
33+
3034
// ---------------------------------------------------------------------------
3135
// platformHosted meta tag tests
3236
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)