Skip to content

Commit ccdaff9

Browse files
committed
feat(iframe-manager): create-iframe-manager
create an iframe manager package which is consumed by davinci-client. Updated types throughout the codebase
1 parent 87c200b commit ccdaff9

File tree

23 files changed

+466
-11
lines changed

23 files changed

+466
-11
lines changed

nx.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
},
7171
"test": {
7272
"inputs": ["default", "^default", "noMarkdown", "^noMarkdown"],
73-
"dependsOn": ["^test", "^build"],
73+
"dependsOn": ["^test", "^build", "^build"],
7474
"outputs": ["{projectRoot}/coverage"],
7575
"cache": true
7676
},

packages/davinci-client/src/lib/config.types.test-d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import { describe, expectTypeOf, it } from 'vitest';
88
import type { DaVinciConfig, InternalDaVinciConfig } from './config.types.js';
9-
import type { AsyncLegacyConfigOptions } from '@forgerock/sdk-types';
9+
import type { AsyncLegacyConfigOptions, AuthorizeUrl } from '@forgerock/sdk-types';
1010
import type { WellknownResponse } from './wellknown.types.js';
1111

1212
describe('Config Types', () => {
@@ -48,7 +48,7 @@ describe('Config Types', () => {
4848
const config: InternalDaVinciConfig = {
4949
wellknownResponse: {
5050
issuer: 'https://example.com',
51-
authorization_endpoint: 'https://example.com/auth',
51+
authorization_endpoint: 'https://example.com/auth' as AuthorizeUrl,
5252
token_endpoint: 'https://example.com/token',
5353
userinfo_endpoint: 'https://example.com/userinfo',
5454
jwks_uri: 'https://example.com/jwks',
@@ -97,7 +97,7 @@ describe('Config Types', () => {
9797
// InternalDaVinciConfig specific property
9898
wellknownResponse: {
9999
issuer: 'https://example.com',
100-
authorization_endpoint: 'https://example.com/auth',
100+
authorization_endpoint: 'https://example.com/auth' as AuthorizeUrl,
101101
token_endpoint: 'https://example.com/token',
102102
userinfo_endpoint: 'https://example.com/userinfo',
103103
jwks_uri: 'https://example.com/jwks',
@@ -135,7 +135,7 @@ describe('WellknownResponse', () => {
135135
it('should have all required OIDC properties', () => {
136136
const wellknown: WellknownResponse = {
137137
issuer: 'https://example.com',
138-
authorization_endpoint: 'https://example.com/auth',
138+
authorization_endpoint: 'https://example.com/auth' as AuthorizeUrl,
139139
token_endpoint: 'https://example.com/token',
140140
userinfo_endpoint: 'https://example.com/userinfo',
141141
jwks_uri: 'https://example.com/jwks',
@@ -178,7 +178,7 @@ describe('WellknownResponse', () => {
178178
it('should allow optional OIDC properties', () => {
179179
const wellknownWithOptionals: WellknownResponse = {
180180
issuer: 'https://example.com',
181-
authorization_endpoint: 'https://example.com/auth',
181+
authorization_endpoint: 'https://example.com/auth' as AuthorizeUrl,
182182
token_endpoint: 'https://example.com/token',
183183
userinfo_endpoint: 'https://example.com/userinfo',
184184
jwks_uri: 'https://example.com/jwks',
@@ -233,7 +233,7 @@ describe('WellknownResponse', () => {
233233
it('should enforce URL format for endpoint properties', () => {
234234
const wellknown: WellknownResponse = {
235235
issuer: 'https://example.com',
236-
authorization_endpoint: 'https://example.com/auth',
236+
authorization_endpoint: 'https://example.com/auth' as AuthorizeUrl,
237237
token_endpoint: 'https://example.com/token',
238238
userinfo_endpoint: 'https://example.com/userinfo',
239239
jwks_uri: 'https://example.com/jwks',

packages/davinci-client/src/lib/wellknown.types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { AuthorizeUrl } from '@forgerock/sdk-types';
2+
13
/*
24
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
35
*
@@ -6,7 +8,7 @@
68
*/
79
export interface WellknownResponse {
810
issuer: string;
9-
authorization_endpoint: string;
11+
authorization_endpoint: AuthorizeUrl;
1012
pushed_authorization_request_endpoint: string;
1113
token_endpoint: string;
1214
userinfo_endpoint: string;
@@ -36,7 +38,7 @@ export interface WellknownResponse {
3638
}
3739

3840
export interface Endpoints {
39-
authorize: string;
41+
authorize: AuthorizeUrl;
4042
issuer: string;
4143
introspection: string;
4244
tokens: string;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# iframe-manager
2+
3+
This library was generated with [Nx](https://nx.dev).
4+
5+
## Building
6+
7+
Run `nx build iframe-manager` to build the library.
8+
9+
## Running unit tests
10+
11+
Run `nx test iframe-manager` to execute the unit tests via [Vitest](https://vitest.dev/).
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import baseConfig from '../../../eslint.config.mjs';
2+
3+
export default [
4+
...baseConfig,
5+
{
6+
files: ['**/*.json'],
7+
rules: {
8+
'@nx/dependency-checks': [
9+
'error',
10+
{
11+
ignoredFiles: [
12+
'{projectRoot}/eslint.config.{js,cjs,mjs}',
13+
'{projectRoot}/vite.config.{js,ts,mjs,mts}',
14+
],
15+
},
16+
],
17+
},
18+
languageOptions: {
19+
parser: await import('jsonc-eslint-parser'),
20+
},
21+
},
22+
];
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@forgerock/iframe-manager",
3+
"version": "0.0.1",
4+
"private": false,
5+
"type": "module",
6+
"main": "./dist/src/index.js",
7+
"module": "./dist/src/index.js",
8+
"types": "./dist/src/index.d.ts",
9+
"exports": {
10+
"./package.json": "./package.json",
11+
".": {
12+
"types": "./dist/src/index.d.ts",
13+
"import": "./dist/src/index.js",
14+
"default": "./dist/src/index.js"
15+
}
16+
},
17+
"dependencies": {
18+
"@forgerock/sdk-request-middleware": "workspace:*",
19+
"@forgerock/sdk-utilities": "workspace:*",
20+
"@forgerock/sdk-types": "workspace:*",
21+
"tslib": "^2.3.0"
22+
},
23+
"nx": {
24+
"tags": ["scope:sdk-effects"]
25+
}
26+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lib/iframe-manager.effects.js';
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
8+
/* eslint-disable @typescript-eslint/no-empty-function */
9+
import { AuthorizeUrl } from '@forgerock/sdk-types';
10+
11+
// Define specific options for the iframe request
12+
export interface IframeRequestOptions {
13+
url: string;
14+
timeout: number;
15+
// Specify query parameters expected on success/error if needed,
16+
// otherwise, parse all parameters found.
17+
// Example: expectedParams?: string[];
18+
}
19+
20+
type ResolvedParams<T> = Record<string, T>;
21+
22+
type Noop = () => void;
23+
24+
/**
25+
* Initializes the Iframe Manager effect.
26+
* This follows the functional effect pattern, returning an API
27+
* within a closure. Configuration could be passed here if needed
28+
* for default behaviors (e.g., default timeout), but currently,
29+
* all config is per-request.
30+
*
31+
* @returns An object containing the API for managing iframe requests.
32+
*/
33+
export default function iframeManagerInit(/*config: OAuthConfig*/) {
34+
/**
35+
* Creates an iframe to make a background request to the specified URL,
36+
* waits for a redirect, and resolves with the query parameters from the
37+
* redirect URL.
38+
*
39+
* @param options - The options for the iframe request (URL, timeout).
40+
* @returns A Promise that resolves with the query parameters from the redirect URL or rejects on timeout or error.
41+
*/
42+
const getAuthCodeByIFrame = (options: {
43+
url: AuthorizeUrl;
44+
requestTimeout: number;
45+
}): Promise<ResolvedParams<string>> => {
46+
const { url, requestTimeout } = options;
47+
48+
return new Promise((resolve, reject) => {
49+
let iframe: HTMLIFrameElement | null = document.createElement('iframe');
50+
let timerId: number | ReturnType<typeof setTimeout> | null = null;
51+
52+
// Define these within the promise closure to avoid retaining
53+
// references after completion.
54+
let onLoadHandler: () => void = () => {};
55+
let cleanup: Noop = () => {};
56+
57+
cleanup = (): void => {
58+
if (timerId) {
59+
clearTimeout(timerId);
60+
timerId = null;
61+
}
62+
if (iframe) {
63+
iframe.removeEventListener('load', onLoadHandler);
64+
// Check if iframe is still mounted before removing
65+
if (iframe.parentNode) {
66+
iframe.remove();
67+
}
68+
iframe = null; // Dereference iframe for garbage collection
69+
}
70+
onLoadHandler = () => {};
71+
cleanup = () => {};
72+
};
73+
74+
onLoadHandler = (): void => {
75+
try {
76+
// Accessing contentWindow or contentDocument can throw cross-origin errors
77+
// if the iframe navigated to a different origin unexpectedly before redirecting back.
78+
// We expect the final navigation to be back to the original redirect_uri origin.
79+
if (iframe && iframe.contentWindow) {
80+
const newHref = iframe.contentWindow.location.href;
81+
82+
// Avoid processing 'about:blank' or initial load if it's not the target URL
83+
if (
84+
newHref === 'about:blank' ||
85+
!newHref.startsWith(options.url.substring(0, options.url.indexOf('?')))
86+
) {
87+
// Check if the newHref origin matches expected redirect_uri origin if possible
88+
// Or simply check if it contains expected parameters.
89+
// For now, we proceed assuming any load could be the redirect.
90+
}
91+
92+
const redirectUrl = new URL(newHref);
93+
const params: ResolvedParams<string> = {};
94+
redirectUrl.searchParams.forEach((value, key) => {
95+
params[key] = value;
96+
});
97+
98+
// Check for standard OAuth error parameters
99+
if (params.error) {
100+
cleanup();
101+
// Reject with error details from the URL
102+
reject(
103+
new Error(
104+
`Authorization error: ${params.error}. Description: ${params.error_description || 'N/A'}`,
105+
),
106+
);
107+
} else if (Object.keys(params).length > 0) {
108+
// Assume success if parameters are present and no error param exists
109+
// More specific checks (e.g., for 'code' or 'state') could be added here
110+
// based on `options.expectedParams` if provided.
111+
cleanup();
112+
resolve(params);
113+
}
114+
// If no params and no error, it might be an intermediate step,
115+
// The timeout will eventually handle non-resolving states.
116+
}
117+
} catch (error) {
118+
// Catch potential cross-origin errors or other issues accessing iframe content
119+
cleanup();
120+
reject(
121+
new Error(
122+
`Failed to process iframe response: ${error instanceof Error ? error.message : String(error)}`,
123+
),
124+
);
125+
}
126+
};
127+
128+
timerId = setTimeout(() => {
129+
cleanup();
130+
reject(new Error(`Iframe request timed out after ${requestTimeout}ms`));
131+
}, requestTimeout);
132+
133+
if (iframe) {
134+
iframe.style.display = 'none';
135+
iframe.addEventListener('load', onLoadHandler);
136+
document.body.appendChild(iframe);
137+
iframe.src = url; // Start the loading process
138+
} else {
139+
// Should not happen, but handle defensively
140+
reject(new Error('Failed to create iframe element'));
141+
}
142+
});
143+
};
144+
145+
// Return the public API
146+
return {
147+
getAuthCodeByIFrame,
148+
};
149+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"extends": "../../../tsconfig.base.json",
3+
"files": [],
4+
"include": [],
5+
"references": [
6+
{
7+
"path": "../../sdk-types"
8+
},
9+
{
10+
"path": "../../sdk-utilities"
11+
},
12+
{
13+
"path": "../sdk-request-middleware"
14+
},
15+
{
16+
"path": "./tsconfig.lib.json"
17+
},
18+
{
19+
"path": "./tsconfig.spec.json"
20+
}
21+
],
22+
"nx": {
23+
"addTypecheckTarget": false
24+
}
25+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"extends": "../../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"baseUrl": ".",
5+
"outDir": "dist",
6+
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
7+
"emitDeclarationOnly": false,
8+
"module": "nodenext",
9+
"moduleResolution": "nodenext",
10+
"forceConsistentCasingInFileNames": true,
11+
"strict": true,
12+
"importHelpers": true,
13+
"noImplicitOverride": true,
14+
"noImplicitReturns": true,
15+
"noFallthroughCasesInSwitch": true,
16+
"types": ["node"]
17+
},
18+
"include": ["src/**/*.ts"],
19+
"references": [
20+
{
21+
"path": "../../sdk-types/tsconfig.lib.json"
22+
},
23+
{
24+
"path": "../../sdk-utilities/tsconfig.lib.json"
25+
},
26+
{
27+
"path": "../sdk-request-middleware/tsconfig.lib.json"
28+
}
29+
],
30+
"exclude": [
31+
"vite.config.ts",
32+
"vite.config.mts",
33+
"vitest.config.ts",
34+
"vitest.config.mts",
35+
"src/**/*.test.ts",
36+
"src/**/*.spec.ts",
37+
"src/**/*.test.tsx",
38+
"src/**/*.spec.tsx",
39+
"src/**/*.test.js",
40+
"src/**/*.spec.js",
41+
"src/**/*.test.jsx",
42+
"src/**/*.spec.jsx"
43+
]
44+
}

0 commit comments

Comments
 (0)