diff --git a/.github/workflows/lwa-github-ci-workflow.yml b/.github/workflows/lwa-github-ci-workflow.yml index 3da8e441..7592f42d 100644 --- a/.github/workflows/lwa-github-ci-workflow.yml +++ b/.github/workflows/lwa-github-ci-workflow.yml @@ -4,10 +4,12 @@ on: push: paths: - 'src/haapi-react-app/**' + - 'src/haapi-react-sdk/**' pull_request: types: [opened, synchronize, reopened, ready_for_review] paths: - 'src/haapi-react-app/**' + - 'src/haapi-react-sdk/**' workflow_dispatch: jobs: @@ -30,16 +32,28 @@ jobs: - name: Install Dependencies run: npm ci - - name: Check Formatting + - name: SDK — Check Formatting + run: npm run prettier-check -w src/haapi-react-sdk + + - name: SDK — Lint + run: npm run lint -w src/haapi-react-sdk + + - name: SDK — Typecheck + run: npm run typecheck -w src/haapi-react-sdk + + - name: SDK — Run Tests + run: npm run test -w src/haapi-react-sdk -- run + + - name: App — Check Formatting run: npm run prettier-check -w src/haapi-react-app - - name: Lint Code + - name: App — Lint run: npm run lint -w src/haapi-react-app - - name: Build Project + - name: App — Build Project run: npm run build:haapi-react-app - - name: Run Tests + - name: App — Run Tests run: npm run test -w src/haapi-react-app -- run notify-slack: diff --git a/README.md b/README.md index 6a4361cf..5c936231 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,12 @@ To deploy build artifacts (assets, templates, and messages) to a production envi ```shell npm run build ``` + + Or build specific projects with `npm run build:` to deploy only those, e.g.: + ```shell + npm run build:ssp + npm run build:haapi-react-app + ``` ### Deployment Options @@ -113,6 +119,7 @@ To deploy build artifacts (assets, templates, and messages) to a production envi This deploys: - Identity Server assets, templates, and messages - Self Service Portal to `templates/overrides` and `messages/overrides` +- HAAPI React App assets to the global `webroot/assets`, with its loader fragment in `templates/overrides` **Deploy to a specific template area:** ```shell @@ -121,6 +128,7 @@ This deploys: This deploys: - Identity Server assets, templates, and messages - Self Service Portal only to the specified template area (not to overrides) +- HAAPI React App assets to the global `webroot/assets`, with its loader fragment in the specified template area ### What Gets Deployed @@ -133,5 +141,9 @@ This deploys: - Templates → `${IDSVR_HOME}/usr/share/templates/overrides/apps/self-service-portal` or `template-areas/{area}/apps/self-service-portal` - Messages → `${IDSVR_HOME}/usr/share/messages/overrides/{language}/apps/self-service-portal` or `template-areas/{area}/{language}/apps/self-service-portal` +**HAAPI React App:** +- Assets → `${IDSVR_HOME}/usr/share/webroot/assets/{js,css}`, preserving content-hashed filenames so different builds can coexist +- Assets fragment → `${IDSVR_HOME}/usr/share/templates/overrides/fragments/api-driven-ui/assets.vm` or `template-areas/{area}/fragments/api-driven-ui/assets.vm`, rewritten to point at the deployed assets + ## License Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE). diff --git a/deploy.sh b/deploy.sh index f568fae9..9e75b4cf 100755 --- a/deploy.sh +++ b/deploy.sh @@ -7,7 +7,7 @@ # # Prerequisites: # - Set IDSVR_HOME environment variable to your Identity Server installation directory -# - Run the build process before deploying: npm run build +# - Run the build process before deploying # # Usage: # ./deploy.sh [template-area-name] @@ -63,6 +63,16 @@ copy_with_feedback() { fi } +# Exit with an error if the given template area does not exist +assert_template_area_exists() { + local area=$1 + + if [ ! -d "$IDSVR_HOME/usr/share/templates/template-areas/$area" ]; then + echo -e "${RED}Error: Template area '$area' does not exist in $IDSVR_HOME/usr/share/templates/template-areas/${NC}" + exit 1 + fi +} + echo "================================================" echo "Identity Server Deployment" echo "================================================" @@ -98,16 +108,11 @@ else if [ -n "$TEMPLATE_AREA" ]; then # Deploy to specific template area only echo -e "${YELLOW}Deploying Self Service Portal to template area: $TEMPLATE_AREA${NC}" + assert_template_area_exists "$TEMPLATE_AREA" USSP_TEMPLATE_PATH="$IDSVR_HOME/usr/share/templates/template-areas/$TEMPLATE_AREA/apps/self-service-portal" USSP_MESSAGES_BASE="$IDSVR_HOME/usr/share/messages/template-areas/$TEMPLATE_AREA" - # Check if template area exists - if [ ! -d "$IDSVR_HOME/usr/share/templates/template-areas/$TEMPLATE_AREA" ]; then - echo -e "${RED}Error: Template area '$TEMPLATE_AREA' does not exist in $IDSVR_HOME/usr/share/templates/template-areas/${NC}" - exit 1 - fi - mkdir -p "$USSP_TEMPLATE_PATH" cp src/self-service-portal/app/dist/index.html "$USSP_TEMPLATE_PATH/index.vm" cp -r src/self-service-portal/app/dist/assets "$USSP_TEMPLATE_PATH/" 2>/dev/null || true @@ -151,6 +156,56 @@ else fi fi +echo "" +echo "================================================" +echo "HAAPI React App Deployment" +echo "================================================" + +# HAAPI React App - Check if dist exists +if [ ! -d "src/haapi-react-app/dist/assets" ]; then + echo -e "${YELLOW}⚠ HAAPI React App build not found. Skipping HAAPI React App deployment.${NC}" + echo -e "${YELLOW} Run 'npm run build:haapi-react-app' first if you want to deploy the HAAPI React App.${NC}" +else + # Locate the content-hashed entry bundles produced by Vite + HAAPI_REACT_APP_JS_FILE=$(ls src/haapi-react-app/dist/assets/index-*.js 2>/dev/null | head -n1) + HAAPI_REACT_APP_CSS_FILE=$(ls src/haapi-react-app/dist/assets/index-*.css 2>/dev/null | head -n1) + + if [ -z "$HAAPI_REACT_APP_JS_FILE" ] || [ -z "$HAAPI_REACT_APP_CSS_FILE" ]; then + echo -e "${YELLOW}⚠ HAAPI React App build output is incomplete (missing JS or CSS bundle). Skipping HAAPI React App deployment.${NC}" + else + + # Assets always go to the global webroot. + # Rename the "index-" prefix to "api-driven-ui-" while keeping Vite's content hash, + # so the deployed assets are recognisable and different builds still coexist. + HAAPI_REACT_APP_JS_NAME="api-driven-ui-$(basename "$HAAPI_REACT_APP_JS_FILE" | sed 's/^index-//')" + HAAPI_REACT_APP_CSS_NAME="api-driven-ui-$(basename "$HAAPI_REACT_APP_CSS_FILE" | sed 's/^index-//')" + + HAAPI_WEBROOT_ASSETS="$IDSVR_HOME/usr/share/webroot/assets" + mkdir -p "$HAAPI_WEBROOT_ASSETS/js" "$HAAPI_WEBROOT_ASSETS/css" + cp "$HAAPI_REACT_APP_JS_FILE" "$HAAPI_WEBROOT_ASSETS/js/$HAAPI_REACT_APP_JS_NAME" + cp "$HAAPI_REACT_APP_CSS_FILE" "$HAAPI_WEBROOT_ASSETS/css/$HAAPI_REACT_APP_CSS_NAME" + echo -e "${GREEN}✓ HAAPI React App assets deployed to webroot/assets ($HAAPI_REACT_APP_JS_NAME, $HAAPI_REACT_APP_CSS_NAME)${NC}" + + # The asset-include fragment is overridable per template location. Always (re)write it to + # point at the hashed filenames just deployed (overrides by default, template area when given). + if [ -n "$TEMPLATE_AREA" ]; then + echo -e "${YELLOW}Deploying HAAPI React App asset fragment to template area: $TEMPLATE_AREA${NC}" + assert_template_area_exists "$TEMPLATE_AREA" + HAAPI_FRAGMENT_DIR="$IDSVR_HOME/usr/share/templates/template-areas/$TEMPLATE_AREA/fragments/api-driven-ui" + else + echo -e "${YELLOW}Deploying HAAPI React App asset fragment to overrides...${NC}" + HAAPI_FRAGMENT_DIR="$IDSVR_HOME/usr/share/templates/overrides/fragments/api-driven-ui" + fi + + mkdir -p "$HAAPI_FRAGMENT_DIR" + cat > "$HAAPI_FRAGMENT_DIR/assets.vm" < + +EOF + echo -e "${GREEN}✓ HAAPI React App asset fragment written to $HAAPI_FRAGMENT_DIR/assets.vm${NC}" + fi +fi + echo "" echo "================================================" echo -e "${GREEN}Deployment completed successfully!${NC}" diff --git a/package-lock.json b/package-lock.json index 76cf304d..a94abdda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "src/common/icons", "src/identity-server", "src/self-service-portal/app", + "src/haapi-react-sdk", "src/haapi-react-app" ], "devDependencies": { @@ -33,7 +34,9 @@ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.19.tgz", "integrity": "sha512-Pp2gAQXPZ2o7lt4j0IMwNRXqQ3pagxtDj5wctL5U2Lz4oV0ocDNlkgx4DpxfyKav4S/bePuI+SMqcBSUHLy9kg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@adobe/css-tools": { "version": "4.4.4", @@ -122,6 +125,8 @@ "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", @@ -136,6 +141,8 @@ "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": "20 || >=22" } @@ -146,6 +153,8 @@ "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", @@ -160,10 +169,22 @@ "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": "20 || >=22" } }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/@asamuzakjp/nwsapi": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", @@ -171,6 +192,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@astrojs/compiler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz", + "integrity": "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==", + "license": "MIT" + }, "node_modules/@astrojs/internal-helpers": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.8.0.tgz", @@ -254,17 +281,15 @@ } }, "node_modules/@astrojs/telemetry": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", - "integrity": "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", + "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==", "license": "MIT", "dependencies": { - "ci-info": "^4.2.0", - "debug": "^4.4.0", - "dlv": "^1.1.3", + "ci-info": "^4.4.0", "dset": "^3.1.4", - "is-docker": "^3.0.0", - "is-wsl": "^3.1.0", + "is-docker": "^4.0.0", + "is-wsl": "^3.1.1", "which-pm-runs": "^1.1.0" }, "engines": { @@ -272,9 +297,9 @@ } }, "node_modules/@astrojs/telemetry/node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -521,9 +546,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -574,6 +599,85 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz", + "integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cacheable/utils": "^2.4.1", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", + "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/@capsizecss/unpack": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", @@ -701,9 +805,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.15.tgz", - "integrity": "sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", "dev": true, "funding": [ { @@ -716,8 +820,13 @@ } ], "license": "MIT-0", - "engines": { - "node": ">=18" + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, "node_modules/@csstools/css-tokenizer": { @@ -761,6 +870,10 @@ "resolved": "src/haapi-react-app", "link": true }, + "node_modules/@curity/haapi-react-sdk": { + "resolved": "src/haapi-react-sdk", + "link": true + }, "node_modules/@curity/identityserver-haapi-web-driver": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@curity/identityserver-haapi-web-driver/-/identityserver-haapi-web-driver-1.5.0.tgz", @@ -806,9 +919,9 @@ "link": true }, "node_modules/@cypress/request": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", - "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-4.0.1.tgz", + "integrity": "sha512-y20e+e6dFYkOUUJLVUZTsJRuTiXZaUQ32WD+R/ux/HBybbTx4ge7cNINcua0pU8+SNkKuRbOF12mBmzuzM8n5w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -825,14 +938,13 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.14.1", + "qs": "^6.15.2", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" + "tunnel-agent": "^0.6.0" }, "engines": { - "node": ">= 6" + "node": ">= 14.17.0" } }, "node_modules/@cypress/xvfb": { @@ -1620,6 +1732,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@fastify/busboy": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", @@ -3476,6 +3606,14 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@mdx-js/mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", @@ -5261,6 +5399,20 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -5547,21 +5699,21 @@ } }, "node_modules/@types/react": { - "version": "19.1.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", - "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", - "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/resolve": { @@ -6406,20 +6558,6 @@ "node": ">= 14" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -6486,32 +6624,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6719,6 +6831,7 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -6733,15 +6846,15 @@ } }, "node_modules/astro": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/astro/-/astro-6.1.6.tgz", - "integrity": "sha512-pRsz+kYriwCV/AUcY/I9OVKtVHuYFs2DtCszAxprXded/kTE53nMwxfnK0Nf6FPfaX9vcUiLnigcSIhuFoKntA==", + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.4.8.tgz", + "integrity": "sha512-KK5lX90uU9EeVaTjINyj3sy9/NFXVa59aowaqbWBDDKLXZh4rr7GwIaCFYVetE22MJtsCNFerQXn0vlCLmpP/Q==", "license": "MIT", "dependencies": { - "@astrojs/compiler": "^3.0.1", - "@astrojs/internal-helpers": "0.8.0", - "@astrojs/markdown-remark": "7.1.0", - "@astrojs/telemetry": "3.3.0", + "@astrojs/compiler": "^4.0.0", + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/markdown-remark": "7.2.0", + "@astrojs/telemetry": "3.3.2", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", @@ -6752,17 +6865,19 @@ "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", - "devalue": "^5.6.3", + "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", + "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", @@ -6772,7 +6887,7 @@ "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", - "picomatch": "^4.0.3", + "picomatch": "^4.0.4", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", @@ -6781,13 +6896,12 @@ "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", - "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", - "unstorage": "^1.17.4", + "unstorage": "^1.17.5", "vfile": "^6.0.3", - "vite": "^7.3.1", + "vite": "^7.3.2", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", @@ -6809,11 +6923,58 @@ "sharp": "^0.34.0" } }, - "node_modules/astro/node_modules/@astrojs/compiler": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-3.0.1.tgz", - "integrity": "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA==", - "license": "MIT" + "node_modules/astro/node_modules/@astrojs/internal-helpers": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz", + "integrity": "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.1.1", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/astro/node_modules/@astrojs/markdown-remark": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.0.tgz", + "integrity": "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/astro/node_modules/@astrojs/prism": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", + "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } }, "node_modules/astro/node_modules/aria-query": { "version": "5.3.2", @@ -7334,6 +7495,32 @@ "node": ">= 0.8" } }, + "node_modules/cacheable": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", + "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cacheable/memory": "^2.0.8", + "@cacheable/utils": "^2.4.1", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/cachedir": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", @@ -7637,29 +7824,6 @@ "node": ">=8" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cli-table3": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", @@ -7676,23 +7840,6 @@ "colors": "1.4.0" } }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -7771,6 +7918,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -8103,6 +8258,17 @@ "uncrypto": "^0.1.3" } }, + "node_modules/css-functions-list": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.3.3.tgz", + "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -8120,13 +8286,13 @@ } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -8203,6 +8369,8 @@ "integrity": "sha512-zDMqXh8Vs1CdRYZQ2M633m/SFgcjlu8RB8b/1h82i+6vpArF507NSYIWJHGlJaTWoS+imcnctmEz43txhbVkOw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@asamuzakjp/css-color": "^4.0.3", "@csstools/css-syntax-patches-for-csstree": "^1.0.14", @@ -8219,14 +8387,14 @@ "license": "MIT" }, "node_modules/cypress": { - "version": "15.11.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.11.0.tgz", - "integrity": "sha512-NXDE6/fqZuzh1Zr53nyhCCa4lcANNTYWQNP9fJO+tzD3qVTDaTUni5xXMuigYjMujQ7CRiT9RkJJONmPQSsDFw==", + "version": "15.18.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.18.0.tgz", + "integrity": "sha512-aLfOYSLlVt1b6QSoVUjbCY27taZlYAT8ST47xQbwd9pvQrY/g5gXi12yItZTB+kxkkj+ZcvUYmRLUC95SlCJsw==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@cypress/request": "^3.0.10", + "@cypress/request": "^4.0.0", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -8235,25 +8403,21 @@ "blob-util": "^2.0.2", "bluebird": "^3.7.2", "buffer": "^5.7.1", - "cachedir": "^2.3.0", + "cachedir": "^2.4.0", "chalk": "^4.1.0", "ci-info": "^4.1.0", - "cli-cursor": "^3.1.0", "cli-table3": "0.6.1", "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", "debug": "^4.3.4", - "enquirer": "^2.3.6", "eventemitter2": "6.4.7", "execa": "4.1.0", "executable": "^4.1.1", - "extract-zip": "2.0.1", - "figures": "^3.2.0", "fs-extra": "^9.1.0", "hasha": "5.2.2", "is-installed-globally": "~0.4.0", - "listr2": "^3.8.3", + "listr2": "^9.0.5", "lodash": "^4.17.23", "log-symbols": "^4.0.0", "minimist": "^1.2.8", @@ -8268,7 +8432,7 @@ "tree-kill": "1.2.2", "tslib": "1.14.1", "untildify": "^4.0.0", - "yauzl": "^2.10.0" + "yauzl": "^3.3.1" }, "bin": { "cypress": "bin/cypress" @@ -8277,34 +8441,6 @@ "node": "^20.1.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/cypress/node_modules/listr2": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", - "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.1", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, "node_modules/cypress/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -8328,22 +8464,17 @@ "dev": true, "license": "0BSD" }, - "node_modules/cypress/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/cypress/node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "pend": "~1.2.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=12" } }, "node_modules/dashdash": { @@ -8375,6 +8506,8 @@ "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" @@ -8676,9 +8809,9 @@ } }, "node_modules/devalue": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", - "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", "license": "MIT" }, "node_modules/devlop": { @@ -8716,12 +8849,6 @@ "node": ">=8" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -9051,20 +9178,6 @@ } } }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -9195,6 +9308,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -10040,6 +10160,17 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -10134,32 +10265,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -10337,17 +10442,17 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -10470,9 +10575,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", "engines": { @@ -10561,6 +10666,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.4", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", + "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -10650,6 +10770,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", @@ -10700,6 +10872,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -10973,10 +11153,24 @@ "node": ">=8" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -11295,6 +11489,14 @@ "react-is": "^16.7.0" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -11440,37 +11642,6 @@ "node": ">=8.12.0" } }, - "node_modules/i18next": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.0.tgz", - "integrity": "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.6" - }, - "peerDependencies": { - "typescript": "^5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -11575,6 +11746,18 @@ "node": ">=8" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -11858,15 +12041,15 @@ } }, "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -11981,6 +12164,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-installed-globally": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", @@ -12429,6 +12627,8 @@ "integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.19", "@asamuzakjp/dom-selector": "^6.7.3", @@ -12469,6 +12669,8 @@ "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tldts-core": "^7.0.17" }, @@ -12481,7 +12683,9 @@ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/jsdom/node_modules/tough-cookie": { "version": "6.0.0", @@ -12489,6 +12693,8 @@ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "dependencies": { "tldts": "^7.0.5" }, @@ -12582,6 +12788,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -12621,6 +12833,17 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", @@ -13035,6 +13258,14 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -13052,43 +13283,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -13526,9 +13720,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, "node_modules/memorystream": { @@ -15241,22 +15435,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-queue": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.2.tgz", @@ -15399,18 +15577,31 @@ } }, "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^6.0.0" + "entities": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -15712,13 +15903,41 @@ "postcss": "^8.2" } }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { + "peer": true, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" }, @@ -15885,6 +16104,28 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/qr.js": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", @@ -15892,9 +16133,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -15991,36 +16232,33 @@ } }, "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.5" + "react": "^19.2.7" } }, "node_modules/react-error-boundary": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz", - "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.2.tgz", + "integrity": "sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, "peerDependencies": { - "react": ">=16.13.1" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/react-hot-toast": { @@ -16040,33 +16278,6 @@ "react-dom": ">=16" } }, - "node_modules/react-i18next": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.2.4.tgz", - "integrity": "sha512-pvbcPQ+YuQQoRkKBA4VCU9aO8dOgP/vdKEizIYXcAk3+AmI8yQKSJaCzxQQu4Kgg2zWZm3ax9KqHv8ItUlRY0A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.6", - "html-parse-stringify": "^3.0.1", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "i18next": ">= 25.5.2", - "react": ">= 16.8.0", - "typescript": "^5" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -16118,9 +16329,9 @@ } }, "node_modules/react-router": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", - "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -16680,6 +16891,15 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resp-modifier": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", @@ -16710,20 +16930,6 @@ "dev": true, "license": "MIT" }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/retext": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", @@ -17647,18 +17853,22 @@ } }, "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/smob": { @@ -18219,55 +18429,6 @@ "inline-style-parser": "0.2.7" } }, - "node_modules/stylelint-config-recommended": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-17.0.0.tgz", - "integrity": "sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/stylelint" - }, - { - "type": "github", - "url": "https://github.com/sponsors/stylelint" - } - ], - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "stylelint": "^16.23.0" - } - }, - "node_modules/stylelint-config-standard": { - "version": "39.0.1", - "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-39.0.1.tgz", - "integrity": "sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/stylelint" - }, - { - "type": "github", - "url": "https://github.com/sponsors/stylelint" - } - ], - "license": "MIT", - "dependencies": { - "stylelint-config-recommended": "^17.0.0" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "stylelint": "^16.23.0" - } - }, "node_modules/suf-log": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", @@ -18302,6 +18463,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true, + "peer": true + }, "node_modules/svgo": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", @@ -18423,6 +18591,50 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -18472,13 +18684,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, - "license": "MIT" - }, "node_modules/timeout-signal": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/timeout-signal/-/timeout-signal-2.0.0.tgz", @@ -18744,6 +18949,7 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, "license": "MIT", "bin": { "tsconfck": "bin/tsconfck.js" @@ -18799,19 +19005,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -18997,6 +19190,16 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", @@ -19004,6 +19207,20 @@ "devOptional": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -19492,16 +19709,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -19914,9 +20121,9 @@ } }, "node_modules/webidl-conversions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", - "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -19952,6 +20159,8 @@ "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" @@ -20329,7 +20538,7 @@ "@curity/ui-kit-icons": "^1.0.0", "@radix-ui/react-dialog": "^1.1.15", "react": "^19.2.0", - "react-router": "^7.9.4" + "react-router": "^7.18.0" }, "devDependencies": { "@eslint/js": "^9.9.0", @@ -20344,8 +20553,8 @@ "eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-simple-import-sort": "^12.1.1", "globals": "^16.4.0", - "jsdom": "^27.0.1", - "stylelint-config-standard": "^39.0.1", + "jsdom": "^29.1.1", + "stylelint-config-standard": "^40.0.0", "typescript": "^5.7.2", "typescript-eslint": "^8.18.0", "vite": "^7.1.12", @@ -20358,598 +20567,2773 @@ "npm": ">=10.9.4" }, "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "react": "^19.0.0", + "react-dom": "^19.0.0" } }, - "src/common/components/app": { - "name": "@curity/ui-kit-react-component-library", - "version": "0.0.0-SNAPSHOT-1", - "extraneous": true, - "license": "Apache-2.0", + "src/common/component-library/node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", "dependencies": { - "@curity/ui-kit-css": "^1.0.0", - "@curity/ui-kit-icons": "^1.0.0", - "@reach/dialog": "^0.18.0", - "i18next": "^25.6.0", - "react": "^19.2.0", - "react-hot-toast": "^2.5.2", - "react-i18next": "^16.1.5", - "react-qr-code": "^2.0.15" - }, - "devDependencies": { - "@eslint/js": "^9.9.0", - "@parcel/watcher": "^2.5.0", - "@testing-library/dom": "^10.4.0", - "@testing-library/react": "^16.1.0", - "@types/react": "^19.0.1", - "@vitejs/plugin-basic-ssl": "^2.1.0", - "@vitejs/plugin-react": "^5.0.4", - "eslint": "^9.16.0", - "eslint-plugin-react-hooks": "^7.0.0", - "eslint-plugin-react-refresh": "^0.4.16", - "eslint-plugin-simple-import-sort": "^12.1.1", - "globals": "^16.4.0", - "jsdom": "^27.0.1", - "stylelint-config-standard": "^39.0.1", - "typescript": "^5.7.2", - "typescript-eslint": "^8.18.0", - "vite": "^7.1.12", - "vite-tsconfig-paths": "^5.1.4", - "vitest": "^4.0.1" + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": ">=22.12.0" - } - }, - "src/common/css": { - "name": "@curity/ui-kit-css", - "version": "1.0.0", - "license": "Apache-2.0", - "dependencies": { - "npm-run-all": "^4.1.5" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "src/common/css/docs": { - "name": "@curity/ui-kit-css-docs", - "version": "1.0.0", - "license": "Apache-2.0", + "src/common/component-library/node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@astrojs/mdx": "^5.0.3", - "@astrojs/partytown": "^2.1.7", - "@astrojs/react": "^5.0.3", - "@curity/ui-kit-css": "*", - "@curity/ui-kit-icons": "*", - "astro": "^6.1.6", - "mdast-util-to-string": "3.2.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "rollup": "^4.40.0" + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" }, - "devDependencies": { - "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.2", - "@typescript/twoslash": "^3.2.9", - "prettier": "^3.5.3", - "prettier-plugin-astro": "^0.12.3" + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "src/common/css/docs/node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "src/common/component-library/node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "src/common/component-library/node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "src/common/component-library/node_modules/@csstools/css-color-parser": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", + "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "src/common/component-library/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "src/common/component-library/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "src/common/component-library/node_modules/@csstools/media-query-list-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.0.tgz", + "integrity": "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "src/common/component-library/node_modules/@csstools/selector-resolve-nested": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-4.0.0.tgz", + "integrity": "sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "src/common/component-library/node_modules/@csstools/selector-specificity": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "src/common/component-library/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "src/common/component-library/node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/common/component-library/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "src/common/component-library/node_modules/file-entry-cache": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz", + "integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^6.1.22" + } + }, + "src/common/component-library/node_modules/flat-cache": { + "version": "6.1.22", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz", + "integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cacheable": "^2.3.4", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "src/common/component-library/node_modules/globby": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", + "integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/common/component-library/node_modules/has-flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", + "integrity": "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/common/component-library/node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/common/component-library/node_modules/html-tags": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-5.1.0.tgz", + "integrity": "sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/common/component-library/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "src/common/component-library/node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/common/component-library/node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "src/common/component-library/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "src/common/component-library/node_modules/mathml-tag-names": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-4.0.0.tgz", + "integrity": "sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "src/common/component-library/node_modules/meow": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", + "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/common/component-library/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "src/common/component-library/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "src/common/component-library/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "src/common/component-library/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/common/component-library/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/common/component-library/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "src/common/component-library/node_modules/stylelint": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.13.0.tgz", + "integrity": "sha512-G1WYzMerp7ihOaIe9VJCHLt12MoAD2QLf1AFerYP37+BCRBUK5UCpq8e/mN+zCIaJPKQcaxhE4WlPmqdiOx/gw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@csstools/css-calc": "^3.2.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-syntax-patches-for-csstree": "^1.1.4", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/media-query-list-parser": "^5.0.0", + "@csstools/selector-resolve-nested": "^4.0.0", + "@csstools/selector-specificity": "^6.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.1", + "css-functions-list": "^3.3.3", + "css-tree": "^3.2.1", + "debug": "^4.4.3", + "fast-glob": "^3.3.3", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^11.1.3", + "global-modules": "^2.0.0", + "globby": "^16.2.0", + "globjoin": "^0.1.4", + "html-tags": "^5.1.0", + "ignore": "^7.0.5", + "import-meta-resolve": "^4.2.0", + "mathml-tag-names": "^4.0.0", + "meow": "^14.1.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.15", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.1.1", + "postcss-value-parser": "^4.2.0", + "string-width": "^8.2.1", + "supports-hyperlinks": "^4.4.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^7.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "src/common/component-library/node_modules/stylelint-config-recommended": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-18.0.0.tgz", + "integrity": "sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "stylelint": "^17.0.0" + } + }, + "src/common/component-library/node_modules/stylelint-config-standard": { + "version": "40.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-40.0.0.tgz", + "integrity": "sha512-EznGJxOUhtWck2r6dJpbgAdPATIzvpLdK9+i5qPd4Lx70es66TkBPljSg4wN3Qnc6c4h2n+WbUrUynQ3fanjHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "stylelint-config-recommended": "^18.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "stylelint": "^17.0.0" + } + }, + "src/common/component-library/node_modules/stylelint/node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "src/common/component-library/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "src/common/component-library/node_modules/supports-hyperlinks": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz", + "integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^5.0.1", + "supports-color": "^10.2.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "src/common/component-library/node_modules/tldts": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.4.tgz", + "integrity": "sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.4" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "src/common/component-library/node_modules/tldts-core": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.4.tgz", + "integrity": "sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==", + "dev": true, + "license": "MIT" + }, + "src/common/component-library/node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "src/common/component-library/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "src/common/component-library/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/common/component-library/node_modules/write-file-atomic": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz", + "integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "src/common/components/app": { + "name": "@curity/ui-kit-react-component-library", + "version": "0.0.0-SNAPSHOT-1", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@curity/ui-kit-css": "^1.0.0", + "@curity/ui-kit-icons": "^1.0.0", + "@reach/dialog": "^0.18.0", + "i18next": "^25.6.0", + "react": "^19.2.0", + "react-hot-toast": "^2.5.2", + "react-i18next": "^16.1.5", + "react-qr-code": "^2.0.15" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@parcel/watcher": "^2.5.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.1.0", + "@types/react": "^19.0.1", + "@vitejs/plugin-basic-ssl": "^2.1.0", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.16.0", + "eslint-plugin-react-hooks": "^7.0.0", + "eslint-plugin-react-refresh": "^0.4.16", + "eslint-plugin-simple-import-sort": "^12.1.1", + "globals": "^16.4.0", + "jsdom": "^27.0.1", + "stylelint-config-standard": "^39.0.1", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0", + "vite": "^7.1.12", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.0.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "src/common/css": { + "name": "@curity/ui-kit-css", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "npm-run-all": "^4.1.5" + } + }, + "src/common/css/docs": { + "name": "@curity/ui-kit-css-docs", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@astrojs/mdx": "^5.0.3", + "@astrojs/partytown": "^2.1.7", + "@astrojs/react": "^5.0.3", + "@curity/ui-kit-css": "*", + "@curity/ui-kit-icons": "*", + "astro": "^6.4.6", + "mdast-util-to-string": "3.2.0", + "react": "^19.2.0", + "react-dom": "^19.2.7", + "rollup": "^4.40.0" + }, + "devDependencies": { + "@types/react": "^19.0.1", + "@types/react-dom": "^19.2.3", + "@typescript/twoslash": "^3.2.9", + "prettier": "^3.5.3", + "prettier-plugin-astro": "^0.12.3" + } + }, + "src/common/css/lib": { + "name": "@curity/ui-kit-css-lib", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "concurrently": "^8.2.0" + }, + "devDependencies": { + "prettier": "^3.4.2", + "vite": "^7.3.1" + } + }, + "src/common/css/node_modules/@astrojs/compiler": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-1.8.2.tgz", + "integrity": "sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==", + "dev": true, + "license": "MIT" + }, + "src/common/css/node_modules/@astrojs/mdx": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-5.0.3.tgz", + "integrity": "sha512-zv/OlM5sZZvyjHqJjR3FjJvoCgbxdqj3t4jO/gSEUNcck3BjdtMgNQw8UgPfAGe4yySdG4vjZ3OC5wUxhu7ckg==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "7.1.0", + "@mdx-js/mdx": "^3.1.1", + "acorn": "^8.16.0", + "es-module-lexer": "^2.0.0", + "estree-util-visit": "^2.0.0", + "hast-util-to-html": "^9.0.5", + "piccolore": "^0.1.3", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.2", + "source-map": "^0.7.6", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "astro": "^6.0.0" + } + }, + "src/common/css/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "src/common/css/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "src/common/css/node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "src/common/css/node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT" + }, + "src/common/css/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "src/common/css/node_modules/prettier-plugin-astro": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.12.3.tgz", + "integrity": "sha512-GthUSu3zCvmtVyqlArosez0xE08vSJ0R1sWurxIWpABaCkNGYFANoUdFkqmIo54EV2uPLGcVJzOucWvCjPBWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^1.5.5", + "prettier": "^3.0.0", + "sass-formatter": "^0.7.6" + }, + "engines": { + "node": "^14.15.0 || >=16.0.0" + } + }, + "src/common/css/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "src/common/css/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "src/common/icons": { + "name": "@curity/ui-kit-icons", + "version": "1.0.0", + "license": "Apache-2.0", + "devDependencies": { + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-commonjs": "^25.0.8", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-terser": "^1.0.0", + "@rollup/plugin-typescript": "^11.1.6", + "rollup": "^4.28.1", + "rollup-plugin-dts": "^6.1.1", + "typescript": "^5.7.2" + } + }, + "src/haapi-react-app": { + "name": "@curity/haapi-react-app", + "version": "0.0.0", + "license": "Apache-2.0", + "dependencies": { + "@curity/haapi-react-sdk": "*", + "@curity/identityserver-haapi-web-driver": "^1.3.0", + "@curity/ui-kit-icons": "^1.0.0", + "react": "^19.0.0", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@eslint-react/eslint-plugin": "^1.43.0", + "@eslint/js": "^9.21.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.0.3", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-basic-ssl": "^2.1.0", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "jsdom": "^29.1.1", + "postcss-extend-rule": "^4.0.0", + "postcss-import": "^16.1.1", + "prettier": "^3.5.3", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^7.3.1", + "vitest": "^4.1.8" + }, + "engines": { + "node": ">=22.22.0", + "npm": ">=10.9.4" + } + }, + "src/haapi-react-app/node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/haapi-react-app/node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/haapi-react-app/node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "src/haapi-react-app/node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "src/haapi-react-app/node_modules/@csstools/css-color-parser": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", + "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "src/haapi-react-app/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "src/haapi-react-app/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "src/haapi-react-app/node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/haapi-react-app/node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "src/haapi-react-app/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/haapi-react-app/node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/haapi-react-app/node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "src/haapi-react-app/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "src/haapi-react-app/node_modules/tldts": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.4.tgz", + "integrity": "sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.4" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "src/haapi-react-app/node_modules/tldts-core": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.4.tgz", + "integrity": "sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==", + "dev": true, + "license": "MIT" + }, + "src/haapi-react-app/node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "src/haapi-react-app/node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "src/haapi-react-app/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "src/haapi-react-app/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/haapi-react-sdk": { + "name": "@curity/haapi-react-sdk", + "version": "0.0.0", + "license": "Apache-2.0", + "dependencies": { + "@curity/identityserver-haapi-web-driver": "^1.3.0", + "@curity/ui-kit-icons": "^1.0.0" + }, + "devDependencies": { + "@eslint-react/eslint-plugin": "^1.43.0", + "@eslint/js": "^9.21.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.0.3", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^5.0.4", + "es-module-lexer": "^1.7.0", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "jsdom": "^26.1.0", + "prettier": "^3.5.3", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^7.3.1", + "vitest": "~4.0.6" + }, + "engines": { + "node": ">=22.22.0", + "npm": ">=10.9.4" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "src/haapi-react-sdk/node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "src/haapi-react-sdk/node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/haapi-react-sdk/node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/haapi-react-sdk/node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/haapi-react-sdk/node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/haapi-react-sdk/node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/haapi-react-sdk/node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/haapi-react-sdk/node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "src/haapi-react-sdk/node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "src/haapi-react-sdk/node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "src/haapi-react-sdk/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/haapi-react-sdk/node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "src/haapi-react-sdk/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "src/haapi-react-sdk/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "src/haapi-react-sdk/node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "src/haapi-react-sdk/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "src/haapi-react-sdk/node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "src/haapi-react-sdk/node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "src/haapi-react-sdk/node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "src/haapi-react-sdk/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "src/haapi-react-sdk/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "src/identity-server": { + "name": "@curity/ui-kit-identity-server", + "version": "1.6.0", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "jquery": "3.5.1" + }, + "devDependencies": { + "browser-sync": "^3.0.2", + "concurrently": "^8.2.2", + "extract-zip": "^2.0.1", + "node-env-file": "^0.1.8", + "vite": "^7.1.5", + "vite-plugin-static-copy": "^3.1.2" + }, + "engines": { + "node": ">=22.22.0", + "npm": ">=10.9.4" + } + }, + "src/identity-server/node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "src/identity-server/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "src/self-service-portal/app": { + "name": "@curity/ui-kit-self-service-portal", + "version": "0.0.0-SNAPSHOT-1", + "license": "Apache-2.0", + "dependencies": { + "@apollo/client": "^3.12.3", + "@curity/token-handler-js-assistant": "^1.2.2", + "@curity/ui-kit-component-library": "^1.0.0", + "@curity/ui-kit-css": "^1.0.0", + "@curity/ui-kit-icons": "^1.0.0", + "graphql": "^16.11.0", + "i18next": "^26.2.0", + "react": "^19.2.0", + "react-dom": "^19.2.7", + "react-error-boundary": "^6.1.2", + "react-hot-toast": "^2.5.2", + "react-i18next": "^17.0.8", + "react-qr-code": "^2.0.15", + "react-router": "^7.18.0", + "react-verification-input": "^4.2.2" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@graphql-codegen/cli": "^6.0.1", + "@graphql-codegen/client-preset": "^5.1.0", + "@parcel/watcher": "^2.5.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-basic-ssl": "^2.1.0", + "@vitejs/plugin-react": "^5.0.4", + "cypress": "^15.17.0", + "eslint": "^9.16.0", + "eslint-plugin-react-hooks": "^7.0.0", + "eslint-plugin-react-refresh": "^0.4.16", + "eslint-plugin-simple-import-sort": "^12.1.1", + "globals": "^16.4.0", + "jsdom": "^29.1.1", + "msw": "^2.12.2", + "stylelint-config-standard": "^40.0.0", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0", + "vite": "^7.3.1", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.0.1" + }, + "engines": { + "node": ">=22.22.0", + "npm": ">=10.9.4" + } + }, + "src/self-service-portal/app/node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/self-service-portal/app/node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/self-service-portal/app/node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "src/self-service-portal/app/node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "src/self-service-portal/app/node_modules/@csstools/css-color-parser": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", + "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "csstype": "^3.2.2" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "src/common/css/docs/node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "src/self-service-portal/app/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, "peerDependencies": { - "@types/react": "^19.2.0" + "@csstools/css-tokenizer": "^4.0.0" } }, - "src/common/css/lib": { - "name": "@curity/ui-kit-css-lib", - "version": "1.0.0", - "license": "Apache-2.0", + "src/self-service-portal/app/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "src/self-service-portal/app/node_modules/@csstools/media-query-list-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.0.tgz", + "integrity": "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "src/self-service-portal/app/node_modules/@csstools/selector-resolve-nested": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-4.0.0.tgz", + "integrity": "sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "src/self-service-portal/app/node_modules/@csstools/selector-specificity": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "src/self-service-portal/app/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "src/self-service-portal/app/node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", "dependencies": { - "concurrently": "^8.2.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/self-service-portal/app/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "src/self-service-portal/app/node_modules/file-entry-cache": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz", + "integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^6.1.22" + } + }, + "src/self-service-portal/app/node_modules/flat-cache": { + "version": "6.1.22", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz", + "integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cacheable": "^2.3.4", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "src/self-service-portal/app/node_modules/globby": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", + "integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/self-service-portal/app/node_modules/has-flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", + "integrity": "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/self-service-portal/app/node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "src/self-service-portal/app/node_modules/html-tags": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-5.1.0.tgz", + "integrity": "sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/self-service-portal/app/node_modules/i18next": { + "version": "26.3.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.2.tgz", + "integrity": "sha512-QQkXAM1sPDHqhxMQuBeHVMUn6mJchF+wdpOoQerciLAFqO3ZYdxO0EUbeEhruyutnNwpUQIITDVzLjwnNL0T1w==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "src/self-service-portal/app/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "src/self-service-portal/app/node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" }, - "devDependencies": { - "prettier": "^3.4.2", - "vite": "^7.3.1" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "src/common/css/node_modules/@astrojs/compiler": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-1.8.2.tgz", - "integrity": "sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==", + "src/self-service-portal/app/node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "dev": true, - "license": "MIT" - }, - "src/common/css/node_modules/@astrojs/mdx": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-5.0.3.tgz", - "integrity": "sha512-zv/OlM5sZZvyjHqJjR3FjJvoCgbxdqj3t4jO/gSEUNcck3BjdtMgNQw8UgPfAGe4yySdG4vjZ3OC5wUxhu7ckg==", "license": "MIT", "dependencies": { - "@astrojs/markdown-remark": "7.1.0", - "@mdx-js/mdx": "^3.1.1", - "acorn": "^8.16.0", - "es-module-lexer": "^2.0.0", - "estree-util-visit": "^2.0.0", - "hast-util-to-html": "^9.0.5", - "piccolore": "^0.1.3", - "rehype-raw": "^7.0.0", - "remark-gfm": "^4.0.1", - "remark-smartypants": "^3.0.2", - "source-map": "^0.7.6", - "unist-util-visit": "^5.1.0", - "vfile": "^6.0.3" + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { - "astro": "^6.0.0" + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "src/common/css/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" + "src/self-service-portal/app/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, - "src/common/css/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "src/common/css/node_modules/concurrently": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", - "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "src/self-service-portal/app/node_modules/mathml-tag-names": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-4.0.0.tgz", + "integrity": "sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==", + "dev": true, "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "date-fns": "^2.30.0", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "spawn-command": "0.0.2", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": "^14.13.0 || >=16.0.0" - }, + "peer": true, "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "src/common/css/node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" - }, - "src/common/css/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "src/self-service-portal/app/node_modules/meow": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", + "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0" + "peer": true, + "engines": { + "node": ">=20" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "src/common/css/node_modules/prettier-plugin-astro": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.12.3.tgz", - "integrity": "sha512-GthUSu3zCvmtVyqlArosez0xE08vSJ0R1sWurxIWpABaCkNGYFANoUdFkqmIo54EV2uPLGcVJzOucWvCjPBWvg==", + "src/self-service-portal/app/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", - "dependencies": { - "@astrojs/compiler": "^1.5.5", - "prettier": "^3.0.0", - "sass-formatter": "^0.7.6" - }, + "peer": true, "engines": { - "node": "^14.15.0 || >=16.0.0" + "node": ">=0.10.0" } }, - "src/common/css/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", + "src/self-service-portal/app/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, "engines": { - "node": ">= 12" + "node": ">=4" } }, - "src/common/css/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "src/self-service-portal/app/node_modules/react-i18next": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", + "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } } }, - "src/common/icons": { - "name": "@curity/ui-kit-icons", - "version": "1.0.0", - "license": "Apache-2.0", - "devDependencies": { - "@rollup/plugin-babel": "^6.0.4", - "@rollup/plugin-commonjs": "^25.0.8", - "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-terser": "^1.0.0", - "@rollup/plugin-typescript": "^11.1.6", - "rollup": "^4.28.1", - "rollup-plugin-dts": "^6.1.1", - "typescript": "^5.7.2" + "src/self-service-portal/app/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "src/haapi-react-app": { - "name": "@curity/haapi-react-app", - "version": "0.0.0", - "license": "Apache-2.0", - "dependencies": { - "@curity/identityserver-haapi-web-driver": "^1.3.0", - "@curity/ui-kit-icons": "^1.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@eslint-react/eslint-plugin": "^1.43.0", - "@eslint/js": "^9.21.0", - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.8.0", - "@testing-library/react": "^16.3.0", - "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.0.3", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "@vitejs/plugin-basic-ssl": "^2.1.0", - "@vitejs/plugin-react": "^5.0.4", - "eslint": "^9.21.0", - "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^15.15.0", - "jsdom": "^26.1.0", - "postcss-extend-rule": "^4.0.0", - "postcss-import": "^16.1.1", - "prettier": "^3.5.3", - "typescript": "~5.7.2", - "typescript-eslint": "^8.24.1", - "vite": "^7.3.1", - "vitest": "^4.1.8" - }, + "src/self-service-portal/app/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "peer": true, "engines": { - "node": ">=22.22.0", - "npm": ">=10.9.4" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "src/haapi-react-app/node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "src/self-service-portal/app/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "src/haapi-react-app/node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "src/self-service-portal/app/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "src/haapi-react-app/node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "src/self-service-portal/app/node_modules/stylelint": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.13.0.tgz", + "integrity": "sha512-G1WYzMerp7ihOaIe9VJCHLt12MoAD2QLf1AFerYP37+BCRBUK5UCpq8e/mN+zCIaJPKQcaxhE4WlPmqdiOx/gw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" + "peer": true, + "dependencies": { + "@csstools/css-calc": "^3.2.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-syntax-patches-for-csstree": "^1.1.4", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/media-query-list-parser": "^5.0.0", + "@csstools/selector-resolve-nested": "^4.0.0", + "@csstools/selector-specificity": "^6.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.1", + "css-functions-list": "^3.3.3", + "css-tree": "^3.2.1", + "debug": "^4.4.3", + "fast-glob": "^3.3.3", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^11.1.3", + "global-modules": "^2.0.0", + "globby": "^16.2.0", + "globjoin": "^0.1.4", + "html-tags": "^5.1.0", + "ignore": "^7.0.5", + "import-meta-resolve": "^4.2.0", + "mathml-tag-names": "^4.0.0", + "meow": "^14.1.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.15", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.1.1", + "postcss-value-parser": "^4.2.0", + "string-width": "^8.2.1", + "supports-hyperlinks": "^4.4.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^7.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, - "src/haapi-react-app/node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "src/self-service-portal/app/node_modules/stylelint-config-recommended": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-18.0.0.tgz", + "integrity": "sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], "license": "MIT", "engines": { - "node": ">=10" + "node": ">=20.19.0" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "stylelint": "^17.0.0" } }, - "src/haapi-react-app/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "src/self-service-portal/app/node_modules/stylelint-config-standard": { + "version": "40.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-40.0.0.tgz", + "integrity": "sha512-EznGJxOUhtWck2r6dJpbgAdPATIzvpLdK9+i5qPd4Lx70es66TkBPljSg4wN3Qnc6c4h2n+WbUrUynQ3fanjHw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], "license": "MIT", + "dependencies": { + "stylelint-config-recommended": "^18.0.0" + }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "stylelint": "^17.0.0" } }, - "src/haapi-react-app/node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "src/self-service-portal/app/node_modules/stylelint/node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" }, "engines": { - "node": ">=18" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "canvas": "^3.0.0" + "typescript": ">=4.9.5" }, "peerDependenciesMeta": { - "canvas": { + "typescript": { "optional": true } } }, - "src/haapi-react-app/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "src/haapi-react-app/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "src/self-service-portal/app/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "dev": true, "license": "MIT", - "dependencies": { - "entities": "^6.0.0" + "peer": true, + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "src/haapi-react-app/node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "src/self-service-portal/app/node_modules/supports-hyperlinks": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz", + "integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "punycode": "^2.3.1" + "has-flag": "^5.0.1", + "supports-color": "^10.2.2" }, "engines": { - "node": ">=18" - } - }, - "src/haapi-react-app/node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "node": ">=20" }, - "engines": { - "node": ">=14.17" - } - }, - "src/haapi-react-app/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, - "src/haapi-react-app/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "src/self-service-portal/app/node_modules/tldts": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.4.tgz", + "integrity": "sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" + "tldts-core": "^7.4.4" }, - "engines": { - "node": ">=18" + "bin": { + "tldts": "bin/cli.js" } }, - "src/identity-server": { - "name": "@curity/ui-kit-identity-server", - "version": "1.6.0", - "hasInstallScript": true, - "license": "Apache-2.0", + "src/self-service-portal/app/node_modules/tldts-core": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.4.tgz", + "integrity": "sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==", + "dev": true, + "license": "MIT" + }, + "src/self-service-portal/app/node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "jquery": "3.5.1" - }, - "devDependencies": { - "browser-sync": "^3.0.2", - "concurrently": "^8.2.2", - "extract-zip": "^2.0.1", - "node-env-file": "^0.1.8", - "vite": "^7.1.5", - "vite-plugin-static-copy": "^3.1.2" + "tldts": "^7.0.5" }, "engines": { - "node": ">=22.22.0", - "npm": ">=10.9.4" + "node": ">=16" } }, - "src/identity-server/node_modules/concurrently": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", - "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "src/self-service-portal/app/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "date-fns": "^2.30.0", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "spawn-command": "0.0.2", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, "engines": { - "node": "^14.13.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + "node": ">=20" } }, - "src/identity-server/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "src/self-service-portal/app/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "src/self-service-portal/app": { - "name": "@curity/ui-kit-self-service-portal", - "version": "0.0.0-SNAPSHOT-1", - "license": "Apache-2.0", + "src/self-service-portal/app/node_modules/write-file-atomic": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz", + "integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==", + "dev": true, + "license": "ISC", + "peer": true, "dependencies": { - "@apollo/client": "^3.12.3", - "@curity/token-handler-js-assistant": "^1.2.2", - "@curity/ui-kit-component-library": "^1.0.0", - "@curity/ui-kit-css": "^1.0.0", - "@curity/ui-kit-icons": "^1.0.0", - "graphql": "^16.11.0", - "i18next": "^25.6.0", - "react": "^19.2.0", - "react-dom": "^19.2.4", - "react-error-boundary": "^6.0.0", - "react-hot-toast": "^2.5.2", - "react-i18next": "^16.1.5", - "react-qr-code": "^2.0.15", - "react-router": "^7.13.1", - "react-verification-input": "^4.2.2" - }, - "devDependencies": { - "@eslint/js": "^9.9.0", - "@graphql-codegen/cli": "^6.0.1", - "@graphql-codegen/client-preset": "^5.1.0", - "@parcel/watcher": "^2.5.0", - "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.1.0", - "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.2", - "@vitejs/plugin-basic-ssl": "^2.1.0", - "@vitejs/plugin-react": "^5.0.4", - "cypress": "^15.11.0", - "eslint": "^9.16.0", - "eslint-plugin-react-hooks": "^7.0.0", - "eslint-plugin-react-refresh": "^0.4.16", - "eslint-plugin-simple-import-sort": "^12.1.1", - "globals": "^16.4.0", - "jsdom": "^27.0.1", - "msw": "^2.12.2", - "stylelint-config-standard": "^39.0.1", - "typescript": "^5.7.2", - "typescript-eslint": "^8.18.0", - "vite": "^7.3.1", - "vite-tsconfig-paths": "^5.1.4", - "vitest": "^4.0.1" + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=22.22.0", - "npm": ">=10.9.4" + "node": "^20.17.0 || >=22.9.0" } } } diff --git a/package.json b/package.json index 6771b269..422c8764 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "src/common/icons", "src/identity-server", "src/self-service-portal/app", + "src/haapi-react-sdk", "src/haapi-react-app" ], "scripts": { diff --git a/src/common/component-library/package.json b/src/common/component-library/package.json index b39a4d45..7891a187 100644 --- a/src/common/component-library/package.json +++ b/src/common/component-library/package.json @@ -38,15 +38,15 @@ "test": "vitest" }, "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "dependencies": { "@curity/ui-kit-css": "^1.0.0", "@curity/ui-kit-icons": "^1.0.0", "@radix-ui/react-dialog": "^1.1.15", "react": "^19.2.0", - "react-router": "^7.9.4" + "react-router": "^7.18.0" }, "devDependencies": { "@eslint/js": "^9.9.0", @@ -61,8 +61,8 @@ "eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-simple-import-sort": "^12.1.1", "globals": "^16.4.0", - "jsdom": "^27.0.1", - "stylelint-config-standard": "^39.0.1", + "jsdom": "^29.1.1", + "stylelint-config-standard": "^40.0.0", "typescript": "^5.7.2", "typescript-eslint": "^8.18.0", "vite": "^7.1.12", diff --git a/src/common/css/docs/package.json b/src/common/css/docs/package.json index ed372eba..00b8954f 100644 --- a/src/common/css/docs/package.json +++ b/src/common/css/docs/package.json @@ -20,15 +20,15 @@ "@astrojs/react": "^5.0.3", "@curity/ui-kit-css": "*", "@curity/ui-kit-icons": "*", - "astro": "^6.1.6", + "astro": "^6.4.6", "mdast-util-to-string": "3.2.0", "react": "^19.2.0", - "react-dom": "^19.2.0", + "react-dom": "^19.2.7", "rollup": "^4.40.0" }, "devDependencies": { "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.2", + "@types/react-dom": "^19.2.3", "@typescript/twoslash": "^3.2.9", "prettier": "^3.5.3", "prettier-plugin-astro": "^0.12.3" diff --git a/src/common/css/lib/src/base/base-buttons.css b/src/common/css/lib/src/base/base-buttons.css index cc719388..d8e97c70 100644 --- a/src/common/css/lib/src/base/base-buttons.css +++ b/src/common/css/lib/src/base/base-buttons.css @@ -29,6 +29,7 @@ background-color: var(--button-background-color); border: none; border-radius: var(--button-border-radius); + color: white; cursor: pointer; display: inline-flex; font-family: var(--type-sans-bold); diff --git a/src/common/icons/src/components/authenticator/IconAuthenticatorWebauthnCrossPlatform.tsx b/src/common/icons/src/components/authenticator/IconAuthenticatorWebauthnCrossPlatform.tsx new file mode 100644 index 00000000..25f0385b --- /dev/null +++ b/src/common/icons/src/components/authenticator/IconAuthenticatorWebauthnCrossPlatform.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import type { SVGProps } from 'react'; +const SvgIconAuthenticatorWebauthnCrossPlatform = (props: SVGProps) => ( + + + + +); +export default SvgIconAuthenticatorWebauthnCrossPlatform; diff --git a/src/common/icons/src/components/authenticator/IconAuthenticatorWebauthnPlatform.tsx b/src/common/icons/src/components/authenticator/IconAuthenticatorWebauthnPlatform.tsx new file mode 100644 index 00000000..7f444f55 --- /dev/null +++ b/src/common/icons/src/components/authenticator/IconAuthenticatorWebauthnPlatform.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import type { SVGProps } from 'react'; +const SvgIconAuthenticatorWebauthnPlatform = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + +); +export default SvgIconAuthenticatorWebauthnPlatform; diff --git a/src/common/icons/src/components/authenticator/index.ts b/src/common/icons/src/components/authenticator/index.ts index 7807d9c3..940dfb74 100644 --- a/src/common/icons/src/components/authenticator/index.ts +++ b/src/common/icons/src/components/authenticator/index.ts @@ -40,5 +40,7 @@ export { default as IconAuthenticatorStackexchange } from './IconAuthenticatorSt export { default as IconAuthenticatorTotp } from './IconAuthenticatorTotp'; export { default as IconAuthenticatorUser } from './IconAuthenticatorUser'; export { default as IconAuthenticatorWebauthn } from './IconAuthenticatorWebauthn'; +export { default as IconAuthenticatorWebauthnCrossPlatform } from './IconAuthenticatorWebauthnCrossPlatform'; +export { default as IconAuthenticatorWebauthnPlatform } from './IconAuthenticatorWebauthnPlatform'; export { default as IconAuthenticatorWindows } from './IconAuthenticatorWindows'; export { default as IconAuthenticatorX } from './IconAuthenticatorX'; diff --git a/src/haapi-react-app/.prettierignore b/src/haapi-react-app/.prettierignore index a3d92a19..47663139 100644 --- a/src/haapi-react-app/.prettierignore +++ b/src/haapi-react-app/.prettierignore @@ -1,3 +1,3 @@ .prettierrc tsconfig.* -README.md \ No newline at end of file +README.md diff --git a/src/haapi-react-app/package.json b/src/haapi-react-app/package.json index a31aa484..36d95c1f 100644 --- a/src/haapi-react-app/package.json +++ b/src/haapi-react-app/package.json @@ -28,10 +28,11 @@ "watch:css-lib": "npm run watch --workspace @curity/ui-kit-css-lib" }, "dependencies": { + "@curity/haapi-react-sdk": "*", "@curity/identityserver-haapi-web-driver": "^1.3.0", "@curity/ui-kit-icons": "^1.0.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.2.7" }, "devDependencies": { "@eslint-react/eslint-plugin": "^1.43.0", @@ -42,14 +43,14 @@ "@testing-library/user-event": "^14.6.1", "@types/node": "^24.0.3", "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-basic-ssl": "^2.1.0", "@vitejs/plugin-react": "^5.0.4", "eslint": "^9.21.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", - "jsdom": "^26.1.0", + "jsdom": "^29.1.1", "postcss-extend-rule": "^4.0.0", "postcss-import": "^16.1.1", "prettier": "^3.5.3", diff --git a/src/haapi-react-app/previewer/Previewer.tsx b/src/haapi-react-app/previewer/Previewer.tsx index a7a8676f..c9a40521 100644 --- a/src/haapi-react-app/previewer/Previewer.tsx +++ b/src/haapi-react-app/previewer/Previewer.tsx @@ -49,6 +49,13 @@ const baseContextValue: Omit = { loading: false, nextStep, history: [], + config: { + bootstrap: mockAppConfig, + pollingInterval: 0, + bankIdAutostart: false, + webAuthnAutostart: false, + autoRedirectOnAuthenticationComplete: false, + }, }; export function Previewer() { diff --git a/src/haapi-react-app/previewer/examples.authentication-actions.ts b/src/haapi-react-app/previewer/examples.authentication-actions.ts index 2c7b90b6..156a8cb8 100644 --- a/src/haapi-react-app/previewer/examples.authentication-actions.ts +++ b/src/haapi-react-app/previewer/examples.authentication-actions.ts @@ -19,7 +19,7 @@ import { HAAPI_FORM_FIELDS, HTTP_METHODS, } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types'; -import { MEDIA_TYPES } from '../src/shared/util/types/media.types'; +import { MEDIA_TYPES } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/media.types'; const authenticationActionRequestAck: HaapiActionStep = { messages: [ diff --git a/src/haapi-react-app/previewer/examples.authenticators.ts b/src/haapi-react-app/previewer/examples.authenticators.ts index a7af3720..450feb6f 100644 --- a/src/haapi-react-app/previewer/examples.authenticators.ts +++ b/src/haapi-react-app/previewer/examples.authenticators.ts @@ -23,7 +23,7 @@ import { HAAPI_FORM_FIELDS, HTTP_METHODS, } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types'; -import { MEDIA_TYPES } from '../src/shared/util/types/media.types'; +import { MEDIA_TYPES } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/media.types'; import { PreviewItemData } from './examples'; const authenticatorHtmlFormLogin: HaapiActionStep = { diff --git a/src/haapi-react-app/previewer/examples.oauth.ts b/src/haapi-react-app/previewer/examples.oauth.ts index 36f744f1..fe675bb0 100644 --- a/src/haapi-react-app/previewer/examples.oauth.ts +++ b/src/haapi-react-app/previewer/examples.oauth.ts @@ -17,7 +17,7 @@ import { HAAPI_FORM_FIELDS, HTTP_METHODS, } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types'; -import { MEDIA_TYPES } from '../src/shared/util/types/media.types'; +import { MEDIA_TYPES } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/media.types'; import { HAAPI_STEPS, HaapiActionStep } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-step.types'; import { PreviewItemData } from './examples'; @@ -42,6 +42,10 @@ const userConsent: HaapiActionStep = { ], metadata: { viewName: 'views/oauth/consent', + viewData: { + clientLogo: + 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTI3cHgiIGhlaWdodD0iMTI5cHgiIHZpZXdCb3g9IjAgMCAxMjcgMTI5IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA0OC4yICg0NzMyNykgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+CiAgICA8dGl0bGU+Y3ViZTwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJjdWJlIiBmaWxsPSIjNjI2RDg3IiBmaWxsLXJ1bGU9Im5vbnplcm8iPgogICAgICAgICAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4LjAwMDAwMCwgOS4wMDAwMDApIiBpZD0iU2hhcGUiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTEwOC44MjUyNSwzMy45NDUyMjk0IEMxMDguMzc0LDMzLjk0NTIyOTQgMTA3Ljk0NjI1LDM0LjAyNzI5NjIgMTA3LjU1Nzc1LDM0LjE4NTE5MzggTDEwNi45ODc1LDM0LjQ3MjA1MzUgTDY0LjExODc1LDU2LjY1NzAzMzQgTDYzLjE0Njc1LDU3LjE1NDkyMiBDNjMuMTQ2NzUsNTcuMTU0OTIyIDYzLjEzMzc1LDU3LjE2MDY1OTIgNjMuMTMzNzUsNTcuMTY2NjQ1OSBDNjEuMjQzNzUsNTguMzMxMDQ2OCA2MCw2MC4yODYxODI2IDYwLDYyLjUxMDk2NjYgTDYwLDEwOC44NzA5ODQgQzYwLDExMC40NjI5MzEgNjEuNDEyMjUsMTExLjc1MDU1NyA2My4xNzI1LDExMS43NTA1NTcgQzYzLjcxNywxMTEuNzUwNTU3IDY0LjIzNTI1LDExMS42MjI1OTIgNjQuNjg4NzUsMTExLjQwMDMzOSBDNjQuNzI3NSwxMTEuMzc2NjQxIDY0Ljc3OTUsMTExLjM1MzQ0MyA2NC44MTg1LDExMS4zMjk5OTYgTDEwOC41MDEyNSw4OC4yMDgxMDY5IEwxMDguNTY2LDg4LjE3MjkzNTQgQzExMC42MjYyNSw4Ny4wNDk0NDMyIDExMS45OTk3NSw4NC45OTQ1MzAxIDExMS45OTk3NSw4Mi42NTg5OTMzIEwxMTEuOTk5NzUsMzYuODEzMzI3NCBDMTEyLDM1LjIyNzExOCAxMTAuNTc0NzUsMzMuOTQ1MjI5NCAxMDguODI1MjUsMzMuOTQ1MjI5NCBaIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTA1LjU2ODI1LDIxLjU4MjA3NTcgTDYwLjE0MzI1LDEuMTk0ODMyOTYgQzYwLjE0MzI1LDEuMTk0ODMyOTYgNTcuNDc4NSwxLjQyMTA4NTQ3ZS0xNCA1NS45OTksMS40MjEwODU0N2UtMTQgQzU0LjUxOTUsMS40MjEwODU0N2UtMTQgNTEuODY3NSwxLjE5NDgzMjk2IDUxLjg2NzUsMS4xOTQ4MzI5NiBMNi40MDM1LDIxLjU4MjA3NTcgQzYuNDAzNSwyMS41ODIwNzU3IDQuNDA2MjUsMjIuNDAzMjQyOCA0LjQwNjI1LDIzLjk0NjU0NzkgQzQuNDA2MjUsMjUuNTgzMzk0MiA2LjQ4MTI1LDI2LjgxNTE0NDggNi40ODEyNSwyNi44MTUxNDQ4IEw1Mi4xMjY3NSw1MC44NTQyMzYxIEw1Mi44NTIyNSw1MS4yMjMxNjI2IEM1My43OTgyNSw1MS42NDM5NzMzIDU0Ljg3MTUsNTEuODgzOTM3NiA1NS45OTksNTEuODgzOTM3NiBDNTcuMTM5MjUsNTEuODgzOTM3NiA1OC4yMjc3NSw1MS42NDM3MjM4IDU5LjE4NDUsNTEuMjExNDM4OCBMNTkuODE5MjUsNTAuODgzNDIwOSBMMTA1LjU0MjUsMjYuODAzMTcxNSBDMTA1LjU0MjUsMjYuODAzMTcxNSAxMDcuNDE5NSwyNS43OTY0MTg3IDEwNy40MTk1LDIzLjk0NjI5ODQgQzEwNy40MTk1LDIyLjMyNTE2NyAxMDUuNTY4MjUsMjEuNTgyMDc1NyAxMDUuNTY4MjUsMjEuNTgyMDc1NyBaIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8cGF0aCBkPSJNNDguODY2NSw1Ny4xNjY4OTUzIEw0Ny44Njg1LDU2LjY1NzI4MjkgTDUuMDEwNSwzNC40NzIzMDI5IEw0LjQ0MDUsMzQuMTg1NDQzMiBDNC4wNjY3NSwzNC4wMjcyOTYyIDMuNjI2LDMzLjk0NTQ3ODggMy4xNzI3NSwzMy45NDU0Nzg4IEMxLjQyMzUsMzMuOTQ1NDc4OCAwLjAwMDI1LDM1LjIyNzExOCAwLjAwMDI1LDM2LjgxMzMyNzQgTDAuMDAwMjUsODIuNjU4OTkzMyBDMC4wMDAyNSw4NS4wMDA3NjYxIDEuMzcxNSw4Ny4wNDk2OTI3IDMuNDMyLDg4LjE3MjkzNTQgTDMuNDg0LDg4LjIwODEwNjkgTDQ3LjE2ODc1LDExMS4zMjk5OTYgQzQ3LjY0ODI1LDExMS41OTkzOTQgNDguMjE4NSwxMTEuNzUwODA2IDQ4LjgyNzc1LDExMS43NTA4MDYgQzUwLjU3NSwxMTEuNzUwODA2IDUyLjAwMDI1LDExMC40NjMxOCA1Mi4wMDAyNSwxMDguODcxMjM0IEw1Mi4wMDAyNSw2Mi41MTEyMTYgQzUyLDYwLjI4NjQzMjEgNTAuNzU2LDU4LjMzMTI5NjIgNDguODY2NSw1Ny4xNjY4OTUzIFoiPjwvcGF0aD4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+', + }, }, type: HAAPI_STEPS.USER_CONSENT, actions: [ diff --git a/src/haapi-react-app/previewer/shared/ui/json-representation/pretty-print.css b/src/haapi-react-app/previewer/shared/ui/json-representation/pretty-print.css index e2dd6359..9bd867c4 100644 --- a/src/haapi-react-app/previewer/shared/ui/json-representation/pretty-print.css +++ b/src/haapi-react-app/previewer/shared/ui/json-representation/pretty-print.css @@ -15,6 +15,8 @@ } .json-pretty-print-string { color: #690; + word-break: break-all; + overflow-wrap: break-word; } .json-pretty-print-number { color: #905; diff --git a/src/haapi-react-app/previewer/shared/ui/preview/Preview.tsx b/src/haapi-react-app/previewer/shared/ui/preview/Preview.tsx index e40c8e92..9169e898 100644 --- a/src/haapi-react-app/previewer/shared/ui/preview/Preview.tsx +++ b/src/haapi-react-app/previewer/shared/ui/preview/Preview.tsx @@ -16,10 +16,11 @@ import { JsonRepresentation } from '../json-representation/JsonRepresentation'; import { Main } from '../main/Main'; import { Header } from '../page-header/PageHeader'; import { PreviewLayout } from '../preview-layout/PreviewLayout'; -import { PageSymbol } from '../../../../src/shared/ui/PageSymbol'; -import { Well } from '@curity/haapi-react-sdk/haapi-stepper/ui/well/Well'; +import { PageSymbol } from '../../../../src/shared/ui/PageSymbol/PageSymbol'; +import { Well } from '../../../../src/shared/ui/Well'; import { useHaapiAppConfig } from '../../../../src/shared/feature/app-config/HaapiAppConfigHook'; -import { Logo } from '../../../../src/shared/ui/Logo'; +import { Logo } from '../../../../src/shared/ui/Logo/Logo'; +import { showPageSymbol } from '../../../../src/shared/util/page-symbol-utils'; import styles from './preview.module.css'; interface PreviewProps { @@ -46,7 +47,7 @@ export function Preview({ title, step, onErrorToggle }: PreviewProps) { {!isInsideWell && } {isInsideWell && } - + {showPageSymbol(step) && } diff --git a/src/haapi-react-app/previewer/vite.config.ts b/src/haapi-react-app/previewer/vite.config.ts index d53b77f7..00ac803d 100644 --- a/src/haapi-react-app/previewer/vite.config.ts +++ b/src/haapi-react-app/previewer/vite.config.ts @@ -29,7 +29,6 @@ export default defineConfig({ }, resolve: { alias: { - '@curity/haapi-react-sdk': path.resolve(__dirname, '../src/haapi-react-sdk'), '@css': path.resolve(__dirname, '../src/shared/util/css'), }, }, diff --git a/src/haapi-react-app/setupTests.ts b/src/haapi-react-app/setupTests.ts index c91014a5..ae3eb35f 100644 --- a/src/haapi-react-app/setupTests.ts +++ b/src/haapi-react-app/setupTests.ts @@ -10,18 +10,3 @@ */ import '@testing-library/jest-dom/vitest'; -import { vi } from 'vitest'; - -// jsdom doesn't implement — minimal mocks for open/close state. -HTMLDialogElement.prototype.show = vi.fn(function mock(this: HTMLDialogElement) { - this.open = true; -}); - -HTMLDialogElement.prototype.showModal = vi.fn(function mock(this: HTMLDialogElement) { - this.open = true; -}); - -HTMLDialogElement.prototype.close = vi.fn(function mock(this: HTMLDialogElement) { - this.open = false; - this.dispatchEvent(new Event('close')); -}); diff --git a/src/haapi-react-app/src/App.tsx b/src/haapi-react-app/src/App.tsx index ebe7894a..b2e9f8da 100644 --- a/src/haapi-react-app/src/App.tsx +++ b/src/haapi-react-app/src/App.tsx @@ -9,7 +9,7 @@ * For further information, please contact Curity AB. */ -import { Layout } from './shared/ui/Layout'; +import { Layout } from './shared/ui/Layout/Layout'; import { HaapiAppConfigProvider } from './shared/feature/app-config/HaapiAppConfigProvider'; import { ErrorBoundary } from './shared/feature/error-handling/ErrorBoundary'; import { HaapiStepperStepUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI'; diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx b/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx deleted file mode 100644 index 65ef011b..00000000 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2026 Curity AB. All rights reserved. - * - * The contents of this file are the property of Curity AB. - * You may not copy or use this file, in either source code - * or executable form, except in compliance with terms - * set by Curity AB. - * - * For further information, please contact Curity AB. - */ - -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import { HAAPI_STEPS } from '../../../data-access/types/haapi-step.types'; -import { - createMockBankIdAction, - createMockExternalBrowserFlowAction, - createMockStep, - createMockWebAuthnAnyDeviceBothOptionsAction, - createMockWebAuthnPlatformOnlyAnyDeviceAction, - createMockWebAuthnRegistrationAction, - externalBrowserFlowActionTitle, - webAuthnAnyDeviceActionTitle, - webAuthnPlatformOnlyAnyDeviceActionTitle, - webAuthnRegistrationActionTitle, -} from '../../../util/tests/mocks'; -import { HaapiStepperActionsUI } from '../../../ui/actions/HaapiStepperActionsUI'; -import { HaapiStepperClientOperationUI } from './HaapiStepperClientOperationUI'; -import { useIsWebAuthnPlatformAuthenticatorAvailable } from './operations/webauthn'; - -vi.mock('./operations/webauthn', async () => { - const actual = await vi.importActual('./operations/webauthn'); - return { - ...(actual as object), - useIsWebAuthnPlatformAuthenticatorAvailable: vi.fn(() => undefined), - }; -}); - -describe('HaapiStepperClientOperationUI', () => { - let user: ReturnType; - - beforeEach(() => { - user = userEvent.setup(); - }); - - describe('Default rendering', () => { - it('renders the action title as an enabled button', () => { - const action = createMockExternalBrowserFlowAction(); - - render(); - - expect(screen.getByRole('button', { name: externalBrowserFlowActionTitle })).toBeEnabled(); - }); - - it('does not render a progress bar when the action has no remaining wait time', () => { - const action = createMockExternalBrowserFlowAction(); - - render(); - - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }); - - it('forwards the action to onAction when clicked', async () => { - const action = createMockExternalBrowserFlowAction(); - const onAction = vi.fn(); - - render(); - - await user.click(screen.getByRole('button', { name: externalBrowserFlowActionTitle })); - - expect(onAction).toHaveBeenCalledTimes(1); - expect(onAction).toHaveBeenCalledWith(action); - }); - }); - - describe('BankID polling progress', () => { - it('renders a progress bar reflecting the session remaining time', () => { - const action = createMockBankIdAction({ maxWaitTime: 60, maxWaitRemainingTime: 30 }); - - render(); - - const progress = screen.getByRole('progressbar'); - expect(progress).toHaveAttribute('value', '30'); - expect(progress).toHaveAttribute('max', '60'); - }); - - it('hides the progress bar when showBankIdSessionTimeLeft is false', () => { - const action = createMockBankIdAction({ maxWaitTime: 60, maxWaitRemainingTime: 30 }); - - render(); - - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }); - }); - - describe('WebAuthn', () => { - afterEach(() => { - vi.unstubAllGlobals(); - vi.mocked(useIsWebAuthnPlatformAuthenticatorAvailable).mockReset(); - }); - - it('enables the button when the WebAuthn API is available', () => { - vi.stubGlobal('PublicKeyCredential', stubPublicKeyCredential()); - const action = createMockWebAuthnRegistrationAction(); - - render(); - - expect(screen.getByRole('button', { name: webAuthnRegistrationActionTitle })).toBeEnabled(); - }); - - it('disables the button when the WebAuthn API is unavailable', () => { - // jsdom does not expose `PublicKeyCredential`, so `isWebAuthnApiSupported()` returns false - // and the gate disables WebAuthn buttons. - const action = createMockWebAuthnRegistrationAction(); - - render(); - - expect(screen.getByRole('button', { name: webAuthnRegistrationActionTitle })).toBeDisabled(); - }); - - it('disables a platform-only any-device registration when no platform authenticator is available', () => { - vi.stubGlobal('PublicKeyCredential', stubPublicKeyCredential()); - vi.mocked(useIsWebAuthnPlatformAuthenticatorAvailable).mockReturnValue(false); - const action = createMockWebAuthnPlatformOnlyAnyDeviceAction(); - - render(); - - expect(screen.getByRole('button', { name: webAuthnPlatformOnlyAnyDeviceActionTitle })).toBeDisabled(); - }); - - it('renders one button per credential option for any-device-mode with both options, suffixing the original title', () => { - const action = createMockWebAuthnAnyDeviceBothOptionsAction(); - const step = createMockStep(HAAPI_STEPS.AUTHENTICATION, { actions: [action] }); - - render(); - - expect(screen.getByRole('button', { name: `${webAuthnAnyDeviceActionTitle} (Built-in)` })).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: `${webAuthnAnyDeviceActionTitle} (Security key)` }) - ).toBeInTheDocument(); - }); - }); -}); - -/** - * Minimal stand-in for the static `PublicKeyCredential` interface — enough for - * `isWebAuthnApiSupported()` to return true and for the platform-authenticator hook to resolve. - * jsdom doesn't expose `PublicKeyCredential`, so tests stub it to emulate a WebAuthn-capable - * browser without reaching into the real `navigator.credentials` API. - */ -const stubPublicKeyCredential = () => - Object.assign(vi.fn(), { - parseCreationOptionsFromJSON: vi.fn(), - parseRequestOptionsFromJSON: vi.fn(), - isUserVerifyingPlatformAuthenticatorAvailable: vi.fn(() => Promise.resolve(true)), - }); diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/typings.ts b/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/typings.ts deleted file mode 100644 index fd5c2431..00000000 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/typings.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum EXTERNAL_BROWSER_FLOW_ERROR_TYPE { - /** Launch failed (popup blocked, browser policy, etc.). */ - LAUNCH = 'launch', - /** Resume failed — postMessage couldn't be consumed (unexpected origin/type) or operation aborted. */ - RESUME = 'resume', -} diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/completed-step.ts b/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/completed-step.ts deleted file mode 100644 index ace7d7dc..00000000 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/completed-step.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2025 Curity AB. All rights reserved. - * - * The contents of this file are the property of Curity AB. - * You may not copy or use this file, in either source code - * or executable form, except in compliance with terms - * set by Curity AB. - * - * For further information, please contact Curity AB. - */ - -import { HAAPI_STEPS, type HaapiCompletedStep } from '../../../data-access/types/haapi-step.types'; -import type { HaapiStepperConfig } from '../haapi-stepper.types'; -import { formatNextStepData } from '../data-formatters/format-next-step-data'; - -export function handleCompletedStep(step: HaapiCompletedStep, config: HaapiStepperConfig) { - if (!config.autoRedirectOnAuthenticationComplete) { - return { nextStepData: formatNextStepData(step) }; - } - - const redirectHref = step.links?.find(link => link.rel === 'authorization-response')?.href; - - if (!redirectHref) { - const isSuccess = step.type === HAAPI_STEPS.COMPLETED_WITH_SUCCESS; - throw new Error( - `autoRedirectOnAuthenticationComplete is enabled, but the completed-with-${isSuccess ? 'success' : 'error'} step did not include an authorization-response link.` - ); - } - - window.location.href = redirectHref; - return { nextStepData: undefined }; -} diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/viewnames/BankIdViewNameBuiltInUI.tsx b/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/viewnames/BankIdViewNameBuiltInUI.tsx deleted file mode 100644 index 26cb897d..00000000 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/viewnames/BankIdViewNameBuiltInUI.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2026 Curity AB. All rights reserved. - * - * The contents of this file are the property of Curity AB. - * You may not copy or use this file, in either source code - * or executable form, except in compliance with terms - * set by Curity AB. - * - * For further information, please contact Curity AB. - */ - -import { isQrCodeLink } from '../../util/isQrCodeLink'; -import { getLinksElement } from '../steps/step-element-factories'; -import type { ViewNameBuiltInUIProps } from './typings'; - -/** - * Built-in UI for the BankID viewName (`HaapiStepperViewNameBuiltInUI.BANKID`). - * - * - Lifts the QR code link above the actions so it's the primary element on the screen. - */ -export const BankIdViewNameBuiltInUI = (props: ViewNameBuiltInUIProps) => { - const { currentStep, linkRenderInterceptor, loadingElement, errorElement, messagesElement, actionsElement } = props; - const { links } = currentStep.dataHelpers; - const qrLink = links.find(isQrCodeLink); - const nonQrLinks = links.filter(link => !isQrCodeLink(link)); - - return ( - <> - {loadingElement} - {errorElement} - {messagesElement} - {qrLink && getLinksElement(props, [qrLink], linkRenderInterceptor)} - {actionsElement} - {nonQrLinks.length > 0 && getLinksElement(props, nonQrLinks, linkRenderInterceptor)} - - ); -}; diff --git a/src/haapi-react-app/src/shared/feature/app-config/HaapiAppConfigProvider.tsx b/src/haapi-react-app/src/shared/feature/app-config/HaapiAppConfigProvider.tsx index 34e3a235..81457e0d 100644 --- a/src/haapi-react-app/src/shared/feature/app-config/HaapiAppConfigProvider.tsx +++ b/src/haapi-react-app/src/shared/feature/app-config/HaapiAppConfigProvider.tsx @@ -11,10 +11,9 @@ import { ReactNode } from 'react'; import { HaapiAppConfigContext } from './HaapiAppConfigContext'; -import { HaapiAppConfig } from './types'; export const HaapiAppConfigProvider = ({ children }: { children: ReactNode }) => { - const configuration = window.__CONFIG__ as HaapiAppConfig | undefined; + const configuration = window.__CONFIG__; if (!configuration) { throw new Error('HaapiAppConfigProvider: window.__CONFIG__ is not set'); } diff --git a/src/haapi-react-app/src/shared/feature/app-config/types.ts b/src/haapi-react-app/src/shared/feature/app-config/types.ts index 7c84af21..1130e838 100644 --- a/src/haapi-react-app/src/shared/feature/app-config/types.ts +++ b/src/haapi-react-app/src/shared/feature/app-config/types.ts @@ -1,20 +1,4 @@ import { HaapiStepperBootstrapConfig } from '@curity/haapi-react-sdk/haapi-stepper/feature'; -export interface HaapiAppConfig extends HaapiStepperBootstrapConfig { - theme: { - logo?: { - path: string; - isInsideWell: boolean; - }; - pageSymbols?: PageSymbols; - }; -} - -export interface PageSymbols { - /** Map of full HAAPI viewName -> symbol path. Highest precedence. */ - views?: Record; - /** Map of plugin implementation type (e.g. `html-form`, `bankid`) -> symbol path. */ - plugins?: Record; - /** Fallback symbol path used when no per-view / per-plugin entry matches. */ - default?: string; -} +// Nothing more to add for now +export type HaapiAppConfig = HaapiStepperBootstrapConfig; diff --git a/src/haapi-react-app/src/shared/feature/error-handling/default-error-fallback.tsx b/src/haapi-react-app/src/shared/feature/error-handling/default-error-fallback.tsx index a235c842..3d8b9370 100644 --- a/src/haapi-react-app/src/shared/feature/error-handling/default-error-fallback.tsx +++ b/src/haapi-react-app/src/shared/feature/error-handling/default-error-fallback.tsx @@ -9,7 +9,7 @@ * For further information, please contact Curity AB. */ -import { Well } from '@curity/haapi-react-sdk/haapi-stepper/ui/well/Well'; +import { Well } from '../../ui/Well'; export interface ErrorFallbackProps { error?: Error; diff --git a/src/haapi-react-app/src/shared/ui/Layout.spec.tsx b/src/haapi-react-app/src/shared/ui/Layout/Layout.spec.tsx similarity index 73% rename from src/haapi-react-app/src/shared/ui/Layout.spec.tsx rename to src/haapi-react-app/src/shared/ui/Layout/Layout.spec.tsx index a409d381..96290299 100644 --- a/src/haapi-react-app/src/shared/ui/Layout.spec.tsx +++ b/src/haapi-react-app/src/shared/ui/Layout/Layout.spec.tsx @@ -11,15 +11,16 @@ import { describe, expect, it, vi } from 'vitest'; import { render } from '@testing-library/react'; -import { Layout } from './Layout'; -import { HaapiStepperContext } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperContext'; -import type { PageSymbols } from '../feature/app-config/types'; +import { Layout } from '../Layout/Layout'; +import { HaapiStepperContext } from '@curity/haapi-react-sdk/haapi-stepper/feature'; import type { HaapiStepperAPI, HaapiStepperStep, -} from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types'; -import type { HaapiAppConfig } from '../feature/app-config/types'; -import { HaapiAppConfigContext } from '../feature/app-config/HaapiAppConfigContext'; + HaapiStepperStepSymbolsConfig, +} from '@curity/haapi-react-sdk/haapi-stepper/feature'; +import { HAAPI_STEPS } from '@curity/haapi-react-sdk/haapi-stepper/data-access'; +import type { HaapiAppConfig } from '../../feature/app-config/types'; +import { HaapiAppConfigContext } from '../../feature/app-config/HaapiAppConfigContext'; describe('Layout', () => { describe('Logo placement', () => { @@ -82,23 +83,25 @@ describe('Layout', () => { expect(container.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull(); }); + + it('page symbol is not rendered for a user consent step, even when one is configured for its viewName', () => { + const { container, getByTestId } = renderLayout({ + pageSymbols: { views: { 'views/oauth/consent': '/symbols/consent.svg' } }, + currentStep: stepWithViewName('views/oauth/consent', HAAPI_STEPS.USER_CONSENT), + }); + + expect(container.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull(); + expect(getByTestId('content')).toBeInTheDocument(); + }); }); }); -const emptyStepperAPI: HaapiStepperAPI = { - loading: false, - history: [], - nextStep: vi.fn(), - currentStep: null, - error: null, -}; - -const stepWithViewName = (viewName: string): HaapiStepperStep => - ({ metadata: { templateArea: 'lwa', viewName } }) as unknown as HaapiStepperStep; +const stepWithViewName = (viewName: string, type?: HAAPI_STEPS): HaapiStepperStep => + ({ type, metadata: { viewName } }) as unknown as HaapiStepperStep; interface LayoutHarnessOptions { isInsideWell?: boolean; - pageSymbols?: PageSymbols; + pageSymbols?: HaapiStepperStepSymbolsConfig; currentStep?: HaapiStepperStep | null; withoutLogo?: boolean; } @@ -114,10 +117,24 @@ const renderLayout = ({ haapi: {} as HaapiAppConfig['haapi'], theme: { logo: withoutLogo ? undefined : { path: '/assets/logo.svg', isInsideWell }, pageSymbols }, }; - const stepper: HaapiStepperAPI = { ...emptyStepperAPI, currentStep }; + const stepperAPI: HaapiStepperAPI = { + loading: false, + history: [], + nextStep: vi.fn(), + currentStep, + error: null, + config: { + bootstrap: config, + pollingInterval: 0, + bankIdAutostart: false, + webAuthnAutostart: false, + autoRedirectOnAuthenticationComplete: false, + }, + }; + return render( - +
diff --git a/src/haapi-react-app/src/shared/ui/Layout.tsx b/src/haapi-react-app/src/shared/ui/Layout/Layout.tsx similarity index 70% rename from src/haapi-react-app/src/shared/ui/Layout.tsx rename to src/haapi-react-app/src/shared/ui/Layout/Layout.tsx index 6a41a5a1..5c9ebd31 100644 --- a/src/haapi-react-app/src/shared/ui/Layout.tsx +++ b/src/haapi-react-app/src/shared/ui/Layout/Layout.tsx @@ -10,11 +10,12 @@ */ import { ReactNode } from 'react'; -import { Well } from '@curity/haapi-react-sdk/haapi-stepper/ui/well/Well'; -import { useHaapiAppConfig } from '../feature/app-config/HaapiAppConfigHook'; -import { Logo } from './Logo'; -import { PageSymbol } from './PageSymbol'; +import { useHaapiAppConfig } from '../../feature/app-config/HaapiAppConfigHook'; +import { Logo } from '../Logo/Logo'; +import { PageSymbol } from '../PageSymbol/PageSymbol'; import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature'; +import { Well } from '../Well'; +import { showPageSymbol } from '../../util/page-symbol-utils'; export const Layout = ({ children }: { children: ReactNode }) => { const { currentStep } = useHaapiStepper(); @@ -27,7 +28,7 @@ export const Layout = ({ children }: { children: ReactNode }) => { {isInsideWell && }
- + {showPageSymbol(currentStep) && } {children}
diff --git a/src/haapi-react-app/src/shared/ui/Logo.spec.tsx b/src/haapi-react-app/src/shared/ui/Logo/Logo.spec.tsx similarity index 81% rename from src/haapi-react-app/src/shared/ui/Logo.spec.tsx rename to src/haapi-react-app/src/shared/ui/Logo/Logo.spec.tsx index 79a6cce6..422a088b 100644 --- a/src/haapi-react-app/src/shared/ui/Logo.spec.tsx +++ b/src/haapi-react-app/src/shared/ui/Logo/Logo.spec.tsx @@ -11,9 +11,9 @@ import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; -import { Logo } from './Logo'; -import { HaapiAppConfigContext } from '../feature/app-config/HaapiAppConfigContext'; -import { HaapiAppConfig } from '../feature/app-config/types'; +import { Logo } from '../Logo/Logo'; +import { HaapiAppConfigContext } from '../../feature/app-config/HaapiAppConfigContext'; +import { HaapiAppConfig } from '../../feature/app-config/types'; const buildConfig = (logoPath: string): HaapiAppConfig => ({ initialUrl: 'https://example/start', @@ -29,9 +29,10 @@ describe('Logo', () => { ); - const img = screen.getByRole('presentation'); + const img = screen.getByRole('img'); expect(img.tagName).toBe('IMG'); expect(img).toHaveAttribute('src', '/assets/logo.svg'); + expect(img).toHaveAttribute('alt', 'Logo'); expect(img).toHaveClass('haapi-stepper-logo'); }); @@ -49,6 +50,6 @@ describe('Logo', () => { ); expect(container.querySelector('img')).toBeNull(); - expect(screen.queryByRole('presentation')).toBeNull(); + expect(screen.queryByRole('img')).toBeNull(); }); }); diff --git a/src/haapi-react-app/src/shared/ui/Logo.tsx b/src/haapi-react-app/src/shared/ui/Logo/Logo.tsx similarity index 83% rename from src/haapi-react-app/src/shared/ui/Logo.tsx rename to src/haapi-react-app/src/shared/ui/Logo/Logo.tsx index 378d3060..98dcf8a3 100644 --- a/src/haapi-react-app/src/shared/ui/Logo.tsx +++ b/src/haapi-react-app/src/shared/ui/Logo/Logo.tsx @@ -9,7 +9,7 @@ * For further information, please contact Curity AB. */ -import { useHaapiAppConfig } from '../feature/app-config/HaapiAppConfigHook'; +import { useHaapiAppConfig } from '../../feature/app-config/HaapiAppConfigHook'; export const Logo = () => { const { theme } = useHaapiAppConfig(); @@ -17,5 +17,5 @@ export const Logo = () => { if (!logo) { return null; } - return ; + return Logo; }; diff --git a/src/haapi-react-app/src/shared/ui/PageSymbol.spec.tsx b/src/haapi-react-app/src/shared/ui/PageSymbol/PageSymbol.spec.tsx similarity index 89% rename from src/haapi-react-app/src/shared/ui/PageSymbol.spec.tsx rename to src/haapi-react-app/src/shared/ui/PageSymbol/PageSymbol.spec.tsx index 5dd1757e..61e05daa 100644 --- a/src/haapi-react-app/src/shared/ui/PageSymbol.spec.tsx +++ b/src/haapi-react-app/src/shared/ui/PageSymbol/PageSymbol.spec.tsx @@ -11,11 +11,12 @@ import { describe, expect, it } from 'vitest'; import { render } from '@testing-library/react'; -import { PageSymbol } from './PageSymbol'; -import { HaapiAppConfigContext } from '../feature/app-config/HaapiAppConfigContext'; -import type { HaapiAppConfig, PageSymbols } from '../feature/app-config/types'; +import { PageSymbol } from '../PageSymbol/PageSymbol'; +import { HaapiAppConfigContext } from '../../feature/app-config/HaapiAppConfigContext'; +import type { HaapiAppConfig } from '../../feature/app-config/types'; +import type { HaapiStepperStepSymbolsConfig } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types'; -const buildConfig = (pageSymbols?: PageSymbols): HaapiAppConfig => ({ +const buildConfig = (pageSymbols?: HaapiStepperStepSymbolsConfig): HaapiAppConfig => ({ initialUrl: 'https://example/start', haapi: {} as HaapiAppConfig['haapi'], theme: { @@ -24,7 +25,7 @@ const buildConfig = (pageSymbols?: PageSymbols): HaapiAppConfig => ({ }, }); -const renderPageSymbol = (viewName: string | undefined, pageSymbols?: PageSymbols) => +const renderPageSymbol = (viewName: string | undefined, pageSymbols?: HaapiStepperStepSymbolsConfig) => render( @@ -32,7 +33,7 @@ const renderPageSymbol = (viewName: string | undefined, pageSymbols?: PageSymbol ); describe('PageSymbol', () => { - const pageSymbols: PageSymbols = { + const pageSymbols: HaapiStepperStepSymbolsConfig = { views: { 'authenticator/html-form/create-account/post': '/symbols/create-account.svg', 'authentication-action/email-verifier/confirm': '/symbols/email-verifier-confirm.svg', diff --git a/src/haapi-react-app/src/shared/ui/PageSymbol.tsx b/src/haapi-react-app/src/shared/ui/PageSymbol/PageSymbol.tsx similarity index 88% rename from src/haapi-react-app/src/shared/ui/PageSymbol.tsx rename to src/haapi-react-app/src/shared/ui/PageSymbol/PageSymbol.tsx index 222015d3..37f82fbc 100644 --- a/src/haapi-react-app/src/shared/ui/PageSymbol.tsx +++ b/src/haapi-react-app/src/shared/ui/PageSymbol/PageSymbol.tsx @@ -9,8 +9,8 @@ * For further information, please contact Curity AB. */ -import { useHaapiAppConfig } from '../feature/app-config/HaapiAppConfigHook'; -import { resolvePageSymbol } from '../util/resolve-page-symbol'; +import { useHaapiAppConfig } from '../../feature/app-config/HaapiAppConfigHook'; +import { resolvePageSymbol } from '../../util/page-symbol-utils'; interface PageSymbolProps { viewName: string | undefined; diff --git a/src/haapi-react-app/src/shared/ui/SuccessCheckmark.tsx b/src/haapi-react-app/src/shared/ui/SuccessCheckmark.tsx deleted file mode 100644 index 299a45c9..00000000 --- a/src/haapi-react-app/src/shared/ui/SuccessCheckmark.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2026 Curity AB. All rights reserved. - * - * The contents of this file are the property of Curity AB. - * You may not copy or use this file, in either source code - * or executable form, except in compliance with terms - * set by Curity AB. - * - * For further information, please contact Curity AB. - */ - -export const SuccessCheckmark = () => ( -
-
- - - - -
-
-); diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/ui/well/Well.tsx b/src/haapi-react-app/src/shared/ui/Well.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/ui/well/Well.tsx rename to src/haapi-react-app/src/shared/ui/Well.tsx diff --git a/src/haapi-react-app/src/shared/util/css/styles.css b/src/haapi-react-app/src/shared/util/css/styles.css index a5780d92..de602993 100644 --- a/src/haapi-react-app/src/shared/util/css/styles.css +++ b/src/haapi-react-app/src/shared/util/css/styles.css @@ -248,6 +248,82 @@ svg { margin-block: 0 var(--space-2); } +.haapi-stepper-consent-logos { + @extend .flex, .flex-center, .justify-center, .p2; + + --consent-logo-circle-size: 80px; + --consent-logo-max-width: 70px; + --consent-logo-client-max-width: 50px; + --consent-checkmark-size: 2.5rem; + + --consent-checkmark-gap: calc(var(--consent-checkmark-size) / 2 + var(--space-1)); +} + +@media (min-width: 576px) { + .haapi-stepper-consent-logos { + --consent-logo-circle-size: 120px; + --consent-logo-max-width: 100px; + --consent-logo-client-max-width: 70px; + } +} + +.haapi-stepper-consent-logo { + flex: 0 0 40%; +} + +.haapi-stepper-consent-logo-circle { + @extend .flex, .flex-center, .justify-center, .relative, .mx-auto; + + width: var(--consent-logo-circle-size); + height: var(--consent-logo-circle-size); + border-radius: 100%; + background-color: var(--color-grey-subtle); + z-index: 2; +} + +.haapi-stepper-consent-logo-image { + max-width: var(--consent-logo-max-width); + max-height: 100%; + width: auto; + height: auto; + object-fit: contain; +} + +.haapi-stepper-consent-logo-image-client { + max-width: var(--consent-logo-client-max-width); +} + +.haapi-stepper-consent-logos-checkmark { + @extend .flex, .flex-center, .justify-center, .relative; + + flex: 0 0 20%; + z-index: 1; + color: var(--color-success); + + &::before, + &::after { + content: ''; + position: absolute; + top: 50%; + border-block-start: 1px dashed var(--color-grey-light); + } + + &::before { + left: -40%; + right: calc(50% + var(--consent-checkmark-gap)); + } + + &::after { + left: calc(50% + var(--consent-checkmark-gap)); + right: -40%; + } + + svg { + width: var(--consent-checkmark-size); + height: var(--consent-checkmark-size); + } +} + .haapi-stepper-page-symbol { text-align: center; width: var(--login-symbol-size); @@ -279,6 +355,57 @@ svg { @extend .center, .flex, .flex-column; } +.haapi-stepper-bankid-qr-code-accessibility { + details + details { + @extend .mt2; + } + + details[open] summary { + margin-bottom: var(--space-1); + } + + details[open]::details-content { + padding-block-start: 0; + } + + summary + * { + margin-block-start: 0; + } +} + +.haapi-stepper-webauthn-registration-attachment { + @extend .button, .w100, .mb2, .p3, .left-align; + + justify-content: unset; + background-color: transparent; + color: var(--color-text); + border: var(--form-field-border-width) var(--form-field-border-style) var(--form-field-border-color); + border-radius: var(--form-field-border-radius); + cursor: pointer; + transition: + color 200ms, + background-color 200ms, + border-color 200ms; + + &:hover, + &:focus-visible { + background-color: var(--button-color); + border-color: var(--button-color); + color: white; + } +} + +.haapi-stepper-webauthn-registration-attachment-icon { + @extend .flex-noshrink; + width: 60px; + height: 60px; +} + +.haapi-stepper-webauthn-registration-attachment-title { + font-family: var(--type-bold); + margin-inline-end: var(--space-2); +} + .haapi-stepper-heading { &:is(h1) { font-size: calc(var(--type-base-size) * 1.75); @@ -297,13 +424,18 @@ svg { width: min(100%, 100px); } -.haapi-stepper-polling-progress { +.haapi-stepper-polling-progress-bar { + @extend .mt2, .mb2; + display: block; + width: 100%; + max-width: 250px; + margin-inline: auto; + border: none; border-radius: var(--form-field-border-radius); background-color: var(--color-grey-subtle); - outline: 1px solid var(--color-grey-light); - outline-offset: 2px; - border: none; + /* Set the track/fill at class specificity — the base `progress` element rules lose the cascade + here, which left the fill as the browser default (blue) instead of the brand colour. */ &::-webkit-progress-bar { background-color: var(--color-grey-subtle); border-radius: var(--form-field-border-radius); @@ -320,6 +452,11 @@ svg { } } +.haapi-stepper-polling-progress-duration { + @extend .center; + margin-block-start: var(--space-1); +} + .haapi-stepper-link-qr-code-button { background: none; border: none; diff --git a/src/haapi-react-app/src/shared/util/resolve-page-symbol.ts b/src/haapi-react-app/src/shared/util/page-symbol-utils.ts similarity index 78% rename from src/haapi-react-app/src/shared/util/resolve-page-symbol.ts rename to src/haapi-react-app/src/shared/util/page-symbol-utils.ts index 353874cf..71283e54 100644 --- a/src/haapi-react-app/src/shared/util/resolve-page-symbol.ts +++ b/src/haapi-react-app/src/shared/util/page-symbol-utils.ts @@ -9,7 +9,8 @@ * For further information, please contact Curity AB. */ -import type { PageSymbols } from '../feature/app-config/types'; +import type { HaapiStepperStepSymbolsConfig } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types'; +import { HAAPI_STEPS, HaapiActionStep, HaapiCompletedStep } from '@curity/haapi-react-sdk/haapi-stepper/data-access'; /** * Resolves the page symbol image path for a given HAAPI step `viewName` against the @@ -25,7 +26,7 @@ import type { PageSymbols } from '../feature/app-config/types'; */ export const resolvePageSymbol = ( viewName: string | undefined, - pageSymbols: PageSymbols | undefined + pageSymbols: HaapiStepperStepSymbolsConfig | undefined ): string | undefined => { /** * Extracts the plugin implementation type from a HAAPI `viewName` of the form @@ -51,3 +52,7 @@ export const resolvePageSymbol = ( return pageSymbols.default; }; + +export function showPageSymbol(step: HaapiActionStep | HaapiCompletedStep | null): boolean { + return step?.type !== HAAPI_STEPS.USER_CONSENT; +} diff --git a/src/haapi-react-app/tsconfig.app.json b/src/haapi-react-app/tsconfig.app.json index 5f91c9ba..888941f8 100644 --- a/src/haapi-react-app/tsconfig.app.json +++ b/src/haapi-react-app/tsconfig.app.json @@ -16,12 +16,6 @@ "noEmit": true, "jsx": "react-jsx", - /* Aliases (mirrored in vite.config.ts resolve.alias) */ - "baseUrl": ".", - "paths": { - "@curity/haapi-react-sdk/*": ["src/haapi-react-sdk/*"], - }, - /* Linting */ "strict": true, "noUnusedLocals": true, diff --git a/src/haapi-react-app/vite.config.ts b/src/haapi-react-app/vite.config.ts index 2e4bc0eb..2e5227fc 100644 --- a/src/haapi-react-app/vite.config.ts +++ b/src/haapi-react-app/vite.config.ts @@ -48,7 +48,6 @@ export default defineConfig({ }, resolve: { alias: { - '@curity/haapi-react-sdk': path.resolve(__dirname, './src/haapi-react-sdk'), '@': path.resolve(__dirname, './src'), '@shared': path.resolve(__dirname, './src/shared'), '@util': path.resolve(__dirname, './src/util'), diff --git a/src/haapi-react-sdk/.prettierignore b/src/haapi-react-sdk/.prettierignore new file mode 100644 index 00000000..47663139 --- /dev/null +++ b/src/haapi-react-sdk/.prettierignore @@ -0,0 +1,3 @@ +.prettierrc +tsconfig.* +README.md diff --git a/src/haapi-react-sdk/.prettierrc b/src/haapi-react-sdk/.prettierrc new file mode 100644 index 00000000..71b5a61f --- /dev/null +++ b/src/haapi-react-sdk/.prettierrc @@ -0,0 +1,11 @@ +{ + "arrowParens": "avoid", + "endOfLine": "lf", + "semi": true, + "printWidth": 120, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false, + "htmlWhitespaceSensitivity": "ignore" +} diff --git a/src/haapi-react-sdk/eslint.config.js b/src/haapi-react-sdk/eslint.config.js new file mode 100644 index 00000000..9bc79574 --- /dev/null +++ b/src/haapi-react-sdk/eslint.config.js @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import globals from 'globals'; +import js from '@eslint/js'; +import react from '@eslint-react/eslint-plugin'; +import tseslint from 'typescript-eslint'; +import * as reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [ + js.configs.recommended, + // If 'strict' becomes annoying we can consider using 'recommended' instead + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + // If all-in-one becomes annoying we can consider using individual rule sets (available as separate packages) + react.configs['recommended-typescript'], + // If 'eslint-plugin-react-hooks' is upgraded to 6.0+, this should be updated to 'reactHooks.configs.recommended' + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + // Allows usage of type-checked rules: https://typescript-eslint.io/getting-started/typed-linting + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreVoidReturningFunctions: true }], + }, + }, + { + files: ['**/*.spec.{ts,tsx}'], + rules: { + '@typescript-eslint/no-non-null-assertion': 'off', + }, + } +); diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/README.md b/src/haapi-react-sdk/haapi-stepper/README.md similarity index 91% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/README.md rename to src/haapi-react-sdk/haapi-stepper/README.md index 538b0bc5..54faf3ed 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/README.md +++ b/src/haapi-react-sdk/haapi-stepper/README.md @@ -105,12 +105,11 @@ When the `HaapiStepper` is consumed as a library — e.g. embedded in a third-pa ```tsx import { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature'; -import type { HaapiAppConfig } from '../shared/feature/app-config/types'; +import type { HaapiStepperBootstrapConfig } from '@curity/haapi-react-sdk/haapi-stepper/feature'; -const bootstrapConfig: HaapiAppConfig = { +const bootstrapConfig: HaapiStepperBootstrapConfig = { initialUrl: 'https://idsvr.example.com/oauth/v2/oauth-authorize/...', haapi: { /* HAAPI web-driver config */ }, - theme: { logo: { path: '/logo.svg', isInsideWell: true } }, }; @@ -148,7 +147,9 @@ Check out [the HaapiStepperStepUI documentation and usage examples](./feature/st ### ViewName built-in UIs -The HaapiStepperStepUI ships built-in UIs for specific HAAPI `viewName`s (`step.metadata.viewName`) that need a more tailored UI than the generic step shell can provide (e.g. the BankID requires the QR link to be lifted above the actions). They are displayed by default and can be customized like any other step by using render interceptors. +The HaapiStepperStepUI ships built-in UIs for specific HAAPI `viewName`s (`step.metadata.viewName`) that need a more tailored UI than the generic step shell can provide (e.g. the BankID requires the QR link to be lifted above the actions). + +ViewName built-in UIs operate at the **step** level: they reshape the whole step for a given `viewName`. They are displayed by default and can be customized like any other step by using render interceptors. Check out documentation and usage examples in [`HaapiStepperStepUI`](./feature/steps/HaapiStepperStepUI.tsx), and the test use cases in [`HaapiStepperStepUI.spec.tsx`](./feature/steps/HaapiStepperStepUI.spec.tsx) (`describe('ViewName built-in UIs Rendering')`) for more details. @@ -183,7 +184,7 @@ Check out documentation and usage examples in the links below: ### CSS Customization -The HAAPI UI components are styled via plain CSS class names — no CSS-in-JS, no inline styles. The components only emit class names; the actual rules live in a stylesheet shipped alongside the components at [`src/shared/util/css/styles.css`](../shared/util/css/styles.css). +The HAAPI UI components are styled via plain CSS class names — no CSS-in-JS, no inline styles. The components only emit class names; the actual rules live in a stylesheet shipped alongside the host application's global stylesheet. For example, in the case of the `haapi-react-app`, in `haapi-react-app/src/shared/util/css/styles.css`. #### Importing CSS styles @@ -232,11 +233,18 @@ The Curity utility composition shown above is just how *this* project chose to i | `.haapi-stepper-link-qr-code-dialog` | `HaapiStepperQrCodeLinkDialog` | Fullscreen QR code dialog | | `.haapi-stepper-link-qr-code-dialog-close-button` | `HaapiStepperQrCodeLinkDialog` | Button wrapping the expanded QR code image; closes the dialog when clicked | | `.haapi-stepper-link-qr-code-dialog-image` | `HaapiStepperQrCodeLinkDialog` | Fullscreen QR code dialog image | +| `.haapi-stepper-bankid-qr-code-accessibility` | `HaapiStepperBankIdQrCodeAccessibilityMessages` | Container for the BankID QR-code accessibility messages (the collapsible "help" and "screen reader" `
` sections) | | `.haapi-stepper-actions` | `HaapiStepperActionsUI` | Actions container | | `.haapi-stepper-heading` | `HaapiStepperMessagesUI` | Heading messages | | `.haapi-stepper-userName` | `HaapiStepperMessagesUI` | User name display | | `.haapi-stepper-userCode` | `HaapiStepperMessagesUI` | User code display (e.g. recovery codes) | -| `.haapi-stepper-polling-progress` | `HaapiStepperClientOperationUI` | Remaining polling time indicator (e.g. recovery codes) | +| `.haapi-stepper-polling-progress-bar` | `HaapiStepperBankIdPollingProgressUI` | Remaining polling time indicator (the "authentication time" bar, e.g. BankID) | +| `.haapi-stepper-polling-progress-duration` | `HaapiStepperBankIdPollingProgressUI` | Numeric time-left readout shown below the bar (e.g. "24 seconds left") | +| `.haapi-stepper-webauthn-registration-attachment` | `HaapiStepperWebAuthnRegistrationAttachmentCard` | WebAuthn registration attachment-selection option card (icon + title + description) | +| `.haapi-stepper-webauthn-registration-attachment-icon` | `HaapiStepperWebAuthnRegistrationAttachmentCard` | Attachment card icon | +| `.haapi-stepper-webauthn-registration-attachment-title` | `HaapiStepperWebAuthnRegistrationAttachmentCard` | Attachment card option label | +| `.haapi-stepper-webauthn-registration-attachment-description` | `HaapiStepperWebAuthnRegistrationAttachmentCard` | Attachment card option description | +| `.haapi-stepper-consent-logos` | `UserConsentViewNameBuiltInUI` | Container for user consent logos | | `.haapi-stepper-error-boundary-fallback` | `DefaultErrorFallback` | Error boundary fallback container | | `.haapi-validation-errors-container` | `HaapiStepperFormValidationErrorInputWrapper` | Wrapper around a form field that has validation errors. Receives the `.has-errors` modifier class while errors are visible | | `.haapi-validation-errors` | `HaapiStepperFormValidationErrorInputWrapper` | Inner container that holds the list of validation error messages | diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-request.ts b/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-request.ts similarity index 94% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-request.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-request.ts index 6fb3a375..e4b61462 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-request.ts +++ b/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-request.ts @@ -10,7 +10,7 @@ */ import { FetchLike } from '@curity/identityserver-haapi-web-driver'; -import { MEDIA_TYPES } from '../../../shared/util/types/media.types'; +import { MEDIA_TYPES } from './types/media.types'; import { createApiRequest } from './haapi-fetch-utils'; import { HaapiFetchAction } from './types/haapi-fetch.types'; import { HaapiStep } from './types/haapi-step.types'; diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.spec.ts b/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.spec.ts similarity index 98% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.spec.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.spec.ts index ed6c51f3..5d4fcb4c 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.spec.ts +++ b/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.spec.ts @@ -11,7 +11,7 @@ import { describe, test, expect } from 'vitest'; import { HAAPI_FORM_FIELDS, HTTP_METHODS } from './types/haapi-form.types'; -import { MEDIA_TYPES } from '../../../shared/util/types/media.types'; +import { MEDIA_TYPES } from './types/media.types'; import { HaapiFormAction, HAAPI_ACTION_TYPES, HAAPI_FORM_ACTION_KINDS } from './types/haapi-action.types'; import { createRequestForUrl, createRequestForForm } from './haapi-fetch-utils'; diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.ts b/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.ts similarity index 98% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.ts index 88fddaaa..d5a29c3a 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.ts +++ b/src/haapi-react-sdk/haapi-stepper/data-access/haapi-fetch-utils.ts @@ -9,7 +9,7 @@ * For further information, please contact Curity AB. */ -import { MEDIA_TYPES } from '../../../shared/util/types/media.types'; +import { MEDIA_TYPES } from './types/media.types'; import { HaapiFetchAction, HaapiFetchFormAction } from './types/haapi-fetch.types'; import { HaapiFormField, HTTP_METHODS } from './types/haapi-form.types'; import { FetchLike } from '@curity/identityserver-haapi-web-driver'; diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/index.ts b/src/haapi-react-sdk/haapi-stepper/data-access/index.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/index.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/index.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-action.types.ts b/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-action.types.ts similarity index 99% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-action.types.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-action.types.ts index 208a9d1b..868b5a6d 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-action.types.ts +++ b/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-action.types.ts @@ -92,6 +92,7 @@ export enum HAAPI_FORM_ACTION_KINDS { AUTHENTICATOR_SELECTOR_OPTION = 'select-authenticator', CONTINUE = 'continue', USER_REGISTER = 'user-register', + AUTHORIZATION_RESPONSE = 'authorization-response', } export enum HAAPI_ACTION_CLIENT_OPERATIONS { diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-fetch.types.ts b/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-fetch.types.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-fetch.types.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-fetch.types.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types.ts b/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types.ts similarity index 98% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types.ts index 9e725e23..e6f61d1a 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types.ts +++ b/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types.ts @@ -9,7 +9,7 @@ * For further information, please contact Curity AB. */ -import { MEDIA_TYPES } from '../../../../shared/util/types/media.types'; +import { MEDIA_TYPES } from './media.types'; import { HaapiAction } from './haapi-action.types'; // ============================================================================ diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-step.types.ts b/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-step.types.ts similarity index 94% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-step.types.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-step.types.ts index 7569c918..121b5c3f 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-step.types.ts +++ b/src/haapi-react-sdk/haapi-stepper/data-access/types/haapi-step.types.ts @@ -217,11 +217,10 @@ export interface HaapiCompletedWithSuccessStep extends HaapiBaseStep { properties: HaapiCompletedWithSuccessStepOAuthAuthorizationResponseProperties; /** User messages. Should be displayed to users to help them understand the context of an interaction. */ messages?: HaapiUserMessage[]; - /** - * Array of hyperlinks that may be used to offer an user alternative flows. The "main" flow the user is expected to take should use "actions" instead. - * Links can be used, for example, to divert the user to register an account or a device from a login page. - */ + /** If the flow response mode is redirect/GET-based, contains a ready-to-use link with the flow response. */ links?: HaapiLink[]; + /** If the flow response mode is form POST, contains a ready-to-use form action with the flow response. */ + actions?: HaapiFormAction[]; } /** @@ -275,6 +274,8 @@ export interface HaapiCompletedWithErrorStep extends HaapiProblemStepBase { error_description?: string; /** Issuer identifier */ iss?: string; + /** If the flow response mode is form POST, contains a ready-to-use form action with the flow response. */ + actions?: HaapiFormAction[]; } /** @@ -343,6 +344,15 @@ export interface HaapiLink { title?: string; } +/** + * View-specific data attached to the response. The shape depends on the view that produced it. + */ +export interface HaapiViewData { + /** Map of message key to its localized text, as sent by the server for this view. */ + messages?: Record; + [key: string]: unknown; +} + /** * Object with additional information about the response. A client may ignore the information present in this object. */ @@ -351,4 +361,6 @@ export interface HaapiMetadata { templateArea?: string; /** The name for the view that produced the response */ viewName?: string; + /** View-specific data (e.g. localized accessibility messages) for the view that produced the response */ + viewData?: HaapiViewData; } diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/index.ts b/src/haapi-react-sdk/haapi-stepper/data-access/types/index.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/types/index.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/types/index.ts diff --git a/src/haapi-react-app/src/shared/util/types/media.types.ts b/src/haapi-react-sdk/haapi-stepper/data-access/types/media.types.ts similarity index 100% rename from src/haapi-react-app/src/shared/util/types/media.types.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/types/media.types.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/useHaapiFetch.spec.ts b/src/haapi-react-sdk/haapi-stepper/data-access/useHaapiFetch.spec.ts similarity index 98% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/useHaapiFetch.spec.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/useHaapiFetch.spec.ts index 400773d1..c4d2c7b5 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/useHaapiFetch.spec.ts +++ b/src/haapi-react-sdk/haapi-stepper/data-access/useHaapiFetch.spec.ts @@ -12,7 +12,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { renderHook } from '@testing-library/react'; import type { HaapiConfiguration } from '@curity/identityserver-haapi-web-driver'; -import { MEDIA_TYPES } from '../../../shared/util/types/media.types'; +import { MEDIA_TYPES } from './types/media.types'; import { useHaapiFetch } from './useHaapiFetch'; import { HAAPI_STEPS, type HaapiLink } from './types/haapi-step.types'; import { HAAPI_ACTION_TYPES, type HaapiFormAction } from './types/haapi-action.types'; diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/useHaapiFetch.ts b/src/haapi-react-sdk/haapi-stepper/data-access/useHaapiFetch.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/data-access/useHaapiFetch.ts rename to src/haapi-react-sdk/haapi-stepper/data-access/useHaapiFetch.ts diff --git a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx new file mode 100644 index 00000000..7b7b5514 --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { HAAPI_STEPS } from '../../../data-access/types/haapi-step.types'; +import { + createMockExternalBrowserFlowAction, + createMockStep, + createMockWebAuthnAnyDeviceBothOptionsAction, + createMockWebAuthnCrossPlatformOnlyAnyDeviceAction, + createMockWebAuthnPlatformOnlyAnyDeviceAction, + createMockWebAuthnRegistrationAction, + externalBrowserFlowActionTitle, + webAuthnAnyDeviceActionTitle, + webAuthnRegistrationActionTitle, +} from '../../../util/tests/mocks'; +import { HaapiStepperActionsUI } from '../../../ui/actions/HaapiStepperActionsUI'; +import { HaapiStepperClientOperationUI } from './HaapiStepperClientOperationUI'; +import type { HaapiStepperStep } from '../../stepper/haapi-stepper.types'; +import { useHaapiStepper } from '../../stepper/HaapiStepperHook'; +import { useIsWebAuthnPlatformAuthenticatorAvailable } from './operations/webauthn/useIsWebAuthnPlatformAuthenticatorAvailable'; + +const PLATFORM_TITLE = 'Built-in'; +const PLATFORM_DESCRIPTION = 'A non-removable built-in device.'; +const PLATFORM_ICON_VIEW_BOX = '0 0 64.2 83.9'; +const CROSS_PLATFORM_TITLE = 'Security key'; +const CROSS_PLATFORM_DESCRIPTION = + 'A security key, such as a portable key attached through USB or wirelessly through NFC.'; +const CROSS_PLATFORM_ICON_VIEW_BOX = '0 0 49 72.6'; +const MESSAGE_PREFIX = 'authenticator.webauthn.register.view.'; +const VIEW_DATA_MESSAGES = { + [`${MESSAGE_PREFIX}button.platform`]: PLATFORM_TITLE, + [`${MESSAGE_PREFIX}button.cross-platform`]: CROSS_PLATFORM_TITLE, + [`${MESSAGE_PREFIX}authenticator-attachment.platform`]: PLATFORM_DESCRIPTION, + [`${MESSAGE_PREFIX}authenticator-attachment.cross-platform`]: CROSS_PLATFORM_DESCRIPTION, +}; + +vi.mock('./operations/webauthn/useIsWebAuthnPlatformAuthenticatorAvailable', () => ({ + useIsWebAuthnPlatformAuthenticatorAvailable: vi.fn(() => undefined), +})); + +// The attachment card resolves its copy from the current step's viewData messages via +// `useHaapiStepper`; tests drive that copy through the mocked hook. +vi.mock('../../stepper/HaapiStepperHook', () => ({ useHaapiStepper: vi.fn() })); + +const mockCurrentStepMessages = (messages?: Record) => + vi.mocked(useHaapiStepper).mockReturnValue({ + currentStep: (messages ? { metadata: { viewData: { messages } } } : null) as HaapiStepperStep | null, + } as ReturnType); + +describe('HaapiStepperClientOperationUI', () => { + let user: ReturnType; + + beforeEach(() => { + user = userEvent.setup(); + mockCurrentStepMessages(VIEW_DATA_MESSAGES); + }); + + afterEach(() => { + vi.mocked(useHaapiStepper).mockReset(); + }); + + describe('Default rendering', () => { + it('renders the action title as an enabled button', () => { + const action = createMockExternalBrowserFlowAction(); + + render(); + + expect(screen.getByRole('button', { name: externalBrowserFlowActionTitle })).toBeEnabled(); + }); + + it('does not render a progress bar when the action has no remaining wait time', () => { + const action = createMockExternalBrowserFlowAction(); + + render(); + + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + }); + + it('forwards the action to onAction when clicked', async () => { + const action = createMockExternalBrowserFlowAction(); + const onAction = vi.fn(); + + render(); + + await user.click(screen.getByRole('button', { name: externalBrowserFlowActionTitle })); + + expect(onAction).toHaveBeenCalledTimes(1); + expect(onAction).toHaveBeenCalledWith(action); + }); + }); + + describe('WebAuthn', () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.mocked(useIsWebAuthnPlatformAuthenticatorAvailable).mockReset(); + }); + + it('enables the button when the WebAuthn API is available', () => { + vi.stubGlobal('PublicKeyCredential', stubPublicKeyCredential()); + const action = createMockWebAuthnRegistrationAction(); + + render(); + + expect(screen.getByRole('button', { name: webAuthnRegistrationActionTitle })).toBeEnabled(); + }); + + it('disables the button when the WebAuthn API is unavailable', () => { + // jsdom does not expose `PublicKeyCredential`, so `isWebAuthnApiSupported()` returns false + // and the gate disables WebAuthn buttons. + const action = createMockWebAuthnRegistrationAction(); + + render(); + + expect(screen.getByRole('button', { name: webAuthnRegistrationActionTitle })).toBeDisabled(); + }); + + it('renders a default button (not a card) for passkeys-mode registration', () => { + const action = createMockWebAuthnRegistrationAction(); + + render(); + + expect(screen.queryByTestId('webauthn-registration-attachment-card')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: webAuthnRegistrationActionTitle })).toBeInTheDocument(); + }); + + it('disables a platform-only any-device registration when no platform authenticator is available', () => { + vi.stubGlobal('PublicKeyCredential', stubPublicKeyCredential()); + vi.mocked(useIsWebAuthnPlatformAuthenticatorAvailable).mockReturnValue(false); + const action = createMockWebAuthnPlatformOnlyAnyDeviceAction(); + + render(); + + expect(screen.getByTestId('webauthn-registration-attachment-card')).toBeDisabled(); + }); + + describe('WebAuthn registration attachment card (any device mode (platform/cross-platform))', () => { + it.each([ + { + name: 'platform', + makeAction: createMockWebAuthnPlatformOnlyAnyDeviceAction, + title: PLATFORM_TITLE, + description: PLATFORM_DESCRIPTION, + iconViewBox: PLATFORM_ICON_VIEW_BOX, + }, + { + name: 'cross-platform', + makeAction: createMockWebAuthnCrossPlatformOnlyAnyDeviceAction, + title: CROSS_PLATFORM_TITLE, + description: CROSS_PLATFORM_DESCRIPTION, + iconViewBox: CROSS_PLATFORM_ICON_VIEW_BOX, + }, + ])( + 'renders the $name attachment option as a card (title, description and matching icon)', + ({ makeAction, title, description, iconViewBox }) => { + const { container } = render(); + + expect(screen.getByTestId('webauthn-registration-attachment-card')).toBeInTheDocument(); + expect(screen.getByText(title)).toBeInTheDocument(); + expect(screen.getByText(description)).toBeInTheDocument(); + expect(container.querySelector('.haapi-stepper-webauthn-registration-attachment-icon svg')).toHaveAttribute( + 'viewBox', + iconViewBox + ); + } + ); + + it('renders one card per option for a both-options any-device registration', () => { + const step = createMockStep(HAAPI_STEPS.REGISTRATION, { + actions: [createMockWebAuthnAnyDeviceBothOptionsAction()], + }); + + render(); + + expect(screen.getAllByTestId('webauthn-registration-attachment-card')).toHaveLength(2); + expect(screen.getByText(PLATFORM_TITLE)).toBeInTheDocument(); + expect(screen.getByText(CROSS_PLATFORM_TITLE)).toBeInTheDocument(); + }); + + it('renders the cards with the split action titles as fallback when the step carries no messages', () => { + mockCurrentStepMessages(undefined); + const step = createMockStep(HAAPI_STEPS.REGISTRATION, { + actions: [createMockWebAuthnAnyDeviceBothOptionsAction()], + }); + + render(); + + const cards = screen.getAllByTestId('webauthn-registration-attachment-card'); + expect(cards).toHaveLength(2); + expect(screen.getByText(`${webAuthnAnyDeviceActionTitle} (Built-in)`)).toBeInTheDocument(); + expect(screen.getByText(`${webAuthnAnyDeviceActionTitle} (Security key)`)).toBeInTheDocument(); + expect(cards[0].querySelector('.haapi-stepper-webauthn-registration-attachment-description')).toBeNull(); + expect(cards[1].querySelector('.haapi-stepper-webauthn-registration-attachment-description')).toBeNull(); + }); + }); + }); +}); + +/** + * Minimal stand-in for the static `PublicKeyCredential` interface — enough for + * `isWebAuthnApiSupported()` to return true and for the platform-authenticator hook to resolve. + * jsdom doesn't expose `PublicKeyCredential`, so tests stub it to emulate a WebAuthn-capable + * browser without reaching into the real `navigator.credentials` API. + */ +const stubPublicKeyCredential = () => + Object.assign(vi.fn(), { + parseCreationOptionsFromJSON: vi.fn(), + parseRequestOptionsFromJSON: vi.fn(), + isUserVerifyingPlatformAuthenticatorAvailable: vi.fn(() => Promise.resolve(true)), + }); diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx similarity index 75% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx index 3a23a13f..8a6302ba 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx @@ -10,12 +10,13 @@ */ import { HaapiStepperClientOperationAction, HaapiStepperFormAction } from '../../stepper/haapi-stepper.types'; +import { HaapiStepperWebAuthnRegistrationAttachmentCard } from './operations/webauthn/HaapiStepperWebAuthnRegistrationAttachmentCard'; +import { isAnyDeviceWebAuthnRegistrationClientOperation } from './operations/webauthn'; import { useIsClientOperationAvailable } from './useIsClientOperationAvailable'; interface HaapiStepperClientOperationUIProps { action: HaapiStepperClientOperationAction; onAction: (action: HaapiStepperClientOperationAction | HaapiStepperFormAction) => void; - showBankIdSessionTimeLeft?: boolean; } /** @@ -43,22 +44,21 @@ interface HaapiStepperClientOperationUIProps { * * ``` */ -export function HaapiStepperClientOperationUI({ - action, - onAction, - showBankIdSessionTimeLeft = true, -}: HaapiStepperClientOperationUIProps) { +export function HaapiStepperClientOperationUI({ action, onAction }: HaapiStepperClientOperationUIProps) { const isAvailable = useIsClientOperationAvailable(action); + if (isAnyDeviceWebAuthnRegistrationClientOperation(action)) { + return ( + onAction(action)} + /> + ); + } + return (
- {showBankIdSessionTimeLeft && action.maxWaitRemainingTime !== undefined && ( - - )} diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/bankid.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/bankid.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/bankid.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/bankid.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/index.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/index.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/index.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/index.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/open-bankid-app.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/open-bankid-app.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/open-bankid-app.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/open-bankid-app.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/utils.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/utils.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/utils.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/bankid/utils.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/client-operations.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/client-operations.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/client-operations.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/client-operations.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.spec.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.spec.ts similarity index 81% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.spec.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.spec.ts index d793fc47..77e8a0e4 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.spec.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.spec.ts @@ -17,7 +17,16 @@ import { createMockExternalBrowserFlowAction, createMockStep } from '../../../.. describe('external-browser-flow', () => { const launchOrigin = new URL(createMockExternalBrowserFlowAction().model.arguments.href).origin; const closeDelay = 0; - const step = createMockStep(HAAPI_STEPS.AUTHENTICATION, { metadata: {} }); + const windowOpenError = 'window_open_error'; + const step = createMockStep(HAAPI_STEPS.AUTHENTICATION, { + metadata: { + viewData: { + messages: { + 'authenticator.external-browser.launch.error.browser-window': windowOpenError, + }, + }, + }, + }); let abortController: AbortController; let externalWindowClose: ReturnType; @@ -77,6 +86,22 @@ describe('external-browser-flow', () => { }, }); }); + + it('return no-action when abort signal fires (cancellation)', async () => { + const externalBrowserFlowResult = runExternalBrowserFlow( + createMockExternalBrowserFlowAction(), + closeDelay, + abortController.signal, + step + ); + + abortController.abort(); + + await expect(externalBrowserFlowResult).resolves.toMatchObject({ + clientOperationData: { action: null }, + }); + expect(externalWindowClose).toHaveBeenCalledTimes(1); + }); }); describe('failure', () => { @@ -94,14 +119,14 @@ describe('external-browser-flow', () => { clientOperationError: { app: { type: HAAPI_PROBLEM_STEPS.UNEXPECTED, - messages: [{ text: EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES.launch }], + messages: [{ text: windowOpenError }], }, }, }); expect(externalWindowClose).not.toHaveBeenCalled(); }); - it('message from unexpected origin → resume error copy', async () => { + it('message from unexpected origin → rejection', async () => { const externalBrowserFlowResult = runExternalBrowserFlow( createMockExternalBrowserFlowAction(), closeDelay, @@ -111,18 +136,11 @@ describe('external-browser-flow', () => { sendBrowserMessage({ source: fakeExternalWindow, origin: 'http://attacker.example', data: 'nonce-x' }); - await expect(externalBrowserFlowResult).resolves.toMatchObject({ - clientOperationError: { - app: { - type: HAAPI_PROBLEM_STEPS.UNEXPECTED, - messages: [{ text: EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES.resume }], - }, - }, - }); + await expect(externalBrowserFlowResult).rejects.toThrow(new Error(EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES.resume)); expect(externalWindowClose).toHaveBeenCalledTimes(1); }); - it('message with non-string data → resume error copy', async () => { + it('message with non-string data → rejection', async () => { const externalBrowserFlowResult = runExternalBrowserFlow( createMockExternalBrowserFlowAction(), closeDelay, @@ -132,35 +150,7 @@ describe('external-browser-flow', () => { sendBrowserMessage({ source: fakeExternalWindow, origin: launchOrigin, data: { not: 'a string' } }); - await expect(externalBrowserFlowResult).resolves.toMatchObject({ - clientOperationError: { - app: { - type: HAAPI_PROBLEM_STEPS.UNEXPECTED, - messages: [{ text: EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES.resume }], - }, - }, - }); - expect(externalWindowClose).toHaveBeenCalledTimes(1); - }); - - it('abort signal fires → resume error copy', async () => { - const externalBrowserFlowResult = runExternalBrowserFlow( - createMockExternalBrowserFlowAction(), - closeDelay, - abortController.signal, - step - ); - - abortController.abort(); - - await expect(externalBrowserFlowResult).resolves.toMatchObject({ - clientOperationError: { - app: { - type: HAAPI_PROBLEM_STEPS.UNEXPECTED, - messages: [{ text: EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES.resume }], - }, - }, - }); + await expect(externalBrowserFlowResult).rejects.toThrow(new Error(EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES.resume)); expect(externalWindowClose).toHaveBeenCalledTimes(1); }); }); diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.ts similarity index 65% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.ts index d195cb36..bcc2f54d 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/external-browser-flow.ts @@ -18,7 +18,6 @@ import { import { HaapiStep } from '../../../../../data-access/types/haapi-step.types'; import { ClientOperationResult } from '../typings'; import { getHaapiStepperError } from '../helpers'; -import { EXTERNAL_BROWSER_FLOW_ERROR_TYPE } from './typings'; /** * Executes an external browser flow by opening a new window at the launch URL and waiting for the @@ -30,16 +29,14 @@ export function runExternalBrowserFlow( abortSignal: AbortSignal, currentStep: HaapiStep | null ): Promise { - return new Promise(resolve => { + return new Promise((resolve, reject) => { const launchUrl = new URL(action.model.arguments.href, window.location.origin); launchUrl.searchParams.set('for_origin', window.location.origin); const externalWindow = window.open(launchUrl); if (!externalWindow) { resolve({ - clientOperationError: getHaapiStepperError( - getExternalBrowserFlowErrorMessage(EXTERNAL_BROWSER_FLOW_ERROR_TYPE.LAUNCH, currentStep) - ), + clientOperationError: getHaapiStepperError(getWindowOpenErrorMessage(currentStep)), }); return; } @@ -50,11 +47,7 @@ export function runExternalBrowserFlow( } if (event.origin !== launchUrl.origin || typeof event.data !== 'string') { cleanup(true); - resolve({ - clientOperationError: getHaapiStepperError( - getExternalBrowserFlowErrorMessage(EXTERNAL_BROWSER_FLOW_ERROR_TYPE.RESUME, currentStep) - ), - }); + reject(new Error(EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES.resume)); return; } @@ -69,11 +62,7 @@ export function runExternalBrowserFlow( const onAbort = () => { cleanup(true); - resolve({ - clientOperationError: getHaapiStepperError( - getExternalBrowserFlowErrorMessage(EXTERNAL_BROWSER_FLOW_ERROR_TYPE.RESUME, currentStep) - ), - }); + resolve({ clientOperationData: { action: null } }); }; window.addEventListener('message', onMessage); @@ -91,27 +80,16 @@ export function runExternalBrowserFlow( }); } -/** - * Hardcoded English copy for each `EXTERNAL_BROWSER_FLOW_ERROR_TYPE` bucket. Used as the single - * source of user-facing copy until the BE emits the equivalent - * `step.metadata.viewData.error.clientOperation.externalBrowserFlow.` keys (tracked - * separately). When those keys land, switch `getExternalBrowserFlowErrorMessage` to read from - * `currentStep` first and fall back to this map. - */ export const EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES = { - launch: 'External browser flow could not start.', resume: 'External browser flow could not be resumed.', + launch: 'External browser flow could not start.', } as const; -function getExternalBrowserFlowErrorMessage( - type: EXTERNAL_BROWSER_FLOW_ERROR_TYPE, - currentStep: HaapiStep | null -): string { - // `currentStep` is kept on the signature for forward-compat with BE-supplied viewData copy. - void currentStep; - return type === EXTERNAL_BROWSER_FLOW_ERROR_TYPE.LAUNCH - ? EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES.launch - : EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES.resume; +function getWindowOpenErrorMessage(currentStep: HaapiStep | null): string { + return ( + currentStep?.metadata?.viewData?.messages?.['authenticator.external-browser.launch.error.browser-window'] ?? + EXTERNAL_BROWSER_FLOW_ERROR_MESSAGES.launch + ); } export const isExternalBrowserFlowClientOperation = ( diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/index.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/index.ts similarity index 93% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/index.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/index.ts index cebfcd5b..afa63cc6 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/index.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow/index.ts @@ -10,4 +10,3 @@ */ export * from './external-browser-flow'; -export * from './typings'; diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/helpers.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/helpers.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/helpers.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/helpers.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/typings.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/typings.ts similarity index 68% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/typings.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/typings.ts index 2f307798..5100f851 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/typings.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/typings.ts @@ -14,14 +14,14 @@ import { HaapiStepperError } from '../../../stepper/haapi-stepper.types'; /** * Discriminated-union return shape shared by all client-operation runners (WebAuthn, - * external-browser-flow, BankID — as each ports onto this pattern per IS-11327). + * external-browser-flow, BankID). * - * Runners always resolve. Success carries the continuation form action + payload; failure - * carries a synthesised {@link HaapiStepperError} which `performClientOperation` forwards to the - * stepper, which routes it through `setError` → `useHaapiStepper().error.app`. Programming + * Runners always resolve. Success carries the continuation form action + payload, or explicit null if no action + * should be taken; failure carries a synthesised {@link HaapiStepperError} which `performClientOperation` forwards + * to the stepper, which routes it through `setError` → `useHaapiStepper().error.app`. Programming * bugs / unexpected runtime errors are not represented here — those still throw and escape to * the React error boundary. */ export type ClientOperationResult = - | { clientOperationData: HaapiFetchFormAction; clientOperationError?: undefined } + | { clientOperationData: HaapiFetchFormAction | { action: null }; clientOperationError?: undefined } | { clientOperationData?: undefined; clientOperationError: HaapiStepperError }; diff --git a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/HaapiStepperWebAuthnRegistrationAttachmentCard.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/HaapiStepperWebAuthnRegistrationAttachmentCard.spec.tsx new file mode 100644 index 00000000..0b46122d --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/HaapiStepperWebAuthnRegistrationAttachmentCard.spec.tsx @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + createMockWebAuthnCrossPlatformOnlyAnyDeviceAction, + createMockWebAuthnPlatformOnlyAnyDeviceAction, + webAuthnPlatformOnlyAnyDeviceActionTitle, +} from '../../../../../util/tests/mocks'; +import { HaapiStepperWebAuthnRegistrationAttachmentCard } from './HaapiStepperWebAuthnRegistrationAttachmentCard'; +import { useHaapiStepper } from '../../../../stepper/HaapiStepperHook'; +import type { HaapiStepperStep } from '../../../../stepper/haapi-stepper.types'; + +vi.mock('../../../../stepper/HaapiStepperHook', () => ({ useHaapiStepper: vi.fn() })); + +const PLATFORM_ICON_VIEW_BOX = '0 0 64.2 83.9'; +const CROSS_PLATFORM_ICON_VIEW_BOX = '0 0 49 72.6'; +const PREFIX = 'authenticator.webauthn.register.view.'; +const PLATFORM_TITLE = 'Built-in'; +const PLATFORM_DESCRIPTION = 'A non-removable built-in device.'; +const CROSS_PLATFORM_TITLE = 'Security key'; + +// The card resolves its copy from the current step's viewData messages via `useHaapiStepper`. +const mockCurrentStepMessages = (messages?: Record) => + vi.mocked(useHaapiStepper).mockReturnValue({ + currentStep: (messages ? { metadata: { viewData: { messages } } } : null) as HaapiStepperStep | null, + } as ReturnType); + +// Safe default so a test that forgets to configure the hook gets a clear assertion +// failure rather than an opaque "cannot destructure currentStep" crash. +beforeEach(() => mockCurrentStepMessages(undefined)); + +afterEach(() => vi.mocked(useHaapiStepper).mockReset()); + +describe('HaapiStepperWebAuthnRegistrationAttachmentCard', () => { + it("renders the title, description and matching icon from the step's data", () => { + mockCurrentStepMessages({ + [`${PREFIX}button.platform`]: PLATFORM_TITLE, + [`${PREFIX}authenticator-attachment.platform`]: PLATFORM_DESCRIPTION, + }); + + const { container } = render( + + ); + + expect(screen.getByText(PLATFORM_TITLE)).toBeInTheDocument(); + expect(screen.getByText(PLATFORM_DESCRIPTION)).toBeInTheDocument(); + expect(container.querySelector('.haapi-stepper-webauthn-registration-attachment-icon svg')).toHaveAttribute( + 'viewBox', + PLATFORM_ICON_VIEW_BOX + ); + }); + + it('omits the description when the step has no description message, still rendering the icon', () => { + mockCurrentStepMessages({ [`${PREFIX}button.cross-platform`]: CROSS_PLATFORM_TITLE }); + + const { container } = render( + + ); + + const card = screen.getByTestId('webauthn-registration-attachment-card'); + expect(screen.getByText(CROSS_PLATFORM_TITLE)).toBeInTheDocument(); + expect(card.querySelector('.haapi-stepper-webauthn-registration-attachment-description')).toBeNull(); + expect(container.querySelector('.haapi-stepper-webauthn-registration-attachment-icon svg')).toHaveAttribute( + 'viewBox', + CROSS_PLATFORM_ICON_VIEW_BOX + ); + }); + + it('falls back to the action title when the step carries no view-data', () => { + mockCurrentStepMessages(undefined); + + render( + + ); + + expect(screen.getByText(webAuthnPlatformOnlyAnyDeviceActionTitle)).toBeInTheDocument(); + }); + + it('calls onClick when the card is clicked', async () => { + const user = userEvent.setup(); + const onClick = vi.fn(); + mockCurrentStepMessages(undefined); + + render( + + ); + + await user.click(screen.getByTestId('webauthn-registration-attachment-card')); + expect(onClick).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/HaapiStepperWebAuthnRegistrationAttachmentCard.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/HaapiStepperWebAuthnRegistrationAttachmentCard.tsx new file mode 100644 index 00000000..888bc549 --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/HaapiStepperWebAuthnRegistrationAttachmentCard.tsx @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { HaapiStepperWebAuthnAnyDeviceRegistrationAction } from '../../../../stepper/haapi-stepper.types'; +import { useHaapiStepper } from '../../../../stepper/HaapiStepperHook'; +import { getAnyDeviceWebAuthnRegistrationAttachment } from './webauthn-registration-attachment'; +import { REGISTRATION_ATTACHMENT_ICON } from './webauthn-registration-attachment-icon-map'; + +export interface HaapiStepperWebAuthnRegistrationAttachmentCardProps { + /** The (split) any-device `webauthn-registration` action this option advances the flow with. */ + action: HaapiStepperWebAuthnAnyDeviceRegistrationAction; + /** Disables the card (e.g. when the required authenticator capability is unavailable). */ + disabled?: boolean; + onClick: () => void; +} + +/** + * A single WebAuthn registration attachment-selection option, rendered as a clickable card + * (icon + bold title + description). + * + * Must be rendered within `HaapiStepper`, for the current active step's action. + */ +export const HaapiStepperWebAuthnRegistrationAttachmentCard = ({ + action, + disabled, + onClick, +}: HaapiStepperWebAuthnRegistrationAttachmentCardProps) => { + const { currentStep } = useHaapiStepper(); + const { kind, title, description } = getAnyDeviceWebAuthnRegistrationAttachment( + action, + currentStep?.metadata?.viewData?.messages + ); + + return ( + + ); +}; diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/auto-start.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/auto-start.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/auto-start.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/auto-start.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/index.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/index.ts similarity index 78% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/index.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/index.ts index a2b588a6..6ef36b03 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/index.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/index.ts @@ -13,3 +13,5 @@ export * from './webauthn'; export * from './utils'; export * from './useIsWebAuthnPlatformAuthenticatorAvailable'; export * from './auto-start'; +export * from './HaapiStepperWebAuthnRegistrationAttachmentCard'; +export * from './webauthn-registration-attachment-icon-map'; diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/typings.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/typings.ts similarity index 85% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/typings.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/typings.ts index cf468efc..89244cba 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/typings.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/typings.ts @@ -1,5 +1,6 @@ export enum WEBAUTHN_ERROR_TYPE { CANCEL_OR_TIMEOUT = 'cancelOrTimeout', + NOT_SUPPORTED = 'notSupported', FAILED = 'failed', } diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/useIsWebAuthnPlatformAuthenticatorAvailable.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/useIsWebAuthnPlatformAuthenticatorAvailable.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/useIsWebAuthnPlatformAuthenticatorAvailable.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/useIsWebAuthnPlatformAuthenticatorAvailable.ts diff --git a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/utils.spec.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/utils.spec.ts new file mode 100644 index 00000000..00ae0128 --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/utils.spec.ts @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { describe, expect, it } from 'vitest'; + +import { + createMockWebAuthnAnyDeviceBothOptionsAction, + createMockWebAuthnAuthenticationAction, + createMockWebAuthnCrossPlatformOnlyAnyDeviceAction, + createMockWebAuthnPlatformOnlyAnyDeviceAction, + createMockWebAuthnRegistrationAction, +} from '../../../../../util/tests/mocks'; +import { getWebAuthnRegistrationAttachmentKind, isAnyDeviceWebAuthnRegistrationClientOperation } from './utils'; + +describe('isAnyDeviceWebAuthnRegistrationClientOperation', () => { + it('is true for any-device registration actions (platform-only, cross-platform-only, both options)', () => { + expect(isAnyDeviceWebAuthnRegistrationClientOperation(createMockWebAuthnPlatformOnlyAnyDeviceAction())).toBe(true); + expect(isAnyDeviceWebAuthnRegistrationClientOperation(createMockWebAuthnCrossPlatformOnlyAnyDeviceAction())).toBe( + true + ); + expect(isAnyDeviceWebAuthnRegistrationClientOperation(createMockWebAuthnAnyDeviceBothOptionsAction())).toBe(true); + }); + + it('is false for a passkeys-mode registration action', () => { + expect(isAnyDeviceWebAuthnRegistrationClientOperation(createMockWebAuthnRegistrationAction())).toBe(false); + }); + + it('is false for a non-registration (authentication) action', () => { + expect(isAnyDeviceWebAuthnRegistrationClientOperation(createMockWebAuthnAuthenticationAction())).toBe(false); + }); +}); + +describe('getWebAuthnRegistrationAttachmentKind', () => { + it('returns "platform" for a platform-only any-device action', () => { + expect(getWebAuthnRegistrationAttachmentKind(createMockWebAuthnPlatformOnlyAnyDeviceAction())).toBe('platform'); + }); + + it('returns "cross-platform" for a cross-platform-only any-device action', () => { + expect(getWebAuthnRegistrationAttachmentKind(createMockWebAuthnCrossPlatformOnlyAnyDeviceAction())).toBe( + 'cross-platform' + ); + }); + + it('returns "platform" for a both-options any-device action (platform takes precedence)', () => { + expect(getWebAuthnRegistrationAttachmentKind(createMockWebAuthnAnyDeviceBothOptionsAction())).toBe('platform'); + }); +}); diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/utils.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/utils.ts similarity index 83% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/utils.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/utils.ts index 2c1ebc62..acac1424 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/utils.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/utils.ts @@ -19,7 +19,7 @@ import { HaapiWebAuthnPasskeysRegistrationAction, HaapiWebAuthnRegistrationClientOperationAction, } from '../../../../../data-access/types/haapi-action.types'; -import type { HaapiStepperStep } from '../../../../stepper/haapi-stepper.types'; +import { WebAuthnRegistrationAttachmentKind, type HaapiStepperStep } from '../../../../stepper/haapi-stepper.types'; const WEBAUTHN_PLATFORM_LABEL = 'Built-in'; const WEBAUTHN_CROSS_PLATFORM_LABEL = 'Security key'; @@ -106,7 +106,7 @@ export const isWebAuthnClientOperationAction = (action: HaapiAction): boolean => isWebAuthnRegistrationClientOperation(action) || isWebAuthnAuthenticationClientOperation(action); export function isPlatformOnlyAnyDeviceWebAuthnRegistrationAction(action: HaapiAction): boolean { - if (!isWebAuthnRegistrationClientOperation(action) || !isAnyDeviceWebAuthnRegistrationAction(action)) { + if (!isAnyDeviceWebAuthnRegistrationClientOperation(action)) { return false; } @@ -138,6 +138,29 @@ export function isAnyDeviceWebAuthnRegistrationAction( return !isPasskeysWebAuthnRegistrationAction(action); } +/** + * Any-device `webauthn-registration` client operation — the only registration case that offers a + * platform vs cross-platform attachment choice (passkeys mode collapses them into a single option). + */ +export function isAnyDeviceWebAuthnRegistrationClientOperation( + action: T +): action is T & HaapiWebAuthnAnyDeviceRegistrationAction { + return isWebAuthnRegistrationClientOperation(action) && isAnyDeviceWebAuthnRegistrationAction(action); +} + +/** + * Which attachment a (split) any-device `webauthn-registration` action carries — platform (built-in) + * or cross-platform (security key). Callers must first confirm the action with + * `isAnyDeviceWebAuthnRegistrationClientOperation`. + */ +export function getWebAuthnRegistrationAttachmentKind( + action: HaapiWebAuthnAnyDeviceRegistrationAction +): WebAuthnRegistrationAttachmentKind { + return action.model.arguments.platformCredentialCreationOptions + ? WebAuthnRegistrationAttachmentKind.PLATFORM + : WebAuthnRegistrationAttachmentKind.CROSS_PLATFORM; +} + export const isWebAuthnStep = (step: HaapiStepperStep): boolean => { const clientOperationActions = step.dataHelpers.actions?.clientOperation ?? []; return clientOperationActions.some(isWebAuthnClientOperationAction); diff --git a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn-registration-attachment-icon-map.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn-registration-attachment-icon-map.tsx new file mode 100644 index 00000000..fdac9abb --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn-registration-attachment-icon-map.tsx @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import type { ReactNode } from 'react'; +import { IconAuthenticatorWebauthnCrossPlatform, IconAuthenticatorWebauthnPlatform } from '@curity/ui-kit-icons'; + +import { WebAuthnRegistrationAttachmentKind } from '../../../../stepper/haapi-stepper.types'; + +/** + * Maps each WebAuthn attachment kind to its icon from the shared `@curity/ui-kit-icons` package. + * The icons use `currentColor`, so their color follows the surrounding text color. + */ +export const REGISTRATION_ATTACHMENT_ICON: Record = { + [WebAuthnRegistrationAttachmentKind.PLATFORM]: , + [WebAuthnRegistrationAttachmentKind.CROSS_PLATFORM]: , +}; diff --git a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn-registration-attachment.spec.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn-registration-attachment.spec.ts new file mode 100644 index 00000000..f58e885a --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn-registration-attachment.spec.ts @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { describe, expect, it } from 'vitest'; + +import { + createMockWebAuthnCrossPlatformOnlyAnyDeviceAction, + createMockWebAuthnPlatformOnlyAnyDeviceAction, + webAuthnPlatformOnlyAnyDeviceActionTitle, +} from '../../../../../util/tests/mocks'; +import { WebAuthnRegistrationAttachmentKind } from '../../../../stepper/haapi-stepper.types'; +import { getAnyDeviceWebAuthnRegistrationAttachment } from './webauthn-registration-attachment'; + +const PREFIX = 'authenticator.webauthn.register.view.'; +const PLATFORM_TITLE = 'Built-in'; +const PLATFORM_DESCRIPTION = 'A non-removable built-in device.'; +const CROSS_PLATFORM_TITLE = 'Security key'; +const CROSS_PLATFORM_DESCRIPTION = 'A security key.'; +const VIEW_DATA_MESSAGES = { + [`${PREFIX}button.platform`]: PLATFORM_TITLE, + [`${PREFIX}button.cross-platform`]: CROSS_PLATFORM_TITLE, + [`${PREFIX}authenticator-attachment.platform`]: PLATFORM_DESCRIPTION, + [`${PREFIX}authenticator-attachment.cross-platform`]: CROSS_PLATFORM_DESCRIPTION, +}; + +describe('getAnyDeviceWebAuthnRegistrationAttachment', () => { + it('resolves the platform kind, localized title and description from the view-data messages', () => { + const result = getAnyDeviceWebAuthnRegistrationAttachment(createMockWebAuthnPlatformOnlyAnyDeviceAction(), { + ...VIEW_DATA_MESSAGES, + 'authenticator.webauthn.register.page.title': 'Register a device', + }); + + expect(result).toEqual({ + kind: WebAuthnRegistrationAttachmentKind.PLATFORM, + title: PLATFORM_TITLE, + description: PLATFORM_DESCRIPTION, + }); + }); + + it('resolves the cross-platform copy for a cross-platform action', () => { + const result = getAnyDeviceWebAuthnRegistrationAttachment( + createMockWebAuthnCrossPlatformOnlyAnyDeviceAction(), + VIEW_DATA_MESSAGES + ); + + expect(result).toEqual({ + kind: WebAuthnRegistrationAttachmentKind.CROSS_PLATFORM, + title: CROSS_PLATFORM_TITLE, + description: CROSS_PLATFORM_DESCRIPTION, + }); + }); + + it('falls back to the action title and omits the description when there are no messages', () => { + const result = getAnyDeviceWebAuthnRegistrationAttachment( + createMockWebAuthnPlatformOnlyAnyDeviceAction(), + undefined + ); + + expect(result).toEqual({ + kind: WebAuthnRegistrationAttachmentKind.PLATFORM, + title: webAuthnPlatformOnlyAnyDeviceActionTitle, + }); + }); +}); diff --git a/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn-registration-attachment.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn-registration-attachment.ts new file mode 100644 index 00000000..6f99401a --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn-registration-attachment.ts @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { HaapiWebAuthnAnyDeviceRegistrationAction } from '../../../../../data-access/types/haapi-action.types'; +import { + HaapiStepperWebAuthnRegistrationAttachment, + WebAuthnRegistrationAttachmentKind, +} from '../../../../stepper/haapi-stepper.types'; +import { getWebAuthnRegistrationAttachmentKind } from './utils'; + +/** + * Builds the registration attachment of a (split) any-device `webauthn-registration` action: + * + * - `kind` — the attachment the action carries, derived from its `model.arguments`. + * - `title` / `description` — the attachment option copy looked up from `metadata.viewData.messages`, + * with the action title as the `title` fallback. + */ +export function getAnyDeviceWebAuthnRegistrationAttachment( + action: HaapiWebAuthnAnyDeviceRegistrationAction, + viewDataMessages?: Record +): HaapiStepperWebAuthnRegistrationAttachment { + // The exact keys the webauthn authenticator uses for the attachment-selection copy. + const attachmentKey = (property: 'button' | 'authenticator-attachment', kind: WebAuthnRegistrationAttachmentKind) => + `authenticator.webauthn.register.view.${property}.${kind}`; + + const kind = getWebAuthnRegistrationAttachmentKind(action); + const title = viewDataMessages?.[attachmentKey('button', kind)] ?? action.title; + const description = viewDataMessages?.[attachmentKey('authenticator-attachment', kind)]; + + return { + kind, + ...(title !== undefined && { title }), + ...(description !== undefined && { description }), + }; +} diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.spec.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.spec.ts similarity index 57% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.spec.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.spec.ts index f465f289..b58c1bef 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.spec.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.spec.ts @@ -23,6 +23,9 @@ describe('webauthn', () => { const abortSignal = new AbortController().signal; const stepWithoutMetadata: HaapiStep | null = null; + const stepWithMessages = (messages: Record): HaapiStep => + ({ metadata: { viewData: { messages } } }) as HaapiStep; + beforeEach(() => { vi.clearAllMocks(); mockParseCreationOptionsFromJSON.mockReset(); @@ -112,7 +115,7 @@ describe('webauthn', () => { }); describe('error', () => { - it('WebAuthn API not supported → registrationError copy', async () => { + it('WebAuthn API not supported → notSupported copy', async () => { vi.unstubAllGlobals(); await expect( @@ -121,13 +124,29 @@ describe('webauthn', () => { clientOperationError: { app: { type: HAAPI_PROBLEM_STEPS.UNEXPECTED, - messages: [{ text: WEBAUTHN_ERROR_MESSAGES.registration }], + messages: [{ text: WEBAUTHN_ERROR_MESSAGES.notSupported }], + }, + }, + }); + + // passkeys prefix — the suffix match resolves the copy regardless of the authenticator prefix + const localizedErrorMessage = 'Passkeys stöds inte i denna webbläsare.'; + const localizedStep = stepWithMessages({ + 'authenticator.passkeys.register.view.error.not-supported': localizedErrorMessage, + }); + await expect( + runWebAuthnRegistration(createMockWebAuthnRegistrationAction(), abortSignal, localizedStep) + ).resolves.toMatchObject({ + clientOperationError: { + app: { + type: HAAPI_PROBLEM_STEPS.UNEXPECTED, + messages: [{ text: localizedErrorMessage }], }, }, }); }); - it('navigator.credentials.create returns null → registrationError copy', async () => { + it('navigator.credentials.create returns null → registration copy', async () => { mockCredentialsCreate.mockResolvedValue(null); await expect( @@ -140,11 +159,44 @@ describe('webauthn', () => { }, }, }); + + const localizedErrorMessage = 'Kunde inte registrera enheten.'; + const localizedStep = stepWithMessages({ + 'authenticator.webauthn.register.view.error.registration': localizedErrorMessage, + }); + await expect( + runWebAuthnRegistration(createMockWebAuthnRegistrationAction(), abortSignal, localizedStep) + ).resolves.toMatchObject({ + clientOperationError: { + app: { + type: HAAPI_PROBLEM_STEPS.UNEXPECTED, + messages: [{ text: localizedErrorMessage }], + }, + }, + }); + }); + + it('falls back to the English copy when the server message is an empty string', async () => { + mockCredentialsCreate.mockResolvedValue(null); + const stepWithEmptyMessage = stepWithMessages({ + 'authenticator.webauthn.register.view.error.registration': '', + }); + + await expect( + runWebAuthnRegistration(createMockWebAuthnRegistrationAction(), abortSignal, stepWithEmptyMessage) + ).resolves.toMatchObject({ + clientOperationError: { + app: { + type: HAAPI_PROBLEM_STEPS.UNEXPECTED, + messages: [{ text: WEBAUTHN_ERROR_MESSAGES.registration }], + }, + }, + }); }); describe('parseCreationOptionsFromJSON throws', () => { it.each(['EncodingError', 'SecurityError'] as const)( - '%s → registrationError copy (failed bucket)', + '%s → registration copy (failed bucket)', async errorName => { mockParseCreationOptionsFromJSON.mockImplementation(() => { throw new DOMException(`${errorName} message`, errorName); @@ -160,6 +212,21 @@ describe('webauthn', () => { }, }, }); + + const localizedErrorMessage = 'Kunde inte registrera enheten.'; + const localizedStep = stepWithMessages({ + 'authenticator.webauthn.register.view.error.registration': localizedErrorMessage, + }); + await expect( + runWebAuthnRegistration(createMockWebAuthnRegistrationAction(), abortSignal, localizedStep) + ).resolves.toMatchObject({ + clientOperationError: { + app: { + type: HAAPI_PROBLEM_STEPS.UNEXPECTED, + messages: [{ text: localizedErrorMessage }], + }, + }, + }); } ); }); @@ -168,7 +235,7 @@ describe('webauthn', () => { it.each([ ['NotAllowedError', new DOMException('user cancelled', 'NotAllowedError')], ['AbortError (non-caller-triggered, signal not aborted)', new DOMException('internal timeout', 'AbortError')], - ] as const)('%s → cancelOrTimeoutError copy', async (_label, error) => { + ] as const)('%s → cancelOrTimeout copy', async (_label, error) => { mockCredentialsCreate.mockRejectedValue(error); await expect( @@ -181,6 +248,21 @@ describe('webauthn', () => { }, }, }); + + const localizedErrorMessage = 'Avbröts eller tog för lång tid.'; + const localizedStep = stepWithMessages({ + 'authenticator.webauthn.register.view.error.cancel-or-timeout': localizedErrorMessage, + }); + await expect( + runWebAuthnRegistration(createMockWebAuthnRegistrationAction(), abortSignal, localizedStep) + ).resolves.toMatchObject({ + clientOperationError: { + app: { + type: HAAPI_PROBLEM_STEPS.UNEXPECTED, + messages: [{ text: localizedErrorMessage }], + }, + }, + }); }); it.each([ @@ -224,7 +306,7 @@ describe('webauthn', () => { }); describe('error', () => { - it('WebAuthn API not supported → authenticationError copy (failed bucket)', async () => { + it('WebAuthn API not supported → notSupported copy', async () => { vi.unstubAllGlobals(); await expect( @@ -233,13 +315,28 @@ describe('webauthn', () => { clientOperationError: { app: { type: HAAPI_PROBLEM_STEPS.UNEXPECTED, - messages: [{ text: WEBAUTHN_ERROR_MESSAGES.authentication }], + messages: [{ text: WEBAUTHN_ERROR_MESSAGES.notSupported }], + }, + }, + }); + + const localizedErrorMessage = 'Passkeys stöds inte i denna webbläsare.'; + const localizedStep = stepWithMessages({ + 'authenticator.passkeys.authenticate-device.view.error.not-supported': localizedErrorMessage, + }); + await expect( + runWebAuthnAuthentication(createMockWebAuthnAuthenticationAction(), abortSignal, localizedStep) + ).resolves.toMatchObject({ + clientOperationError: { + app: { + type: HAAPI_PROBLEM_STEPS.UNEXPECTED, + messages: [{ text: localizedErrorMessage }], }, }, }); }); - it('navigator.credentials.get returns null → authenticationError copy', async () => { + it('navigator.credentials.get returns null → authentication copy', async () => { mockCredentialsGet.mockResolvedValue(null); await expect( @@ -252,9 +349,24 @@ describe('webauthn', () => { }, }, }); + + const localizedErrorMessage = 'Kunde inte autentisera.'; + const localizedStep = stepWithMessages({ + 'authenticator.webauthn.authenticate-device.view.error.authentication': localizedErrorMessage, + }); + await expect( + runWebAuthnAuthentication(createMockWebAuthnAuthenticationAction(), abortSignal, localizedStep) + ).resolves.toMatchObject({ + clientOperationError: { + app: { + type: HAAPI_PROBLEM_STEPS.UNEXPECTED, + messages: [{ text: localizedErrorMessage }], + }, + }, + }); }); - it('parseRequestOptionsFromJSON throws SecurityError → authenticationError copy', async () => { + it('parseRequestOptionsFromJSON throws SecurityError → authentication copy', async () => { mockParseRequestOptionsFromJSON.mockImplementation(() => { throw new DOMException('rp id mismatch', 'SecurityError'); }); @@ -269,10 +381,25 @@ describe('webauthn', () => { }, }, }); + + const localizedErrorMessage = 'Kunde inte autentisera.'; + const localizedStep = stepWithMessages({ + 'authenticator.webauthn.authenticate-device.view.error.authentication': localizedErrorMessage, + }); + await expect( + runWebAuthnAuthentication(createMockWebAuthnAuthenticationAction(), abortSignal, localizedStep) + ).resolves.toMatchObject({ + clientOperationError: { + app: { + type: HAAPI_PROBLEM_STEPS.UNEXPECTED, + messages: [{ text: localizedErrorMessage }], + }, + }, + }); }); describe('navigator.credentials.get throws', () => { - it.each(['NotAllowedError', 'AbortError'])('%s → cancelOrTimeoutError copy', async errorName => { + it.each(['NotAllowedError', 'AbortError'])('%s → cancelOrTimeout copy', async errorName => { mockCredentialsGet.mockRejectedValue(new DOMException(`${errorName} message`, errorName)); await expect( @@ -285,10 +412,25 @@ describe('webauthn', () => { }, }, }); + + const localizedErrorMessage = 'Avbröts eller tog för lång tid.'; + const localizedStep = stepWithMessages({ + 'authenticator.webauthn.authenticate-device.view.error.cancel-or-timeout': localizedErrorMessage, + }); + await expect( + runWebAuthnAuthentication(createMockWebAuthnAuthenticationAction(), abortSignal, localizedStep) + ).resolves.toMatchObject({ + clientOperationError: { + app: { + type: HAAPI_PROBLEM_STEPS.UNEXPECTED, + messages: [{ text: localizedErrorMessage }], + }, + }, + }); }); it.each(['TimeoutError', 'NetworkError', 'IdentityCredentialError', 'SecurityError'])( - '%s → authenticationError copy (failed bucket)', + '%s → authentication copy (failed bucket)', async errorName => { mockCredentialsGet.mockRejectedValue(new DOMException(`${errorName} message`, errorName)); @@ -302,6 +444,21 @@ describe('webauthn', () => { }, }, }); + + const localizedErrorMessage = 'Kunde inte autentisera.'; + const localizedStep = stepWithMessages({ + 'authenticator.webauthn.authenticate-device.view.error.authentication': localizedErrorMessage, + }); + await expect( + runWebAuthnAuthentication(createMockWebAuthnAuthenticationAction(), abortSignal, localizedStep) + ).resolves.toMatchObject({ + clientOperationError: { + app: { + type: HAAPI_PROBLEM_STEPS.UNEXPECTED, + messages: [{ text: localizedErrorMessage }], + }, + }, + }); } ); @@ -321,6 +478,55 @@ describe('webauthn', () => { }); }); }); + + describe('selects the matching error copy from a full view-data message set', () => { + const registrationErrors = (prefix: string) => ({ + [`${prefix}.register.view.error.registration`]: `${prefix} registration failed`, + [`${prefix}.register.view.error.cancel-or-timeout`]: `${prefix} registration cancelled`, + [`${prefix}.register.view.error.not-supported`]: `${prefix} registration not supported`, + }); + const authenticationErrors = (prefix: string) => ({ + [`${prefix}.authenticate-device.view.error.authentication`]: `${prefix} authentication failed`, + [`${prefix}.authenticate-device.view.error.cancel-or-timeout`]: `${prefix} authentication cancelled`, + [`${prefix}.authenticate-device.view.error.not-supported`]: `${prefix} authentication not supported`, + }); + + it.each(['authenticator.webauthn', 'authenticator.passkeys'])('registration errors (%s)', async prefix => { + const messages = registrationErrors(prefix); + const step = stepWithMessages(messages); + const expectRegistrationError = (text: string) => + expect( + runWebAuthnRegistration(createMockWebAuthnRegistrationAction(), abortSignal, step) + ).resolves.toMatchObject({ clientOperationError: { app: { messages: [{ text }] } } }); + + mockCredentialsCreate.mockResolvedValue(null); + await expectRegistrationError(messages[`${prefix}.register.view.error.registration`]); + + mockCredentialsCreate.mockRejectedValue(new DOMException('cancelled', 'NotAllowedError')); + await expectRegistrationError(messages[`${prefix}.register.view.error.cancel-or-timeout`]); + + vi.unstubAllGlobals(); + await expectRegistrationError(messages[`${prefix}.register.view.error.not-supported`]); + }); + + it.each(['authenticator.webauthn', 'authenticator.passkeys'])('authentication errors (%s)', async prefix => { + const messages = authenticationErrors(prefix); + const step = stepWithMessages(messages); + const expectAuthenticationError = (text: string) => + expect( + runWebAuthnAuthentication(createMockWebAuthnAuthenticationAction(), abortSignal, step) + ).resolves.toMatchObject({ clientOperationError: { app: { messages: [{ text }] } } }); + + mockCredentialsGet.mockResolvedValue(null); + await expectAuthenticationError(messages[`${prefix}.authenticate-device.view.error.authentication`]); + + mockCredentialsGet.mockRejectedValue(new DOMException('cancelled', 'NotAllowedError')); + await expectAuthenticationError(messages[`${prefix}.authenticate-device.view.error.cancel-or-timeout`]); + + vi.unstubAllGlobals(); + await expectAuthenticationError(messages[`${prefix}.authenticate-device.view.error.not-supported`]); + }); + }); }); const mockParseCreationOptionsFromJSON = vi.fn(); diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.ts similarity index 76% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.ts index 89817c16..5f25c974 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/operations/webauthn/webauthn.ts @@ -72,7 +72,7 @@ async function createWebAuthnRegistrationCredential( if (!isWebAuthnApiSupported()) { return { error: getHaapiStepperError( - getWebAuthnErrorMessage(WEBAUTHN_ERROR_TYPE.FAILED, WEBAUTHN_OPERATION.REGISTRATION, currentStep) + getWebAuthnErrorMessage(WEBAUTHN_ERROR_TYPE.NOT_SUPPORTED, WEBAUTHN_OPERATION.REGISTRATION, currentStep) ), }; } @@ -110,7 +110,7 @@ async function getWebAuthnAuthenticationCredential( if (!isWebAuthnApiSupported()) { return { error: getHaapiStepperError( - getWebAuthnErrorMessage(WEBAUTHN_ERROR_TYPE.FAILED, WEBAUTHN_OPERATION.AUTHENTICATION, currentStep) + getWebAuthnErrorMessage(WEBAUTHN_ERROR_TYPE.NOT_SUPPORTED, WEBAUTHN_OPERATION.AUTHENTICATION, currentStep) ), }; } @@ -144,23 +144,48 @@ async function getWebAuthnAuthenticationCredential( export const WEBAUTHN_ERROR_MESSAGES = { cancelOrTimeout: 'The operation was cancelled or timed out.', + notSupported: 'WebAuthn is currently not supported in this browser.', registration: 'Registration failed.', authentication: 'Authentication failed.', } as const; +// The stable key suffix each error resolves to in `metadata.viewData.messages`. The full key's +// prefix varies per authenticator + flow (webauthn/passkeys, register/authenticate-device), but the +// step carries a single authenticator+flow, so a suffix match uniquely selects the localized copy. +function getWebAuthnErrorViewDataSuffix(type: WEBAUTHN_ERROR_TYPE, operation: WEBAUTHN_OPERATION): string { + switch (type) { + case WEBAUTHN_ERROR_TYPE.CANCEL_OR_TIMEOUT: + return '.view.error.cancel-or-timeout'; + case WEBAUTHN_ERROR_TYPE.NOT_SUPPORTED: + return '.view.error.not-supported'; + case WEBAUTHN_ERROR_TYPE.FAILED: + return operation === WEBAUTHN_OPERATION.REGISTRATION ? '.view.error.registration' : '.view.error.authentication'; + } +} + +function getWebAuthnErrorFallback(type: WEBAUTHN_ERROR_TYPE, operation: WEBAUTHN_OPERATION): string { + switch (type) { + case WEBAUTHN_ERROR_TYPE.CANCEL_OR_TIMEOUT: + return WEBAUTHN_ERROR_MESSAGES.cancelOrTimeout; + case WEBAUTHN_ERROR_TYPE.NOT_SUPPORTED: + return WEBAUTHN_ERROR_MESSAGES.notSupported; + case WEBAUTHN_ERROR_TYPE.FAILED: + return operation === WEBAUTHN_OPERATION.REGISTRATION + ? WEBAUTHN_ERROR_MESSAGES.registration + : WEBAUTHN_ERROR_MESSAGES.authentication; + } +} + function getWebAuthnErrorMessage( type: WEBAUTHN_ERROR_TYPE, operation: WEBAUTHN_OPERATION, currentStep: HaapiStep | null ): string { - // `currentStep` is kept on the signature for forward-compat with BE-supplied viewData copy; - // until those keys land, every bucket resolves to a hardcoded string. - void currentStep; - return type === WEBAUTHN_ERROR_TYPE.CANCEL_OR_TIMEOUT - ? WEBAUTHN_ERROR_MESSAGES.cancelOrTimeout - : operation === WEBAUTHN_OPERATION.REGISTRATION - ? WEBAUTHN_ERROR_MESSAGES.registration - : WEBAUTHN_ERROR_MESSAGES.authentication; + const suffix = getWebAuthnErrorViewDataSuffix(type, operation); + const messages = currentStep?.metadata?.viewData?.messages ?? {}; + const localizedMessage = Object.entries(messages).find(([key]) => key.endsWith(suffix))?.[1]; + + return localizedMessage?.length ? localizedMessage : getWebAuthnErrorFallback(type, operation); } function getWebAuthnErrorType(error: unknown): WEBAUTHN_ERROR_TYPE { diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/useIsClientOperationAvailable.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/useIsClientOperationAvailable.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/useIsClientOperationAvailable.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/useIsClientOperationAvailable.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormContext.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormContext.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormContext.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormContext.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormHook.spec.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormHook.spec.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormHook.spec.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormHook.spec.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormHook.ts b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormHook.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormHook.ts rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormHook.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton.spec.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton.spec.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton.spec.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton.tsx similarity index 96% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton.tsx index a64557a7..50f9dcff 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton.tsx +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton.tsx @@ -12,7 +12,7 @@ import type { ComponentPropsWithRef, ReactElement, ReactNode } from 'react'; import { HAAPI_FORM_ACTION_KINDS } from '../../../data-access/types/haapi-action.types'; -import { resolveAuthenticatorIcon } from '../../../../../shared/ui/icons/authenticator-icons'; +import { resolveAuthenticatorIcon } from '../../../ui/icons/authenticator-icons'; import { useHaapiStepperForm } from './HaapiStepperFormContext'; interface HaapiStepperFormSubmitButtonProps extends ComponentPropsWithRef<'button'> { diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI.spec.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI.spec.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI.spec.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormValidationErrorInputWrapper.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormValidationErrorInputWrapper.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormValidationErrorInputWrapper.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormValidationErrorInputWrapper.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperCheckboxFormFieldUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperCheckboxFormFieldUI.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperCheckboxFormFieldUI.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperCheckboxFormFieldUI.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperFormFieldUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperFormFieldUI.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperFormFieldUI.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperFormFieldUI.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperPasswordFormFieldUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperPasswordFormFieldUI.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperPasswordFormFieldUI.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperPasswordFormFieldUI.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperSelectFormFieldUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperSelectFormFieldUI.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperSelectFormFieldUI.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperSelectFormFieldUI.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperTextFormFieldUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperTextFormFieldUI.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperTextFormFieldUI.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperTextFormFieldUI.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/defaultHaapiStepperFormFieldElementFactory.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/defaultHaapiStepperFormFieldElementFactory.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/defaultHaapiStepperFormFieldElementFactory.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/defaultHaapiStepperFormFieldElementFactory.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/selector/HaapiStepperSelectorUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/actions/selector/HaapiStepperSelectorUI.tsx similarity index 90% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/selector/HaapiStepperSelectorUI.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/actions/selector/HaapiStepperSelectorUI.tsx index 4ed826dd..474b060a 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/actions/selector/HaapiStepperSelectorUI.tsx +++ b/src/haapi-react-sdk/haapi-stepper/feature/actions/selector/HaapiStepperSelectorUI.tsx @@ -51,11 +51,11 @@ export function HaapiStepperSelectorUI({ action, onSubmit }: HaapiStepperSelecto return (
{action.title &&

{action.title}

} - {options.map(option => { - if (option.subtype === HAAPI_ACTION_TYPES.FORM) { - return ; - } - })} + {options.map(option => + option.subtype === HAAPI_ACTION_TYPES.FORM ? ( + + ) : null + )}
); } diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/index.ts b/src/haapi-react-sdk/haapi-stepper/feature/index.ts similarity index 94% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/index.ts rename to src/haapi-react-sdk/haapi-stepper/feature/index.ts index d657fd5f..724e8d49 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/index.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/index.ts @@ -20,6 +20,7 @@ export * from './stepper/step-handlers/polling-step'; export * from './stepper/data-formatters/problem-step'; export * from './steps/HaapiStepperStepUI'; +export * from './viewnames/HaapiStepperBankIdPollingProgressUI'; export * from './actions/form/HaapiStepperFormUI'; export * from './actions/form/HaapiStepperFormValidationErrorInputWrapper'; export * from './actions/form/HaapiStepperFormHook'; diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx similarity index 86% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx index 11291fe2..d413931b 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx +++ b/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx @@ -17,20 +17,22 @@ import { HAAPI_FORM_ACTION_KINDS, } from '../../data-access/types/haapi-action.types'; import { HTTP_METHODS } from '../../data-access/types/haapi-form.types'; -import { MEDIA_TYPES } from '../../../../shared/util/types/media.types'; +import { MEDIA_TYPES } from '../../data-access/types/media.types'; import { authenticationStep, completedWithErrorStep, - completedWithErrorStepWithoutLinks, + completedWithErrorStepFormPost, + completedWithErrorStepWithoutLinksOrActions, completedWithSuccessStep, - completedWithSuccessStepWithoutLinks, + completedWithSuccessStepFormPost, + completedWithSuccessStepWithoutLinksOrActions, continueSameStep, createProblemStep, createRegistrationStep, pollingBankIdStep, pollingPendingStep, redirectionStep, -} from '../../../../shared/util/api-responses'; +} from '../../util/tests/api-responses'; import { act } from 'react'; import { useHaapiStepper } from './HaapiStepperHook'; import type { @@ -40,8 +42,9 @@ import type { HaapiStepperNextStepAction, } from './haapi-stepper.types'; import { HaapiStepperActionStep, HaapiStepperFormAction } from './haapi-stepper.types'; -import { isQrCodeLink } from '../../util/isQrCodeLink'; +import { isQrCodeLink } from '../../util/link-predicates'; import { + createMockFormAction, createMockWebAuthnAnyDeviceBothOptionsAction, createMockWebAuthnAuthenticationAction, createMockWebAuthnCrossPlatformOnlyAnyDeviceAction, @@ -83,6 +86,7 @@ describe('HaapiStepper', () => { const standaloneBootstrapConfig: HaapiStepperBootstrapConfig = { initialUrl: 'https://standalone.example/start', haapi: {} as HaapiStepperBootstrapConfig['haapi'], + theme: {}, }; render( @@ -101,6 +105,7 @@ describe('HaapiStepper', () => { const standaloneBootstrapConfig: HaapiStepperBootstrapConfig = { initialUrl: 'https://standalone.example/start', haapi: {} as HaapiStepperBootstrapConfig['haapi'], + theme: {}, }; render( @@ -898,71 +903,135 @@ describe('HaapiStepper', () => { }); }); - describe.each([ - { - label: 'success', - stepType: HAAPI_STEPS.COMPLETED_WITH_SUCCESS, - stepFixture: completedWithSuccessStep, - }, - { - label: 'error', - stepType: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR, - stepFixture: completedWithErrorStep, - }, - ] as const)('Completed With $label Step', ({ label, stepType, stepFixture }) => { - const authorizationResponseUrl = stepFixture.links?.find(link => link.rel === 'authorization-response')?.href; - - describe('autoRedirectOnAuthenticationComplete enabled (default)', () => { - it('should redirect to the authorization-response URL', async () => { - render( - - - - ); + describe('Completed Step', () => { + describe.each([ + { + label: 'success - redirect', + stepType: HAAPI_STEPS.COMPLETED_WITH_SUCCESS, + }, + { + label: 'error - redirect', + stepType: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR, + }, + { + label: 'success - Form POST', + formPostResponseMode: true, + stepType: HAAPI_STEPS.COMPLETED_WITH_SUCCESS, + }, + { + label: 'error - Form POST', + formPostResponseMode: true, + stepType: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR, + }, + ])('Common - $label', ({ label, stepType, formPostResponseMode }) => { + describe('autoRedirectOnAuthenticationComplete enabled (default)', () => { + it('should throw error to the error boundary when no authorization-response link or action exists', async () => { + render( + + + + ); - await screen.findByTestId('step-type'); - await goToNextStep(stepType); + await screen.findByTestId('step-type'); + await goToNextStep(stepType, { noLinksOrActions: true, formPostResponseMode }); - await waitFor(() => { - expect(window.location.href).toBe(authorizationResponseUrl); + await waitFor(() => { + expect(mockThrowErrorToAppErrorBoundary).toHaveBeenCalledWith( + `autoRedirectOnAuthenticationComplete is enabled, but the '${stepType}' step did not include an authorization-response link or action.` + ); + }); }); - }); - it('should throw error to the error boundary when no authorization-response link exists', async () => { - render( - - - - ); + it('should throw error to the error boundary when links or actions exist but none have rel/kind "authorization-response"', async () => { + render( + + + + ); - await screen.findByTestId('step-type'); - await goToNextStep(stepType, { noLinks: true }); + await screen.findByTestId('step-type'); + await goToNextStep(stepType, { formPostResponseMode, withoutAuthorizationResponseLinkOrAction: true }); - await waitFor(() => { - expect(mockThrowErrorToAppErrorBoundary).toHaveBeenCalledWith( - `autoRedirectOnAuthenticationComplete is enabled, but the completed-with-${label} step did not include an authorization-response link.` - ); + await waitFor(() => { + expect(mockThrowErrorToAppErrorBoundary).toHaveBeenCalledWith( + `autoRedirectOnAuthenticationComplete is enabled, but the '${stepType}' step did not include an authorization-response link or action.` + ); + }); }); }); - it('should throw error to the error boundary when links exist but none have rel "authorization-response"', async () => { - render( - - - - ); + describe('autoRedirectOnAuthenticationComplete disabled', () => { + it('should render the completed step instead of redirecting', async () => { + const initialWindowLocation = window.location.href; + render( + + + + ); - await screen.findByTestId('step-type'); - await goToNextStep(stepType, { linksWithoutAuthorizationResponse: true }); + await screen.findByTestId('step-type'); + await goToNextStep(stepType, { formPostResponseMode }); - await waitFor(() => { - expect(mockThrowErrorToAppErrorBoundary).toHaveBeenCalledWith( - `autoRedirectOnAuthenticationComplete is enabled, but the completed-with-${label} step did not include an authorization-response link.` + await waitFor(() => { + expect(screen.getByTestId('step-type')).toHaveTextContent(stepType); + }); + + expect(window.location.href).toBe(initialWindowLocation); + }); + + it('should add the completed step to history with the full OAuth payload accessible to consumers', async () => { + render( + + + ); + + await screen.findByTestId('step-type'); + await goToNextStep(stepType, { formPostResponseMode }); + + await waitFor(() => { + expect(screen.getByTestId('step-type')).toHaveTextContent(stepType); + }); + + const historyData = getHistoryData(screen.getByTestId('history')); + const completedHistoryEntry = historyData[ + historyData.length - 1 + ] as HaapiStepperHistoryEntry; + + expect(completedHistoryEntry.step.type).toBe(stepType); + + if (label.startsWith('success')) { + // @ts-expect-error - narrowing the history step union for test access + expect(completedHistoryEntry.step.properties).toMatchObject({ + code: 'ziQUB25BIR9xbMLnCK0vetFEsVfYsrl8', + iss: 'https://localhost:8443/dev/oauth/anonymous', + state: 'foo', + }); + } else { + // @ts-expect-error - narrowing the history step union for test access + expect(completedHistoryEntry.step.error).toBe('server_error'); + // @ts-expect-error - narrowing the history step union for test access + expect(completedHistoryEntry.step.error_description).toBe('An error occurred during authorization'); + } }); }); + }); + + describe.each([ + { + label: 'success', + stepType: HAAPI_STEPS.COMPLETED_WITH_SUCCESS, + stepFixture: completedWithSuccessStep, + }, + { + label: 'error', + stepType: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR, + stepFixture: completedWithErrorStep, + }, + ] as const)('Redirect/GET - $label', ({ stepType, stepFixture }) => { + const authorizationResponseUrl = stepFixture.links?.find(link => link.rel === 'authorization-response')?.href; - it('should not update the current step when redirecting', async () => { + it('should redirect to the authorization-response URL', async () => { render( @@ -976,62 +1045,63 @@ describe('HaapiStepper', () => { expect(window.location.href).toBe(authorizationResponseUrl); }); + // current step is not updated expect(screen.getByTestId('step-type')).toHaveTextContent(initialStepType); }); }); - describe('autoRedirectOnAuthenticationComplete disabled', () => { - it('should render the completed step instead of redirecting', async () => { - render( - - - - ); + describe.each([ + { + label: 'success', + stepType: HAAPI_STEPS.COMPLETED_WITH_SUCCESS, + stepFixture: completedWithSuccessStepFormPost, + }, + { + label: 'error', + stepType: HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR, + stepFixture: completedWithErrorStepFormPost, + }, + ] as const)('Form POST - $label', ({ stepType, stepFixture }) => { + const formModel = stepFixture.actions[0].model; - await screen.findByTestId('step-type'); - await goToNextStep(stepType); - - await waitFor(() => { - expect(screen.getByTestId('step-type')).toHaveTextContent(stepType); - }); - - expect(window.location.href).not.toBe(authorizationResponseUrl); + afterEach(() => { + // The handler appends the auto-submit form directly to document.body (outside the + // React render container), so Testing Library's cleanup does not remove it. + document.body.querySelectorAll('form').forEach(form => form.remove()); }); - it('should add the completed step to history with the full OAuth payload accessible to consumers', async () => { + it('POSTs a form to the authorization-response URL', async () => { + const submitSpy = vi.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation(() => undefined); + render( - + ); await screen.findByTestId('step-type'); - await goToNextStep(stepType); + await goToNextStep(stepType, { formPostResponseMode: true }); await waitFor(() => { - expect(screen.getByTestId('step-type')).toHaveTextContent(stepType); + expect(submitSpy).toHaveBeenCalledTimes(1); }); - const historyData = getHistoryData(screen.getByTestId('history')); - const completedHistoryEntry = historyData[ - historyData.length - 1 - ] as HaapiStepperHistoryEntry; - - expect(completedHistoryEntry.step.type).toBe(stepType); + // current step is not updated + expect(screen.getByTestId('step-type')).toHaveTextContent(initialStepType); - if (label === 'success') { - // @ts-expect-error - narrowing the history step union for test access - expect(completedHistoryEntry.step.properties).toMatchObject({ - code: 'ziQUB25BIR9xbMLnCK0vetFEsVfYsrl8', - iss: 'https://localhost:8443/dev/oauth/anonymous', - state: 'foo', - }); - } else { - // @ts-expect-error - narrowing the history step union for test access - expect(completedHistoryEntry.step.error).toBe('server_error'); - // @ts-expect-error - narrowing the history step union for test access - expect(completedHistoryEntry.step.error_description).toBe('An error occurred during authorization'); - } + const form = document.body.querySelector('form'); + expect(form).not.toBeNull(); + expect(form!.getAttribute('action')).toBe(formModel.href); + expect(form!.method.toUpperCase()).toBe(formModel.method); + + const inputs = Array.from(form!.querySelectorAll('input')); + expect(inputs).toHaveLength(formModel.fields.length); + inputs.forEach((input, index) => { + const field = formModel.fields[index]; + expect(input.type).toBe('hidden'); + expect(input.name).toBe(field.name); + expect(input.value).toBe(field.value); + }); }); }); }); @@ -1473,21 +1543,33 @@ function getStepMock(stepType: HAAPI_STEPS | HAAPI_PROBLEM_STEPS, config?: Recor } break; case HAAPI_STEPS.COMPLETED_WITH_SUCCESS: - if (config?.noLinks) { - stepMock = completedWithSuccessStepWithoutLinks; - } else if (config?.linksWithoutAuthorizationResponse) { - stepMock = { ...completedWithSuccessStep, links: [{ href: '/dev/cancel', rel: 'cancel' }] }; + if (config?.noLinksOrActions) { + stepMock = completedWithSuccessStepWithoutLinksOrActions; + } else if (config?.formPostResponseMode) { + stepMock = completedWithSuccessStepFormPost; + if (config.withoutAuthorizationResponseLinkOrAction) { + stepMock = { ...stepMock, actions: [createMockFormAction()] }; + } } else { stepMock = completedWithSuccessStep; + if (config?.withoutAuthorizationResponseLinkOrAction) { + stepMock = { ...stepMock, links: [{ href: '/dev/cancel', rel: 'cancel' }] }; + } } break; case HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR: - if (config?.noLinks) { - stepMock = completedWithErrorStepWithoutLinks; - } else if (config?.linksWithoutAuthorizationResponse) { - stepMock = { ...completedWithErrorStep, links: [{ href: '/dev/cancel', rel: 'cancel' }] }; + if (config?.noLinksOrActions) { + stepMock = completedWithErrorStepWithoutLinksOrActions; + } else if (config?.formPostResponseMode) { + stepMock = completedWithErrorStepFormPost; + if (config.withoutAuthorizationResponseLinkOrAction) { + stepMock = { ...stepMock, actions: [createMockFormAction()] }; + } } else { stepMock = completedWithErrorStep; + if (config?.withoutAuthorizationResponseLinkOrAction) { + stepMock = { ...stepMock, links: [{ href: '/dev/cancel', rel: 'cancel' }] }; + } } break; case HAAPI_STEPS.REGISTRATION: diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.tsx b/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.tsx similarity index 98% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.tsx index 1dcfbe02..bcde029e 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.tsx +++ b/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper.tsx @@ -358,8 +358,8 @@ export function HaapiStepper({ children, config }: HaapiStepperProps) { }, [nextStep, configResult.bootstrap.initialUrl]); const contextValue = useMemo( - () => ({ currentStep, loading, error, nextStep, history }), - [currentStep, loading, error, nextStep, history] + () => ({ currentStep, loading, error, nextStep, history, config: configResult }), + [currentStep, loading, error, nextStep, history, configResult] ); return {children}; @@ -383,7 +383,7 @@ interface ProcessHaapiNextStepParams { } async function processHaapiNextStep(params: ProcessHaapiNextStepParams): Promise<{ - nextStepData?: HaapiStepperStep; + nextStepData?: HaapiStepperStep | null; nextStepError?: HaapiStepperError; }> { const { currentStep, nextStep, history, action, payload, pendingOperation, config, sendHaapiFetchRequest } = params; @@ -399,6 +399,10 @@ async function processHaapiNextStep(params: ProcessHaapiNextStepParams): Promise return { nextStepError: clientOperationError }; } + if (clientOperationData.action === null) { + return { nextStepData: null }; + } + return processHaapiNextStep({ ...params, action: clientOperationData.action, diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperContext.tsx b/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperContext.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperContext.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperContext.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier.spec.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier.spec.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier.spec.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier.tsx b/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier.tsx similarity index 61% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier.tsx index fb74c0ee..23ed821e 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier.tsx +++ b/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier.tsx @@ -11,6 +11,7 @@ import { IconGeneralClose } from '@curity/ui-kit-icons'; import { ReactNode, useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; import { useHaapiStepper } from './HaapiStepperHook'; import { HaapiStepperAppError, HaapiStepperInputError } from './haapi-stepper.types'; @@ -51,35 +52,37 @@ export function HaapiStepperErrorNotifier({ return ( <> - {isNotificationVisible && ( -
-
-
-

- {errorFormatter(currentError)} -

+ {isNotificationVisible && + createPortal( +
+
+
+

+ {errorFormatter(currentError)} +

- -
- - {notificationMessages.length > 0 && ( -
- {notificationMessages.map(message => ( -

{message.text}

- ))} +
- )} -
-
- )} + + {notificationMessages.length > 0 && ( +
+ {notificationMessages.map(message => ( +

{message.text}

+ ))} +
+ )} +
+
, + document.body + )} {children} diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook.ts b/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook.ts rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/continue-same-step.ts b/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/continue-same-step.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/continue-same-step.ts rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/continue-same-step.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts b/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts similarity index 90% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts index 74770346..9a516c11 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts @@ -22,7 +22,6 @@ import { } from '../../../data-access/types'; import { HaapiStepperAction, - HaapiStepperClientOperationAction, HaapiStepperDataHelpers, HaapiStepperDataHelpersActionsMap, HaapiStepperLink, @@ -98,18 +97,6 @@ function addActionDataHelpers( }; } - if (step.type === HAAPI_STEPS.POLLING && actionWithDataHelpers.subtype === HAAPI_ACTION_TYPES.CLIENT_OPERATION) { - const clientOperationPollingAction = { - ...actionWithDataHelpers, - ...(step.properties.maxWaitTime != null && { maxWaitTime: step.properties.maxWaitTime }), - ...(step.properties.maxWaitRemainingTime != null && { - maxWaitRemainingTime: step.properties.maxWaitRemainingTime, - }), - }; - - return clientOperationPollingAction as HaapiStepperClientOperationAction; - } - return { ...action, ...actionWithDataHelpers }; } diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/problem-step.ts b/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/problem-step.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/problem-step.ts rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/data-formatters/problem-step.ts diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types.ts b/src/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types.ts similarity index 90% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types.ts rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types.ts index eba10ec2..36f6933e 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types.ts @@ -69,6 +69,7 @@ export interface HaapiStepperAPI { nextStep: HaapiStepperNextStep; // Complete history of all steps and actions taken history: HaapiStepperHistoryEntry[]; + config: HaapiStepperConfig; } /* @@ -85,6 +86,22 @@ export interface HaapiStepperConfig { export interface HaapiStepperBootstrapConfig { initialUrl: string; haapi: HaapiConfiguration; + theme: { + logo?: { + path: string; + isInsideWell: boolean; + }; + pageSymbols?: HaapiStepperStepSymbolsConfig; + }; +} + +export interface HaapiStepperStepSymbolsConfig { + /** Map of full HAAPI viewName -> symbol path. Highest precedence. */ + views?: Record; + /** Map of plugin implementation type (e.g. `html-form`, `bankid`) -> symbol path. */ + plugins?: Record; + /** Fallback symbol path used when no per-view / per-plugin entry matches. */ + default?: string; } declare global { @@ -138,12 +155,24 @@ export type HaapiStepperSelectorAction = Omit & }; }; export type HaapiStepperClientOperationAction = HaapiClientOperationAction & - HaapiStepperDataHelpersDetails & { - /** Polling session maximum time in seconds before the session expires. */ - maxWaitTime?: number; - /** Polling session remaining time in seconds before the session expires. */ - maxWaitRemainingTime?: number; - }; + HaapiStepperDataHelpersDetails; + +/** The registration attachment option a (split) any-device `webauthn-registration` action carries. */ +export interface HaapiStepperWebAuthnRegistrationAttachment { + /** The attachment the action carries — platform (built-in) or cross-platform (security key). */ + kind: WebAuthnRegistrationAttachmentKind; + /** The localized attachment option label, falling back to the action title. */ + title?: string; + /** The localized attachment option description. */ + description?: string; +} + +/** WebAuthn any-device registration attachment. */ +export enum WebAuthnRegistrationAttachmentKind { + PLATFORM = 'platform', + CROSS_PLATFORM = 'cross-platform', +} + export type HaapiStepperWebAuthnRegistrationClientOperationAction = HaapiStepperClientOperationAction & HaapiWebAuthnRegistrationClientOperationAction; export type HaapiStepperWebAuthnPasskeysRegistrationAction = HaapiStepperClientOperationAction & @@ -257,6 +286,7 @@ export interface HaapiStepperDataHelpers { links: HaapiStepperLink[]; }; } + export type HaapiStepperDataHelpersDetails< T, ST extends T extends HAAPI_STEPPER_ELEMENT_TYPES.ACTION diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/authentication-or-registration-step.ts b/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/authentication-or-registration-step.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/authentication-or-registration-step.ts rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/authentication-or-registration-step.ts diff --git a/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/completed-step.ts b/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/completed-step.ts new file mode 100644 index 00000000..d9216907 --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/completed-step.ts @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { type HaapiCompletedStep } from '../../../data-access/types/haapi-step.types'; +import { HAAPI_FORM_ACTION_KINDS, type HaapiFormAction } from '../../../data-access/types/haapi-action.types'; +import type { HaapiStepperConfig } from '../haapi-stepper.types'; +import { formatNextStepData } from '../data-formatters/format-next-step-data'; + +/** + * Handles a terminal step by redirecting back to the client application that initiated the flow (when configured to + * automatically do so). + * This is always done via a top-level navigation, meaning that the current application will be unloaded. That's why + * the window/document are modified directly, instead of rendering a component and using effects. + */ +export function handleCompletedStep(step: HaapiCompletedStep, config: HaapiStepperConfig) { + if (!config.autoRedirectOnAuthenticationComplete) { + return { nextStepData: formatNextStepData(step) }; + } + + if (tryHandleRedirectResponse(step) || tryHandleFormPostResponse(step)) { + return { nextStepData: undefined }; + } + + throw new Error( + `autoRedirectOnAuthenticationComplete is enabled, but the '${step.type}' step did not include an authorization-response link or action.` + ); +} + +/** + * Response modes that use redirect/GET. + */ +function tryHandleRedirectResponse(step: HaapiCompletedStep): boolean { + const responseLink = step.links?.find(link => link.rel === 'authorization-response'); + if (responseLink) { + window.location.href = responseLink.href; + return true; + } + + return false; +} + +/** + * Form POST response mode. + */ +function tryHandleFormPostResponse(step: HaapiCompletedStep): boolean { + const responseAction = step.actions?.find(action => action.kind === HAAPI_FORM_ACTION_KINDS.AUTHORIZATION_RESPONSE); + if (responseAction) { + const htmlForm = createHtmlForm(responseAction); + document.body.appendChild(htmlForm); + htmlForm.submit(); + return true; + } + + return false; +} + +function createHtmlForm(action: HaapiFormAction): HTMLFormElement { + const { href, method, fields } = action.model; + + const form = document.createElement('form'); + form.method = method; + form.action = href; + form.classList.add('hidden'); + + (fields ?? []).forEach(field => { + const input = document.createElement('input'); + input.type = field.type; + input.name = field.name; + input.value = field.value ?? ''; + form.appendChild(input); + }); + + return form; +} diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/polling-step.ts b/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/polling-step.ts similarity index 88% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/polling-step.ts rename to src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/polling-step.ts index 4afc1447..dc019842 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/polling-step.ts +++ b/src/haapi-react-sdk/haapi-stepper/feature/stepper/step-handlers/polling-step.ts @@ -10,18 +10,18 @@ */ import { RefObject } from 'react'; -import { HaapiPollingStep, HAAPI_POLLING_STATUS, HAAPI_STEPS } from '../../../data-access/types/haapi-step.types'; +import { HAAPI_POLLING_STATUS, HAAPI_STEPS, HaapiPollingStep } from '../../../data-access/types/haapi-step.types'; import type { HaapiStepperConfig, - HaapiStepperNextStep, HaapiStepperError, - HaapiStepperStep, HaapiStepperHistoryEntry, + HaapiStepperNextStep, + HaapiStepperStep, } from '../haapi-stepper.types'; +import { HaapiStepperPollingStep } from '../haapi-stepper.types'; import { formatNextStepData } from '../data-formatters/format-next-step-data'; import { HAAPI_FORM_ACTION_KINDS } from '../../../data-access/types/haapi-action.types'; import { isBankIdClientOperation, openBankIdApp } from '../../actions/client-operation/operations/bankid'; -import { HaapiStepperPollingStep } from '../haapi-stepper.types'; export function handlePollingStep( pollingStep: HaapiPollingStep, @@ -49,9 +49,21 @@ export function handlePollingStep( return { nextStepData: formattedPollingStep }; } + case HAAPI_POLLING_STATUS.FAILED: { + const nextAction = formattedPollingStep.actions.length === 1 && formattedPollingStep.dataHelpers.actions?.form[0]; + + const goToNextStep = + nextAction && + (nextAction.kind === HAAPI_FORM_ACTION_KINDS.REDIRECT || nextAction.kind === HAAPI_FORM_ACTION_KINDS.CONTINUE); + + if (goToNextStep) { + nextStep(nextAction); + } + return { nextStepData: formattedPollingStep }; } + case HAAPI_POLLING_STATUS.PENDING: { const pollingAction = formattedPollingStep.dataHelpers.actions?.formByKind.poll?.[0]; diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.spec.tsx similarity index 92% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.spec.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.spec.tsx index 6a84788a..04e08c7d 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.spec.tsx +++ b/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.spec.tsx @@ -39,7 +39,7 @@ import type { HaapiStepperVisibleFormField, } from '../stepper/haapi-stepper.types'; import { HAAPI_FORM_FIELDS } from '../../data-access/types/haapi-form.types'; -import { MEDIA_TYPES } from '../../../../shared/util/types/media.types'; +import { MEDIA_TYPES } from '../../data-access/types/media.types'; import { HaapiStepperActionsUI } from '../../ui/actions/HaapiStepperActionsUI'; import { HAAPI_FORM_ACTION_KINDS, @@ -58,6 +58,7 @@ import { createMockQrLink, createMockSelectorAction, createMockStep, + createUserConsentStep, defaultStepperAPI, MockActionTitle, MockLinkText, @@ -2017,7 +2018,157 @@ describe('HaapiStepperStepUI', () => { expect(screen.queryByTestId('messages')).toBeInTheDocument(); expect(screen.queryByTestId('links')).toBeInTheDocument(); }); + + it('should render the polling progress bar between the QR code and the actions', () => { + const step = createPollingStep({ + links: [createMockQrLink()], + actions: [createMockClientOperationAction({ title: 'Launch BankID App' })], + maxWaitTime: '60', + maxWaitRemainingTime: '30', + }); + + renderWithContext(, { currentStep: step }); + + const progress = screen.getByRole('progressbar', { hidden: true }); + expect(progress).toHaveAttribute('value', '30'); + expect(progress).toHaveAttribute('max', '60'); + + const qrCode = screen.getByTestId('qr-code-button'); + const actions = screen.getByTestId('client-operation-action'); + expect(follows(progress, qrCode)).toBe(true); + expect(follows(actions, progress)).toBe(true); + }); + + it('should render the progress bar sourced from the step even without a client-operation (QR-only mode)', () => { + const step = createPollingStep({ + links: [createMockQrLink()], + actions: [], + maxWaitTime: '60', + maxWaitRemainingTime: '30', + }); + + renderWithContext(, { currentStep: step }); + + expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '30'); + expect(screen.queryByTestId('client-operation-action')).not.toBeInTheDocument(); + }); + + it('should not render the progress bar without a QR code (client-operation-only mode)', () => { + const step = createPollingStep({ + links: [], + actions: [createMockClientOperationAction({ title: 'Launch BankID App' })], + maxWaitTime: '60', + maxWaitRemainingTime: '30', + }); + + renderWithContext(, { currentStep: step }); + + expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument(); + expect(screen.queryByTestId('qr-code-button')).not.toBeInTheDocument(); + }); + + it('should not render a progress bar when the step exposes no remaining wait time', () => { + const step = createPollingStep({ links: [createMockQrLink()] }); + + renderWithContext(, { currentStep: step }); + + expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument(); + }); + + it.each([HAAPI_POLLING_STATUS.DONE, HAAPI_POLLING_STATUS.FAILED])( + 'should not render a progress bar for a %s polling step', + status => { + const step = createPollingStep({ + status, + links: [createMockQrLink()], + maxWaitTime: '60', + maxWaitRemainingTime: '30', + }); + + renderWithContext(, { currentStep: step }); + + expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument(); + } + ); + }); + + describe('User Consent viewName built-in UI', () => { + const companyLogoPath = '/assets/images/company-logo.svg'; + const clientLogoPath = '/assets/images/client-logo.svg'; + + // Context override adding (or omitting) the company logo in the theme configuration + const withCompanyLogo = (path?: string): Partial => ({ + config: { + ...defaultStepperAPI.config, + bootstrap: { + ...defaultStepperAPI.config.bootstrap, + theme: path ? { logo: { path, isInsideWell: false } } : {}, + }, + }, + }); + + it('should render logos section and remainder items when logos are available', () => { + const step = createUserConsentStep({ clientLogo: clientLogoPath }); + + renderWithContext(, { + currentStep: step, + ...withCompanyLogo(companyLogoPath), + }); + + const [companyLogo, clientLogo] = screen.getAllByRole('img'); + expect(companyLogo).toHaveAttribute('src', companyLogoPath); + expect(clientLogo).toHaveAttribute('src', clientLogoPath); + expect(follows(clientLogo, companyLogo)).toBe(true); + + const logos = screen.getByTestId('consent-logos'); + expect(logos).toContainElement(companyLogo); + expect(logos).toContainElement(clientLogo); + expect(companyLogo).toHaveClass('haapi-stepper-consent-logo-image'); + expect(companyLogo).not.toHaveClass('haapi-stepper-consent-logo-image-client'); + expect(clientLogo).toHaveClass('haapi-stepper-consent-logo-image-client'); + + // The checkmark joining the two logos is decorative. + const checkmark = logos.querySelector('.haapi-stepper-consent-logos-checkmark svg'); + expect(checkmark).toHaveAttribute('aria-hidden', 'true'); + + const messages = screen.getByTestId('messages'); + const action = screen.getByTestId('form-action'); + const links = screen.getByTestId('links'); + expect(follows(messages, clientLogo)).toBe(true); + expect(follows(action, messages)).toBe(true); + expect(follows(links, action)).toBe(true); + }); + + it('should not render the logos section when only the company logo is available', () => { + const step = createUserConsentStep(); + + renderWithContext(, { + currentStep: step, + ...withCompanyLogo(companyLogoPath), + }); + + expect(screen.queryByTestId('consent-logos')).not.toBeInTheDocument(); + expect(screen.queryAllByRole('img')).toHaveLength(0); + expect(screen.queryByTestId('messages')).toBeInTheDocument(); + }); + + it('should not render the logos when only the client logo is available', () => { + const step = createUserConsentStep({ clientLogo: clientLogoPath }); + + renderWithContext(, { + currentStep: step, + ...withCompanyLogo(), + }); + + expect(screen.queryByTestId('consent-logos')).not.toBeInTheDocument(); + expect(screen.queryAllByRole('img')).toHaveLength(0); + expect(screen.queryByTestId('messages')).toBeInTheDocument(); + }); }); }); }); }); + +/** True when `node` appears after `reference` in document order. */ +const follows = (node: Element, reference: Element) => + Boolean(reference.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING); diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.tsx similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI.tsx diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/steps/step-element-factories.tsx b/src/haapi-react-sdk/haapi-stepper/feature/steps/step-element-factories.tsx similarity index 98% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/steps/step-element-factories.tsx rename to src/haapi-react-sdk/haapi-stepper/feature/steps/step-element-factories.tsx index c8da24bf..3d39aa5f 100644 --- a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/steps/step-element-factories.tsx +++ b/src/haapi-react-sdk/haapi-stepper/feature/steps/step-element-factories.tsx @@ -34,7 +34,7 @@ import type { HaapiStepperStepUISelectorActionRenderInterceptor, HaapiStepperUserMessage, } from '../stepper/haapi-stepper.types'; -import { Spinner } from '../../../../shared/ui/spinner/Spinner'; +import { Spinner } from '../../ui/spinner/Spinner'; export const getLoadingElement = ( haapiStepperAPI: HaapiStepperAPI, diff --git a/src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/steps/typings.ts b/src/haapi-react-sdk/haapi-stepper/feature/steps/typings.ts similarity index 100% rename from src/haapi-react-app/src/haapi-react-sdk/haapi-stepper/feature/steps/typings.ts rename to src/haapi-react-sdk/haapi-stepper/feature/steps/typings.ts diff --git a/src/haapi-react-sdk/haapi-stepper/feature/viewnames/BankIdViewNameBuiltInUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/BankIdViewNameBuiltInUI.tsx new file mode 100644 index 00000000..2afcc5f6 --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/BankIdViewNameBuiltInUI.tsx @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { HAAPI_STEPS } from '../../data-access/types/haapi-step.types'; +import { isQrCodeLink } from '../../util/link-predicates'; +import { getLinksElement } from '../steps/step-element-factories'; +import { HaapiStepperBankIdPollingProgressUI } from './HaapiStepperBankIdPollingProgressUI'; +import { HaapiStepperBankIdQrCodeAccessibilityMessages } from './HaapiStepperBankIdQrCodeAccessibilityMessages'; +import type { ViewNameBuiltInUIProps } from './typings'; + +/** + * Built-in UI for the BankID viewName (`HaapiStepperViewNameBuiltInUI.BANKID`). + * + * - Lifts the QR code link above the actions so it's the primary element on the screen. + * - Renders the polling "authentication time" progress bar under the QR code, only when a QR code is + * present. + * - Renders the QR-code accessibility messages (`metadata.viewData.messages`) as collapsible + * sections below the QR code. + */ +export const BankIdViewNameBuiltInUI = (props: ViewNameBuiltInUIProps) => { + const { currentStep, linkRenderInterceptor, loadingElement, errorElement, messagesElement, actionsElement } = props; + const { links } = currentStep.dataHelpers; + const qrCodeLink = links.find(isQrCodeLink); + const nonQrCodeLinks = links.filter(link => !isQrCodeLink(link)); + + return ( + <> + {loadingElement} + {errorElement} + {messagesElement} + {qrCodeLink && ( + <> + {getLinksElement(props, [qrCodeLink], linkRenderInterceptor)} + {currentStep.type === HAAPI_STEPS.POLLING && ( + + )} + + + )} + {actionsElement} + {nonQrCodeLinks.length > 0 && getLinksElement(props, nonQrCodeLinks, linkRenderInterceptor)} + + ); +}; diff --git a/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.spec.tsx b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.spec.tsx new file mode 100644 index 00000000..085f45ee --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.spec.tsx @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { act, render, screen } from '@testing-library/react'; + +import { HaapiStepperBankIdPollingProgressUI } from './HaapiStepperBankIdPollingProgressUI'; +import { createPollingStep } from '../../util/tests/mocks'; +import { HAAPI_POLLING_STATUS } from '../../data-access/types/haapi-step.types'; + +const QR_MINUTES_LEFT_KEY = 'authenticator.bankid.launch.view.qr.minutes-left'; +const QR_SECONDS_LEFT_KEY = 'authenticator.bankid.launch.view.qr.seconds-left'; + +describe('HaapiStepperBankIdPollingProgressUI', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.clearAllTimers(); + vi.useRealTimers(); + }); + + const tick = async (ms: number) => { + await act(async () => { + vi.advanceTimersByTime(ms); + await Promise.resolve(); + }); + }; + + it('fills the bar with the elapsed portion of the session', () => { + render( + + ); + + const progress = screen.getByRole('progressbar', { hidden: true }); + expect(progress).toHaveAttribute('value', '40'); + expect(progress).toHaveAttribute('max', '60'); + }); + + it('fills the bar one second at a time, clamped at max', async () => { + render( + + ); + + expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '57'); + + await tick(1000); + expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '58'); + + await tick(5000); + expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '60'); + }); + + it('progresses on the local countdown and ignores the server value on later polls', async () => { + const { rerender } = render( + + ); + + await tick(3000); + expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '33'); + + rerender( + + ); + expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '33'); + + rerender( + + ); + expect(screen.getByRole('progressbar', { hidden: true })).toHaveAttribute('value', '33'); + }); + + // "1:00" at exactly 60s matches the Velocity reference (curity-ui.js _qrTimer). + it.each([ + ['90', '1:30 minutes left'], + ['65', '1:05 minutes left'], + ['60', '1:00 minutes left'], + ['600', '10:00 minutes left'], + ['59', '59 seconds left'], + ['24', '24 seconds left'], + ['0', '0 seconds left'], + ])('formats %s seconds remaining as "%s"', (maxWaitRemainingTime, expected) => { + render( + + ); + + expect(screen.getByTestId('polling-progress-duration')).toHaveTextContent(expected); + }); + + it('renders only the bar when the step has no unit labels', () => { + render( + + ); + + expect(screen.getByRole('progressbar', { hidden: true })).toBeInTheDocument(); + expect(screen.queryByTestId('polling-progress-duration')).not.toBeInTheDocument(); + }); + + it('renders the readout but no bar when the step has no max wait time', () => { + render( + + ); + + expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument(); + expect(screen.getByTestId('polling-progress-duration')).toHaveTextContent('20 seconds left'); + }); + + it('renders nothing when the polling step has no remaining wait time', () => { + const { container } = render( + + ); + + expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument(); + expect(container).toBeEmptyDOMElement(); + }); + + it.each([HAAPI_POLLING_STATUS.DONE, HAAPI_POLLING_STATUS.FAILED])('renders nothing for a %s polling step', status => { + const { container } = render( + + ); + + expect(screen.queryByRole('progressbar', { hidden: true })).not.toBeInTheDocument(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.tsx b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.tsx new file mode 100644 index 00000000..cc5ca203 --- /dev/null +++ b/src/haapi-react-sdk/haapi-stepper/feature/viewnames/HaapiStepperBankIdPollingProgressUI.tsx @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { useEffect, useState } from 'react'; +import { HAAPI_POLLING_STATUS } from '../../data-access/types/haapi-step.types'; +import type { HaapiStepperPollingStep } from '../stepper/haapi-stepper.types'; + +interface HaapiStepperBankIdPollingProgressUIProps { + /** The BankID polling step whose session time this bar reflects. */ + currentStep: HaapiStepperPollingStep; +} + +// BankID emits the countdown unit labels under this namespace in `metadata.viewData.messages` +// (see `WaitBankIdRepresentationFunction` in the server) — the same namespace as the QR accessibility copy. +const qrMessageKey = (suffix: string) => `authenticator.bankid.launch.view.qr.${suffix}`; + +const toSeconds = (value?: number | string): number | undefined => { + if (value === undefined) { + return undefined; + } + + const seconds = Number(value); + + return Number.isFinite(seconds) ? seconds : undefined; +}; + +/** + * @description + * # BANKID POLLING PROGRESS COMPONENT + * + * Renders the BankID "authentication time" progress bar for a *pending* polling step. Done and failed + * steps render nothing — they show their own outcome UI. + * + * The remaining time is seeded from the step's `maxWaitRemainingTime` once and counted down locally, + * one second at a time, clamped at 0. It is not re-synced on each poll: the server's remaining time + * can only arrive later/higher than the local count (poll cadence + latency), so re-syncing would only + * ever jerk the bar backwards. The bar shows elapsed time (so it fills toward timeout, matching the + * Velocity reference) while the readout counts remaining time down; the server decides when the session + * actually ends by returning a failed polling step. + * + * Accessibility: the bar is decorative (`aria-hidden`) — the adjacent numeric readout carries the + * remaining-time text for assistive tech, and only renders when the localized unit label is present in + * the step's `metadata.viewData.messages` (`minutes-left` for a minute or more left, `seconds-left` + * otherwise). + */ +export function HaapiStepperBankIdPollingProgressUI({ currentStep }: HaapiStepperBankIdPollingProgressUIProps) { + const { maxWaitTime, maxWaitRemainingTime, status } = currentStep.properties; + const viewDataMessages = currentStep.metadata?.viewData?.messages; + const minutesLeftLabel = viewDataMessages?.[qrMessageKey('minutes-left')]; + const secondsLeftLabel = viewDataMessages?.[qrMessageKey('seconds-left')]; + + const [remaining, setRemaining] = useState(() => toSeconds(maxWaitRemainingTime)); + + const shouldCountDown = status === HAAPI_POLLING_STATUS.PENDING && remaining !== undefined && remaining > 0; + + useEffect(() => { + if (!shouldCountDown) { + return; + } + + const intervalId = setInterval(() => { + setRemaining(previous => (previous === undefined ? previous : Math.max(0, previous - 1))); + }, 1000); + + return () => clearInterval(intervalId); + }, [shouldCountDown]); + + if (remaining === undefined || status !== HAAPI_POLLING_STATUS.PENDING) { + return null; + } + + const maxValue = toSeconds(maxWaitTime); + const totalSeconds = Math.floor(remaining); + const showMinutes = totalSeconds >= 60; + const readoutLabel = showMinutes ? minutesLeftLabel : secondsLeftLabel; + const readoutValue = showMinutes + ? `${String(Math.floor(totalSeconds / 60))}:${String(totalSeconds % 60).padStart(2, '0')}` + : String(totalSeconds); + + return ( + <> + {maxValue !== undefined && ( +
-
+
diff --git a/src/identity-server/templates/core/fragments/api-driven-ui/assets.vm b/src/identity-server/templates/core/fragments/api-driven-ui/assets.vm new file mode 100644 index 00000000..bb8074fc --- /dev/null +++ b/src/identity-server/templates/core/fragments/api-driven-ui/assets.vm @@ -0,0 +1,13 @@ +#* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + *# + + + diff --git a/src/identity-server/templates/core/settings-defaults.vm b/src/identity-server/templates/core/settings-defaults.vm index ef7032c8..481d5b97 100644 --- a/src/identity-server/templates/core/settings-defaults.vm +++ b/src/identity-server/templates/core/settings-defaults.vm @@ -211,11 +211,20 @@ #set ($page_symbol = $page_symbol_authenticate_desktop) #* Other settings configurable *# + +#* + * Merge the page's img-src (set by authenticators that render inline data: images, + * e.g. the TOTP / BankID / Duo QR codes) with the img-src configured in "Look and Feel" + * (which adds the logo's origin). + *# #if ($_configured_cspImgsrc) - #set ($_cspImgsrc = $_configured_cspImgsrc) + #if ($_cspImgsrc) + #set ($_cspImgsrc = $CspUtil.combine($_configured_cspImgsrc, $_cspImgsrc)) + #else + #set ($_cspImgsrc = $_configured_cspImgsrc) + #end #end - #* This adds a cachebusting query to the CSS and JS files used by the kit Override this with custom busting if necessary or set to empty if cache busting should not be used diff --git a/src/identity-server/templates/core/views/api-driven-ui/index.vm b/src/identity-server/templates/core/views/api-driven-ui/index.vm index 5abb6431..39a0770d 100644 --- a/src/identity-server/templates/core/views/api-driven-ui/index.vm +++ b/src/identity-server/templates/core/views/api-driven-ui/index.vm @@ -20,9 +20,21 @@ #end #parse("fragments/csp") - - - + + #if ($!_apiDrivenApplicationName && $_apiDrivenApplicationName != "") + #message("meta.signin-to") $_tr.encodeHtml($_apiDrivenApplicationName) + #else + #message("meta.signin") + #end + + + #if ($_cacheBustingHash) + #set ($_cacheBustingQuery = "?v=" + $_cacheBustingHash) + #end + #parse("fragments/api-driven-ui/assets") + #if ($theme_css_path) + + #end #if ($_configured_ui_theme_css_path) #end diff --git a/src/self-service-portal/app/README.md b/src/self-service-portal/app/README.md index 4eb642a6..982190ac 100644 --- a/src/self-service-portal/app/README.md +++ b/src/self-service-portal/app/README.md @@ -56,6 +56,11 @@ application created in the Identity Server Admin UI under "Profiles -> {Applicat ## Build +If you plan to deploy the SSP as a [standalone application](#ssp-served-as-a-standalone-application), first copy +the `.env-example` to `.env` and update the [environment variables](#environment-file) to point to your Identity +Server instance and configure other settings as needed — the variables are baked into the bundle at build time. +When the SSP is [served under the Identity Server](#ssp-served-under-the-identity-server), no `.env` file is needed. + To build the Self-Service Portal application for production, run the following command: ```shell @@ -70,12 +75,16 @@ There are two options how to take the SSP application into use: ### SSP served under the Identity Server +In this scenario, no `.env` file is needed — the Identity Server injects the configuration into the HTML template at runtime. + 1. Copy the contents of the `dist/assets` directory to the Identity Server under the following path: `${IDSVR_HOME}/usr/share/webroot/assets/apps/self-service-portal/` -2. In case the html template needs to be customized, check the parent README.md for instructions. -3. Create a Self-Service Portal Application in the Identity Server. +2. Rename the copied entrypoint assets to match what the Identity Server html template references: + `index-*.js` → `index.js`, `index-*.css` → `index.css`, `favicon-*.png` → `favicon.png`. + Do not rename other hashed assets (eg chunks, fonts, images), since they are referenced by their hashed names. +3. In case the html template needs to be customized, check the parent README.md for instructions. +4. Create a Self-Service Portal Application in the Identity Server. Follow the instructions in the [Identity Server documentation](https://curity.io/docs/idsvr/latest/application-service-admin-guide/applications/self-service-portal.html) to set up the SSP. -4. Copy the `.env-example` to e.g. `.env` and update the [environment variables](#environment-file) to point to your Identity Server instance and configure other settings as needed. 5. The Identity Server will serve the SSP application under the path which can be found from the Admin UI. ### SSP served as a standalone application @@ -86,9 +95,9 @@ It's important to ensure that the Self-Service Portal and the Identity Server ar to allow proper cookie sharing for authentication and session management. [Definition of same site](https://developer.mozilla.org/en-US/docs/Glossary/Site) -1. Deploy the contents of the `dist` directory to your web server or hosting environment. -2. Create a Self-Service Portal Application in the Identity Server. -3. Set the web server URL to the Self-Service Portal Application. -4. Copy the `.env-example` to e.g. `.env` and update the [environment variables](#environment-file) to point to your Identity Server instance and configure other settings as needed. +1. Build the application with the [environment variables](#environment-file) configured, as described in the [Build](#build) section. +2. Deploy the contents of the `dist` directory to your web server or hosting environment. +3. Create a Self-Service Portal Application in the Identity Server. +4. Set the web server URL to the Self-Service Portal Application. 5. Ensure that the Self-Service Portal and the Identity Server are configured to be under the same site domain to allow proper cookie sharing for authentication and session management. It's convenient to use a reverse proxy in front of both applications to achieve this. diff --git a/src/self-service-portal/app/package.json b/src/self-service-portal/app/package.json index cfb6eab4..7e8e6bf3 100644 --- a/src/self-service-portal/app/package.json +++ b/src/self-service-portal/app/package.json @@ -35,14 +35,14 @@ "@curity/ui-kit-css": "^1.0.0", "@curity/ui-kit-icons": "^1.0.0", "graphql": "^16.11.0", - "i18next": "^25.6.0", + "i18next": "^26.2.0", "react": "^19.2.0", - "react-dom": "^19.2.4", - "react-error-boundary": "^6.0.0", + "react-dom": "^19.2.7", + "react-error-boundary": "^6.1.2", "react-hot-toast": "^2.5.2", - "react-i18next": "^16.1.5", + "react-i18next": "^17.0.8", "react-qr-code": "^2.0.15", - "react-router": "^7.13.1", + "react-router": "^7.18.0", "react-verification-input": "^4.2.2" }, "devDependencies": { @@ -54,18 +54,18 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.2", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-basic-ssl": "^2.1.0", "@vitejs/plugin-react": "^5.0.4", - "cypress": "^15.11.0", + "cypress": "^15.17.0", "eslint": "^9.16.0", "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-simple-import-sort": "^12.1.1", "globals": "^16.4.0", - "jsdom": "^27.0.1", + "jsdom": "^29.1.1", "msw": "^2.12.2", - "stylelint-config-standard": "^39.0.1", + "stylelint-config-standard": "^40.0.0", "typescript": "^5.7.2", "typescript-eslint": "^8.18.0", "vite": "^7.3.1", diff --git a/src/self-service-portal/app/src/error-handling/RouteErrorBoundary.tsx b/src/self-service-portal/app/src/error-handling/RouteErrorBoundary.tsx index c2570a77..9b69c1dd 100644 --- a/src/self-service-portal/app/src/error-handling/RouteErrorBoundary.tsx +++ b/src/self-service-portal/app/src/error-handling/RouteErrorBoundary.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useLocation } from 'react-router'; -import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; +import { getErrorMessage, ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { useTranslation } from 'react-i18next'; import { usePageTitle } from '@/shared/utils/useRouteTitle'; import { Alert, Button, Spinner } from '@curity/ui-kit-component-library'; @@ -20,8 +20,9 @@ const RouteErrorBoundaryFallback = ({ error, resetErrorBoundary }: FallbackProps const { t } = useTranslation(); const pageTitle = usePageTitle(); const [isResettingErrorBoundary, setIsResettingErrorBoundary] = useState(false); - const isAppBootstrappingError = error?.message?.includes('Error bootstrapping'); - const errorMessage = isAppBootstrappingError ? error?.message : `${t('Failed to load')} ${pageTitle} ${t('page')}`; + const boundaryMessage = getErrorMessage(error) + const isAppBootstrappingError = boundaryMessage && boundaryMessage.includes('Error bootstrapping'); + const errorMessage = isAppBootstrappingError ? boundaryMessage : `${t('Failed to load')} ${pageTitle} ${t('page')}`; useEffect(() => { let timer: ReturnType; diff --git a/src/self-service-portal/templates/apps/self-service-portal/index.vm b/src/self-service-portal/templates/apps/self-service-portal/index.vm index 8b012f20..3869006e 100644 --- a/src/self-service-portal/templates/apps/self-service-portal/index.vm +++ b/src/self-service-portal/templates/apps/self-service-portal/index.vm @@ -75,14 +75,19 @@ } } - - - +#if ($_cacheBustingHash) + #set ($_cacheBustingQuery = "?v=" + $_cacheBustingHash) +#end + + + #if ($theme_css_path) + + #end #if ($_configured_ui_theme_css_path) - + #end #if ($_configured_ui_custom_theme_css_path) - + #end