diff --git a/src/CortiEmbedded.ts b/src/CortiEmbedded.ts index fb4bdcb..27c1a38 100644 --- a/src/CortiEmbedded.ts +++ b/src/CortiEmbedded.ts @@ -30,6 +30,9 @@ import { type PostMessageHandlerCallbacks, } from './utils/PostMessageHandler.js'; +const IFRAME_SANDBOX_POLICY = + 'allow-forms allow-modals allow-scripts allow-same-origin'; + export class CortiEmbedded extends LitElement implements CortiEmbeddedAPI { static styles = [baseStyles, containerStyles]; @@ -43,6 +46,7 @@ export class CortiEmbedded extends LitElement implements CortiEmbeddedAPI { private normalizedBaseURL: string | null = null; + // eslint-disable-next-line class-methods-use-this private getIframeAllowPolicy(normalizedBaseURL?: string | null): string { const permissionTarget = normalizedBaseURL ? new URL(normalizedBaseURL).origin @@ -566,13 +570,13 @@ export class CortiEmbedded extends LitElement implements CortiEmbeddedAPI { `; } diff --git a/src/types/api.ts b/src/types/api.ts index 51b6c8a..33c6bac 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -176,7 +176,7 @@ export interface CortiEmbeddedAPI { * @param credentials Authentication credentials to store * @returns Promise that resolves when credentials are set */ - setCredentials(credentials: { password: string }): Promise; + setCredentials(credentials: SetCredentialsPayload): Promise; /** * Show the embedded UI diff --git a/src/utils/PostMessageHandler.ts b/src/utils/PostMessageHandler.ts index 6e256b3..0d457cb 100644 --- a/src/utils/PostMessageHandler.ts +++ b/src/utils/PostMessageHandler.ts @@ -14,11 +14,41 @@ export interface PostMessageHandlerCallbacks { requestTimeout?: number; } +interface PostMessageHandlerError { + message: string; + code?: string; + details?: unknown; +} + +interface PendingRequest { + resolve: (value: EmbeddedResponse) => void; + reject: (reason: PostMessageHandlerError) => void; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function isEmbeddedEventMessage(value: unknown): value is AnyEvent { + return ( + isRecord(value) && + value.type === 'CORTI_EMBEDDED_EVENT' && + typeof value.event === 'string' + ); +} + +function isEmbeddedResponseMessage(value: unknown): value is EmbeddedResponse { + return ( + isRecord(value) && + value.type === 'CORTI_EMBEDDED_RESPONSE' && + typeof value.action === 'string' && + typeof value.requestId === 'string' && + typeof value.success === 'boolean' + ); +} + export class PostMessageHandler { - private pendingRequests = new Map< - string, - { resolve: (value: any) => void; reject: (reason: any) => void } - >(); + private pendingRequests = new Map(); private messageListener: ((event: MessageEvent) => void) | null = null; @@ -60,13 +90,16 @@ export class PostMessageHandler { const { data } = event; // Check for Corti embedded events - if (data?.type === 'CORTI_EMBEDDED_EVENT') { + if (isEmbeddedEventMessage(data)) { this.handleEvent(data); return; } // Check if this is a response to a pending request - if (data.requestId && this.pendingRequests.has(data.requestId)) { + if ( + isEmbeddedResponseMessage(data) && + this.pendingRequests.has(data.requestId) + ) { this.handleResponse(data); } }; @@ -75,7 +108,7 @@ export class PostMessageHandler { } private handleEvent(eventData: AnyEvent): void { - const eventType = (eventData as any).event; + const eventType = eventData.event; const { payload } = eventData; // Only 'embedded.ready' signals that the iframe is ready to receive messages @@ -83,7 +116,10 @@ export class PostMessageHandler { this.isReady = true; // Store and validate the protocol version from the ready payload - const version = (payload as any)?.version; + const version = + isRecord(payload) && typeof payload.version === 'string' + ? payload.version + : undefined; if (typeof version === 'string') { this._protocolVersion = version; if (version !== PostMessageHandler.SUPPORTED_PROTOCOL_VERSION) { @@ -126,7 +162,7 @@ export class PostMessageHandler { }); } - private handleResponse(data: any): void { + private handleResponse(data: EmbeddedResponse): void { const pendingRequest = this.pendingRequests.get(data.requestId); if (pendingRequest) { const { resolve, reject } = pendingRequest; @@ -245,11 +281,11 @@ export class PostMessageHandler { }, effectiveTimeout); this.pendingRequests.set(requestId, { - resolve: (value: any) => { + resolve: value => { clearTimeout(timeoutId); resolve(value); }, - reject: (reason: any) => { + reject: reason => { clearTimeout(timeoutId); reject(reason); }, diff --git a/src/utils/errorFormatter.ts b/src/utils/errorFormatter.ts index 6fe7b74..d947963 100644 --- a/src/utils/errorFormatter.ts +++ b/src/utils/errorFormatter.ts @@ -5,12 +5,34 @@ interface ValidationError { message?: string; } +type ValidationErrorLike = + | ({ expected: unknown } & Record) + | ({ code: unknown } & Record) + | ({ path: unknown } & Record) + | ({ message: unknown } & Record); + interface FormattedError { message: string; code?: string; details?: unknown; } +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function toStringIfFiniteNumberOrString(value: unknown): string | undefined { + if (typeof value === 'string') { + return value; + } + + if (typeof value === 'number' && Number.isFinite(value)) { + return String(value); + } + + return undefined; +} + /** * Attempts to parse a JSON string safely */ @@ -34,7 +56,7 @@ function extractStatusCode(message: string): string | undefined { /** * Checks if an object looks like a validation error */ -function isValidationError(obj: unknown): obj is ValidationError { +function isValidationError(obj: unknown): obj is ValidationErrorLike { return ( typeof obj === 'object' && obj !== null && @@ -138,13 +160,17 @@ export function formatError( } // Handle error objects - if (typeof error === 'object') { - const errorObj = error as any; + if (isRecord(error)) { + const errorObj = error; + const objectMessage = + typeof errorObj.message === 'string' ? errorObj.message : undefined; + const objectCode = toStringIfFiniteNumberOrString(errorObj.code); + const objectStatus = toStringIfFiniteNumberOrString(errorObj.status); // Handle objects with message and details structure (your original case) - if (errorObj.message && typeof errorObj.message === 'string') { - let formattedMessage = errorObj.message; - let { code } = errorObj; + if (objectMessage) { + let formattedMessage = objectMessage; + let code = objectCode; // If no code is provided, try to extract it from the message if (!code) { @@ -153,7 +179,7 @@ export function formatError( // Try to enhance the message with parsed details if ( - errorObj.details?.message && + isRecord(errorObj.details) && typeof errorObj.details.message === 'string' ) { const parsedDetails = tryParseJson(errorObj.details.message); @@ -178,21 +204,32 @@ export function formatError( // Handle single validation error objects if (isValidationError(errorObj)) { + const validationError: ValidationError = { + expected: + typeof errorObj.expected === 'string' ? errorObj.expected : undefined, + code: objectCode, + path: + Array.isArray(errorObj.path) && + errorObj.path.every(pathPart => typeof pathPart === 'string') + ? errorObj.path + : undefined, + message: objectMessage, + }; return { - message: formatValidationError(errorObj), - code: errorObj.code, + message: formatValidationError(validationError), + code: validationError.code, details: error, }; } // Handle objects that might have useful error info - if (errorObj.error || errorObj.message || errorObj.detail) { - const message = errorObj.error || errorObj.message || errorObj.detail; + if (errorObj.error || errorObj.detail || objectStatus || objectCode) { + const message = errorObj.error ?? objectMessage ?? errorObj.detail; const messageStr = typeof message === 'string' ? message : fallbackMessage; // Try to get code from various properties, or extract from message - let code = errorObj.code || errorObj.status; + let code = objectCode ?? objectStatus; if (!code && typeof message === 'string') { code = extractStatusCode(message); } diff --git a/test/error-formatter.test.ts b/test/error-formatter.test.ts new file mode 100644 index 0000000..667fb5d --- /dev/null +++ b/test/error-formatter.test.ts @@ -0,0 +1,41 @@ +import { expect } from '@open-wc/testing'; +import { formatError } from '../src/utils/errorFormatter.js'; + +describe('formatError', () => { + it('formats object errors without any-based narrowing', () => { + const result = formatError({ + message: '400 Bad Request', + details: { + message: JSON.stringify([ + { path: ['payload', 'path'], expected: 'string' }, + ]), + }, + }); + + expect(result).to.deep.equal({ + message: 'Invalid payload.path: expected string', + code: '400', + details: { + message: JSON.stringify([ + { path: ['payload', 'path'], expected: 'string' }, + ]), + }, + }); + }); + + it('formats status codes from generic object errors', () => { + const result = formatError({ + error: 'Unauthorized', + status: 401, + }); + + expect(result).to.deep.equal({ + message: 'Unauthorized', + code: '401', + details: { + error: 'Unauthorized', + status: 401, + }, + }); + }); +});