diff --git a/.github/actions/generate-github-token/action.yml b/.github/actions/generate-github-token/action.yml new file mode 100644 index 000000000..43536397d --- /dev/null +++ b/.github/actions/generate-github-token/action.yml @@ -0,0 +1,56 @@ +name: "Generate GitHub App Token" +description: "Generates a GitHub App token for accessing repositories in the selfxyz organization" + +inputs: + app-id: + description: "The GitHub App ID" + required: true + private-key: + description: "The GitHub App private key" + required: true + configure-netrc: + description: "If true, writes a ~/.netrc entry for github.com using the generated token (useful for CocoaPods / git HTTPS fetches)" + required: false + default: "false" + netrc-machine: + description: "The machine hostname to write into ~/.netrc (default: github.com)" + required: false + default: "github.com" + owner: + description: "The owner (organization) of the repositories" + required: false + default: "selfxyz" + repositories: + description: "Comma-separated list of repository names to grant access to" + required: false + default: "NFCPassportReader,android-passport-nfc-reader,react-native-passport-reader,mobile-sdk-native" + +outputs: + token: + description: "The generated GitHub App installation token" + value: ${{ steps.app-token.outputs.token }} + +runs: + using: "composite" + steps: + - name: Generate GitHub App Token + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 + id: app-token + with: + app-id: ${{ inputs.app-id }} + private-key: ${{ inputs.private-key }} + owner: ${{ inputs.owner }} + repositories: ${{ inputs.repositories }} + - name: Configure Git auth via ~/.netrc (optional) + if: ${{ inputs.configure-netrc == 'true' }} + shell: bash + run: | + set -euo pipefail + TOKEN="${{ steps.app-token.outputs.token }}" + MACHINE="${{ inputs.netrc-machine }}" + + # Mask the token in logs defensively (it shouldn't print, but this protects against future edits). + echo "::add-mask::${TOKEN}" + + printf "machine %s\n login x-access-token\n password %s\n" "${MACHINE}" "${TOKEN}" > "${HOME}/.netrc" + chmod 600 "${HOME}/.netrc" diff --git a/.github/workflows/mobile-bundle-analysis.yml b/.github/workflows/mobile-bundle-analysis.yml index efee6c5f4..d53fcfc1e 100644 --- a/.github/workflows/mobile-bundle-analysis.yml +++ b/.github/workflows/mobile-bundle-analysis.yml @@ -58,6 +58,14 @@ jobs: path: | ~/.gradle/caches ~/.gradle/wrapper + - name: Generate token for self repositories + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + configure-netrc: "true" - name: Install Mobile Dependencies uses: ./.github/actions/mobile-setup with: @@ -66,7 +74,7 @@ jobs: ruby_version: ${{ env.RUBY_VERSION }} workspace: ${{ env.WORKSPACE }} env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - name: Build dependencies shell: bash run: yarn workspace @selfxyz/common build @@ -114,6 +122,14 @@ jobs: with: path: app/ios/Pods lockfile: app/ios/Podfile.lock + - name: Generate token for self repositories + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + configure-netrc: "true" - name: Install Mobile Dependencies uses: ./.github/actions/mobile-setup with: @@ -122,7 +138,7 @@ jobs: ruby_version: ${{ env.RUBY_VERSION }} workspace: ${{ env.WORKSPACE }} env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - name: Build dependencies shell: bash run: yarn workspace @selfxyz/common build diff --git a/.github/workflows/mobile-ci.yml b/.github/workflows/mobile-ci.yml index 0f1fe3c19..924542201 100644 --- a/.github/workflows/mobile-ci.yml +++ b/.github/workflows/mobile-ci.yml @@ -267,6 +267,7 @@ jobs: - name: Cache Ruby gems uses: ./.github/actions/cache-bundler with: + # TODO(jcortejoso): Confirm the path of the bundle cache path: app/ios/vendor/bundle lock-file: app/Gemfile.lock cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }} @@ -314,6 +315,14 @@ jobs: bundle config set --local path 'vendor/bundle' bundle install --jobs 4 --retry 3 working-directory: ./app + - name: Generate token for self repositories + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + configure-netrc: "true" - name: Install iOS Dependencies uses: nick-fields/retry@v3 with: @@ -324,7 +333,7 @@ jobs: cd app/ios bundle exec bash scripts/pod-install-with-cache-fix.sh env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - name: Resolve iOS workspace run: | WORKSPACE_OPEN="ios/OpenPassport.xcworkspace" @@ -469,12 +478,19 @@ jobs: run: | echo "Cache miss for built dependencies. Building now..." yarn workspace @selfxyz/mobile-app run build:deps + - name: Generate token for self repositories + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} - name: Setup Android private modules run: | cd ${{ env.APP_PATH }} PLATFORM=android node scripts/setup-private-modules.cjs env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} CI: true - name: Build Android (with AAPT2 symlink fix) run: yarn android:ci diff --git a/.github/workflows/mobile-deploy.yml b/.github/workflows/mobile-deploy.yml index 4ac61e8f2..65609a404 100644 --- a/.github/workflows/mobile-deploy.yml +++ b/.github/workflows/mobile-deploy.yml @@ -386,6 +386,7 @@ jobs: id: gems-cache uses: ./.github/actions/cache-bundler with: + # TODO(jcortejoso): Confirm the path of the bundle cache path: ${{ env.APP_PATH }}/ios/vendor/bundle lock-file: app/Gemfile.lock cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }} @@ -429,6 +430,14 @@ jobs: fi echo "βœ… Lock files exist" + - name: Generate token for self repositories + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + configure-netrc: "true" - name: Install Mobile Dependencies (main repo) if: inputs.platform != 'android' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) @@ -439,7 +448,7 @@ jobs: ruby_version: ${{ env.RUBY_VERSION }} workspace: ${{ env.WORKSPACE }} env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - name: Install Mobile Dependencies (forked PRs - no secrets) if: inputs.platform != 'android' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true @@ -692,7 +701,7 @@ jobs: IOS_TESTFLIGHT_GROUPS: ${{ secrets.IOS_TESTFLIGHT_GROUPS }} NODE_OPTIONS: "--max-old-space-size=8192" SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }} - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }} TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }} @@ -1047,6 +1056,14 @@ jobs: echo "βœ… Lock files exist" + - name: Generate token for self repositories + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + - name: Install Mobile Dependencies (main repo) if: inputs.platform != 'ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) uses: ./.github/actions/mobile-setup @@ -1056,7 +1073,7 @@ jobs: ruby_version: ${{ env.RUBY_VERSION }} workspace: ${{ env.WORKSPACE }} env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} PLATFORM: ${{ inputs.platform }} - name: Install Mobile Dependencies (forked PRs - no secrets) @@ -1113,7 +1130,7 @@ jobs: cd ${{ env.APP_PATH }} PLATFORM=android node scripts/setup-private-modules.cjs env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} CI: true - name: Build Dependencies (Android) diff --git a/.github/workflows/mobile-e2e.yml b/.github/workflows/mobile-e2e.yml index da1f946ae..3ecb7b2b3 100644 --- a/.github/workflows/mobile-e2e.yml +++ b/.github/workflows/mobile-e2e.yml @@ -70,6 +70,14 @@ jobs: - name: Toggle Yarn hardened mode for trusted PRs if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV + - name: Generate token for self repositories + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + configure-netrc: "true" - name: Install deps (internal PRs and protected branches) if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} uses: nick-fields/retry@v3 @@ -79,7 +87,7 @@ jobs: retry_wait_seconds: 5 command: yarn install --immutable --silent env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - name: Install deps (forked PRs - no secrets) if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }} uses: nick-fields/retry@v3 @@ -138,7 +146,7 @@ jobs: cd app PLATFORM=android node scripts/setup-private-modules.cjs env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} CI: true - name: Build Android APK run: | @@ -149,6 +157,8 @@ jobs: - name: Clean up Gradle build artifacts uses: ./.github/actions/cleanup-gradle-artifacts - name: Verify APK and android-passport-nfc-reader integration + env: + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} run: | echo "πŸ” Verifying build artifacts..." APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk" @@ -160,8 +170,8 @@ jobs: echo "πŸ“± APK size: $APK_SIZE bytes" # Verify private modules were properly integrated (skip for forks) - if [ -z "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]; then - echo "πŸ”• No PAT available β€” skipping private module verification" + if [ -z "${SELFXYZ_APP_TOKEN:-}" ]; then + echo "πŸ”• No SELFXYZ_APP_TOKEN available β€” skipping private module verification" else # Verify android-passport-nfc-reader if [ -d "app/android/android-passport-nfc-reader" ]; then @@ -263,6 +273,14 @@ jobs: - name: Toggle Yarn hardened mode for trusted PRs if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV + - name: Generate token for self repositories + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + configure-netrc: "true" - name: Install deps (internal PRs and protected branches) if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} uses: nick-fields/retry@v3 @@ -272,7 +290,7 @@ jobs: retry_wait_seconds: 5 command: yarn install --immutable --silent env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - name: Install deps (forked PRs - no secrets) if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }} uses: nick-fields/retry@v3 @@ -360,7 +378,7 @@ jobs: echo "πŸ“¦ Installing pods via centralized script…" BUNDLE_GEMFILE=../Gemfile bundle exec bash scripts/pod-install-with-cache-fix.sh env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - name: Setup iOS Simulator run: | echo "Setting up iOS Simulator..." diff --git a/.github/workflows/mobile-sdk-demo-e2e.yml b/.github/workflows/mobile-sdk-demo-e2e.yml index d1ecbc6ec..be554e3df 100644 --- a/.github/workflows/mobile-sdk-demo-e2e.yml +++ b/.github/workflows/mobile-sdk-demo-e2e.yml @@ -73,6 +73,13 @@ jobs: - name: Toggle Yarn hardened mode for trusted PRs if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV + - name: Generate token for self repositories + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} - name: Install deps (internal PRs and protected branches) if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} uses: nick-fields/retry@v3 @@ -82,7 +89,7 @@ jobs: retry_wait_seconds: 5 command: yarn install --immutable --silent env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - name: Install deps (forked PRs - no secrets) if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }} uses: nick-fields/retry@v3 @@ -237,6 +244,13 @@ jobs: - name: Toggle Yarn hardened mode for trusted PRs if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV + - name: Generate token for self repositories + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} - name: Install deps (internal PRs and protected branches) if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} uses: nick-fields/retry@v3 @@ -246,7 +260,7 @@ jobs: retry_wait_seconds: 5 command: yarn install --immutable --silent env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - name: Install deps (forked PRs - no secrets) if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }} uses: nick-fields/retry@v3 @@ -322,15 +336,15 @@ jobs: max_attempts: 3 retry_wait_seconds: 10 command: | - if [ -n "${SELFXYZ_INTERNAL_REPO_PAT}" ]; then - echo "πŸ”‘ Using SELFXYZ_INTERNAL_REPO_PAT for private pod access" - echo "::add-mask::${SELFXYZ_INTERNAL_REPO_PAT}" + if [ -n "${SELFXYZ_APP_TOKEN}" ]; then + echo "πŸ”‘ Using GitHub App token for private pod access" + echo "::add-mask::${SELFXYZ_APP_TOKEN}" fi cd packages/mobile-sdk-demo/ios echo "πŸ“¦ Installing pods via cache-fix script…" bash scripts/pod-install-with-cache-fix.sh env: - SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} + SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} GIT_TERMINAL_PROMPT: 0 - name: Setup iOS Simulator run: | diff --git a/app/ios/Podfile b/app/ios/Podfile index 8aa6f7023..c44bb5f41 100755 --- a/app/ios/Podfile +++ b/app/ios/Podfile @@ -33,7 +33,7 @@ def using_https_git_auth? auth_data.include?("Logged in to github.com account") && auth_data.include?("Git operations protocol: https") rescue => e - puts "gh auth status failed, assuming no HTTPS auth -- will try SSH" + # Avoid printing auth-related details in CI logs. false end end @@ -51,18 +51,16 @@ target "Self" do # External fork - use public NFCPassportReader repository (placeholder) # TODO: Replace with actual public NFCPassportReader repository URL nfc_repo_url = "https://github.com/PLACEHOLDER/NFCPassportReader.git" - puts "πŸ“¦ Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})" - elsif ENV["GITHUB_ACTIONS"] == "true" && ENV["SELFXYZ_INTERNAL_REPO_PAT"] - # Running in selfxyz GitHub Actions with PAT available - use private repo with token - nfc_repo_url = "https://#{ENV["SELFXYZ_INTERNAL_REPO_PAT"]}@github.com/selfxyz/NFCPassportReader.git" - puts "πŸ“¦ Using private NFCPassportReader with PAT (selfxyz GitHub Actions)" + elsif ENV["GITHUB_ACTIONS"] == "true" + # CI: NEVER embed credentials in URLs. Rely on workflow-provided auth via: + # - ~/.netrc or a Git credential helper, and token masking in logs. + nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git" elsif using_https_git_auth? # Local development with HTTPS GitHub auth via gh - use HTTPS to private repo nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git" else # Local development in selfxyz repo - use SSH to private repo nfc_repo_url = "git@github.com:selfxyz/NFCPassportReader.git" - puts "πŸ“¦ Using SSH for private NFCPassportReader (local selfxyz development)" end pod "NFCPassportReader", git: nfc_repo_url, commit: "9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b" diff --git a/app/scripts/mobile-ci-build-android.sh b/app/scripts/mobile-ci-build-android.sh index 5da46c08f..501d48782 100755 --- a/app/scripts/mobile-ci-build-android.sh +++ b/app/scripts/mobile-ci-build-android.sh @@ -109,7 +109,13 @@ clone_private_module() { local dir_name=$(basename "$target_dir") # Use different clone methods based on environment - if is_ci && [[ -n "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]]; then + if is_ci && [[ -n "${SELFXYZ_APP_TOKEN:-}" ]]; then + # CI environment with GitHub App installation token + git clone "https://x-access-token:${SELFXYZ_APP_TOKEN}@github.com/selfxyz/${repo_name}.git" "$dir_name" || { + log "ERROR: Failed to clone $repo_name with GitHub App token" + exit 1 + } + elif is_ci && [[ -n "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]]; then # CI environment with PAT (fallback if action didn't run) git clone "https://${SELFXYZ_INTERNAL_REPO_PAT}@github.com/selfxyz/${repo_name}.git" "$dir_name" || { log "ERROR: Failed to clone $repo_name with PAT" @@ -119,14 +125,14 @@ clone_private_module() { # Local development with SSH git clone "git@github.com:selfxyz/${repo_name}.git" "$dir_name" || { log "ERROR: Failed to clone $repo_name with SSH" - log "Please ensure you have SSH access to the repository or set SELFXYZ_INTERNAL_REPO_PAT" + log "Please ensure you have SSH access to the repository or set SELFXYZ_APP_TOKEN/SELFXYZ_INTERNAL_REPO_PAT" exit 1 } else log "ERROR: No authentication method available for cloning $repo_name" log "Please either:" log " - Set up SSH access (for local development)" - log " - Set SELFXYZ_INTERNAL_REPO_PAT environment variable (for CI)" + log " - Set SELFXYZ_APP_TOKEN or SELFXYZ_INTERNAL_REPO_PAT environment variable (for CI)" exit 1 fi @@ -194,14 +200,15 @@ log "βœ… Package files backed up successfully" # Install SDK from tarball in app with timeout log "Installing SDK as real files..." if is_ci; then - # Temporarily unset PAT to skip private modules during SDK installation - env -u SELFXYZ_INTERNAL_REPO_PAT timeout 180 yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" || { + # Temporarily unset both auth tokens to skip private modules during SDK installation + # Both tokens must be unset to prevent setup-private-modules.cjs from attempting clones + env -u SELFXYZ_INTERNAL_REPO_PAT -u SELFXYZ_APP_TOKEN timeout 180 yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" || { log "SDK installation timed out after 3 minutes" exit 1 } else - # Temporarily unset PAT to skip private modules during SDK installation - env -u SELFXYZ_INTERNAL_REPO_PAT yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" + # Temporarily unset both auth tokens to skip private modules during SDK installation + env -u SELFXYZ_INTERNAL_REPO_PAT -u SELFXYZ_APP_TOKEN yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" fi # Verify installation (check for AAR file in both local and hoisted locations) diff --git a/app/scripts/setup-private-modules.cjs b/app/scripts/setup-private-modules.cjs index 73db64b4e..39ded4be9 100644 --- a/app/scripts/setup-private-modules.cjs +++ b/app/scripts/setup-private-modules.cjs @@ -29,8 +29,9 @@ const PRIVATE_MODULES = [ // Environment detection // CI is set by GitHub Actions, CircleCI, etc. Check for truthy value -const isCI = !!process.env.CI || process.env.GITHUB_ACTIONS === 'true'; +const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'; const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT; +const appToken = process.env.SELFXYZ_APP_TOKEN; // GitHub App installation token const isDryRun = process.env.DRY_RUN === 'true'; // Platform detection for Android-specific modules @@ -150,13 +151,17 @@ function clonePrivateRepo(repoName, localPath) { let cloneUrl; - if (isCI && repoToken) { + if (isCI && appToken) { + // CI environment with GitHub App installation token + log('CI detected: Using SELFXYZ_APP_TOKEN for clone', 'info'); + cloneUrl = `https://x-access-token:${appToken}@github.com/${GITHUB_ORG}/${repoName}.git`; + } else if (isCI && repoToken) { // CI environment with Personal Access Token log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for clone', 'info'); cloneUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${repoName}.git`; } else if (isCI) { log( - 'CI environment detected but SELFXYZ_INTERNAL_REPO_PAT not available - skipping private module setup', + 'CI environment detected but no token available - skipping private module setup', 'info', ); log( @@ -173,7 +178,7 @@ function clonePrivateRepo(repoName, localPath) { } // Security: Use quiet mode for credentialed URLs to prevent token exposure - const isCredentialedUrl = isCI && repoToken; + const isCredentialedUrl = isCI && (appToken || repoToken); const quietFlag = isCredentialedUrl ? '--quiet' : ''; const targetDir = path.basename(localPath); const cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" "${targetDir}"`; @@ -190,7 +195,7 @@ function clonePrivateRepo(repoName, localPath) { } catch (error) { if (isCI) { log( - 'Clone failed in CI environment. Check SELFXYZ_INTERNAL_REPO_PAT permissions.', + 'Clone failed in CI environment. Check SELFXYZ_APP_TOKEN or SELFXYZ_INTERNAL_REPO_PAT permissions.', 'error', ); } else { @@ -231,7 +236,7 @@ function setupPrivateModule(module) { } // Security: Remove credential-embedded remote URL after clone - if (isCI && repoToken && !isDryRun) { + if (isCI && (appToken || repoToken) && !isDryRun) { scrubGitRemoteUrl(localPath, repoName); } @@ -275,6 +280,11 @@ function setupAndroidPassportReader() { `Setup complete: ${successCount}/${PRIVATE_MODULES.length} modules cloned`, 'warning', ); + } else { + log( + 'No private modules were cloned - this is expected for forked PRs', + 'info', + ); } } diff --git a/app/tests/src/integrations/nfc/nfcScanner.test.ts b/app/tests/src/integrations/nfc/nfcScanner.test.ts index 1b70a5fc3..45a3e6af4 100644 --- a/app/tests/src/integrations/nfc/nfcScanner.test.ts +++ b/app/tests/src/integrations/nfc/nfcScanner.test.ts @@ -3,19 +3,35 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. // Mock Platform without requiring react-native to avoid memory issues -// Use a simple object that can be modified directly +// Use a global variable with getter to allow per-test platform switching +// This pattern avoids hoisting issues with jest.mock import { Buffer } from 'buffer'; import { parseScanResponse, scan } from '@/integrations/nfc/nfcScanner'; import { PassportReader } from '@/integrations/nfc/passportReader'; -const Platform = { - OS: 'ios', // Default to iOS - Version: 14, -}; +// Declare global variable for platform OS that can be modified per-test +declare global { + // eslint-disable-next-line no-var + var mockPlatformOS: 'ios' | 'android'; +} +// Initialize the global mock platform - default to iOS +global.mockPlatformOS = 'ios'; + +// Override the react-native mock from jest.setup.js with a getter-based Platform +// This allows tests to change Platform.OS dynamically by modifying global.mockPlatformOS jest.mock('react-native', () => ({ - Platform, + Platform: { + get OS() { + return global.mockPlatformOS; + }, + Version: 14, + select: jest.fn((obj: Record) => { + const os = global.mockPlatformOS; + return obj[os] || obj.default; + }), + }, })); // Ensure the Node Buffer implementation is available to the module under test @@ -25,7 +41,7 @@ describe('parseScanResponse', () => { beforeEach(() => { jest.clearAllMocks(); // Reset Platform.OS to default before each test to prevent pollution - Platform.OS = 'ios'; + global.mockPlatformOS = 'ios'; }); it('parses iOS response', () => { @@ -93,9 +109,8 @@ describe('parseScanResponse', () => { }); it('parses Android response', () => { - // Temporarily override Platform.OS for this test - const originalOS = Platform.OS; - Platform.OS = 'android'; + // Set Platform.OS to android for this test + global.mockPlatformOS = 'android'; const mrz = 'P { // dg2Hash should be parsed from hex string '1234': 12 = 18, 34 = 52 expect(result.dg2Hash).toEqual([18, 52]); expect(result.dgPresents).toEqual([1, 2]); - - // Restore original value - Platform.OS = originalOS; }); it('handles malformed iOS response', () => { @@ -172,8 +184,8 @@ describe('parseScanResponse', () => { }); it('handles malformed Android response', () => { - const originalOS = Platform.OS; - Platform.OS = 'android'; + // Set Platform.OS to android for this test + global.mockPlatformOS = 'android'; const response = { mrz: 'valid_mrz', @@ -182,9 +194,6 @@ describe('parseScanResponse', () => { }; expect(() => parseScanResponse(response)).toThrow(); - - // Restore original value - Platform.OS = originalOS; }); it('handles missing required fields', () => { @@ -232,7 +241,7 @@ describe('scan', () => { beforeEach(() => { jest.clearAllMocks(); // Reset Platform.OS to default before each test to prevent pollution - Platform.OS = 'ios'; + global.mockPlatformOS = 'ios'; // Reset PassportReader mock before each test // The implementation checks for scanPassport property, so we need to ensure it exists Object.defineProperty(PassportReader, 'scanPassport', { diff --git a/packages/mobile-sdk-alpha/scripts/setup-native-source.cjs b/packages/mobile-sdk-alpha/scripts/setup-native-source.cjs index 1e1dbcc2a..e465601aa 100644 --- a/packages/mobile-sdk-alpha/scripts/setup-native-source.cjs +++ b/packages/mobile-sdk-alpha/scripts/setup-native-source.cjs @@ -18,6 +18,7 @@ const BRANCH = 'main'; // Environment detection const isCI = process.env.CI === 'true'; const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT; +const appToken = process.env.SELFXYZ_APP_TOKEN; // GitHub App installation token const isDryRun = process.env.DRY_RUN === 'true'; function log(message, type = 'info') { @@ -89,19 +90,24 @@ function setupSubmodule() { let submoduleUrl; - if (isCI && repoToken) { + if (isCI && appToken) { + // CI environment with GitHub App installation token + // Security: NEVER embed credentials in git URLs. Rely on CI-provided auth via: + // - ~/.netrc, a Git credential helper, or SSH agent configuration. + submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`; + } else if (isCI && repoToken) { // CI environment with Personal Access Token - log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for submodule', 'info'); - submoduleUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${REPO_NAME}.git`; + // Security: NEVER embed credentials in git URLs. Rely on CI-provided auth via: + // - ~/.netrc, a Git credential helper, or SSH agent configuration. + submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`; } else if (isCI) { - log('CI environment detected but SELFXYZ_INTERNAL_REPO_PAT not available - skipping private module setup', 'info'); + log('CI environment detected but no token available - skipping private module setup', 'info'); log('This is expected for forked PRs or environments without access to private modules', 'info'); return false; // Return false to indicate setup was skipped } else if (usingHTTPSGitAuth()) { submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`; } else { // Local development with SSH - log('Local development: Using SSH for submodule', 'info'); submoduleUrl = `git@github.com:${GITHUB_ORG}/${REPO_NAME}.git`; } @@ -113,7 +119,7 @@ function setupSubmodule() { } else { // Add submodule const addCommand = `git submodule add -b ${BRANCH} "${submoduleUrl}" mobile-sdk-native`; - if (isCI && repoToken) { + if (isCI && (appToken || repoToken)) { // Security: Run command silently to avoid token exposure in logs runCommand(addCommand, { stdio: 'pipe' }); } else { @@ -125,7 +131,7 @@ function setupSubmodule() { return true; // Return true to indicate successful setup } catch (error) { if (isCI) { - log('Submodule setup failed in CI environment. Check SELFXYZ_INTERNAL_REPO_PAT permissions.', 'error'); + log('Submodule setup failed in CI environment. Check repository access/credentials configuration.', 'error'); } else { log('Submodule setup failed. Ensure you have SSH access to the repository.', 'error'); } @@ -169,7 +175,7 @@ function setupMobileSDKNative() { } // Security: Remove credential-embedded remote URL after setup - if (isCI && repoToken && !isDryRun) { + if (isCI && (appToken || repoToken) && !isDryRun) { scrubGitRemoteUrl(); } diff --git a/packages/mobile-sdk-demo/ios/Podfile b/packages/mobile-sdk-demo/ios/Podfile index b7217c203..386604467 100644 --- a/packages/mobile-sdk-demo/ios/Podfile +++ b/packages/mobile-sdk-demo/ios/Podfile @@ -46,12 +46,15 @@ target "SelfDemoApp" do nfc_repo_url = if !is_selfxyz_repo puts "πŸ“¦ Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})" "https://github.com/PLACEHOLDER/NFCPassportReader.git" - elsif ENV["GITHUB_ACTIONS"] == "true" && ENV["SELFXYZ_INTERNAL_REPO_PAT"] && !ENV["SELFXYZ_INTERNAL_REPO_PAT"].empty? - puts "πŸ“¦ Using private NFCPassportReader with PAT (selfxyz GitHub Actions)" - "https://#{ENV["SELFXYZ_INTERNAL_REPO_PAT"]}@github.com/selfxyz/NFCPassportReader.git" + elsif ENV["GITHUB_ACTIONS"] == "true" + # CI: NEVER embed credentials in URLs. Rely on workflow-provided auth via: + # - ~/.netrc or a Git credential helper, and token masking in logs. + "https://github.com/selfxyz/NFCPassportReader.git" elsif using_https_git_auth? + # Local development with HTTPS GitHub auth via gh - use HTTPS to private repo "https://github.com/selfxyz/NFCPassportReader.git" else + # Local development in selfxyz repo - use SSH to private repo puts "πŸ“¦ Using SSH for private NFCPassportReader (local selfxyz development)" "git@github.com:selfxyz/NFCPassportReader.git" end