Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions .github/workflows/qrcode-sdk-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
name: QRCode SDK CI

env:
# Build environment versions
NODE_VERSION: 22
# Cache versioning - increment these to bust caches when needed
GH_CACHE_VERSION: v1 # Global cache version
GH_YARN_CACHE_VERSION: v1 # Yarn-specific cache version
GH_SDK_CACHE_VERSION: v1 # SDK build cache version

on:
pull_request:
paths:
- "sdk/qrcode/**"
- "common/**"
- ".github/workflows/qrcode-sdk-ci.yml"
- ".github/actions/**"
push:
branches: [main, develop]
paths:
- "sdk/qrcode/**"
- "common/**"

jobs:
# Build dependencies once and cache for other jobs
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'

- name: Enable Corepack
run: corepack enable

- name: Cache Yarn dependencies
id: yarn-cache
uses: actions/cache@v4
with:
path: |
.yarn/cache
node_modules
sdk/qrcode/node_modules
common/node_modules
key: ${{ runner.os }}-node${{ env.NODE_VERSION }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-node${{ env.NODE_VERSION }}-yarn-
- name: Install Dependencies
uses: ./.github/actions/yarn-install

- name: Build dependencies
shell: bash
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/qrcode build
- name: Cache build artifacts
uses: actions/cache/save@v4
with:
path: |
common/dist
sdk/qrcode/dist
node_modules
sdk/qrcode/node_modules
common/node_modules
key: qrcode-sdk-build-${{ github.sha }}

# Quality checks job
quality-checks:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'

- name: Enable Corepack
run: corepack enable

- name: Cache Yarn dependencies
id: yarn-cache
uses: actions/cache@v4
with:
path: |
.yarn/cache
node_modules
sdk/qrcode/node_modules
common/node_modules
key: ${{ runner.os }}-node${{ env.NODE_VERSION }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-node${{ env.NODE_VERSION }}-yarn-
- name: Install Dependencies
uses: ./.github/actions/yarn-install

- name: Restore build artifacts
uses: actions/cache/restore@v4
with:
path: |
common/dist
sdk/qrcode/dist
node_modules
sdk/qrcode/node_modules
common/node_modules
key: qrcode-sdk-build-${{ github.sha }}
fail-on-cache-miss: true

- name: Run linter
run: yarn workspace @selfxyz/qrcode lint:imports:check

- name: Check Prettier formatting
run: yarn workspace @selfxyz/qrcode lint

- name: Type checking
run: yarn workspace @selfxyz/qrcode types

- name: Log cache status
run: |
echo "Cache hit results:"
echo "- Yarn cache hit: ${{ steps.yarn-cache.outputs.cache-hit }}"
# Build verification job
build-verification:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'

- name: Enable Corepack
run: corepack enable

- name: Cache Yarn dependencies
id: yarn-cache
uses: actions/cache@v4
with:
path: |
.yarn/cache
node_modules
sdk/qrcode/node_modules
common/node_modules
key: ${{ runner.os }}-node${{ env.NODE_VERSION }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-node${{ env.NODE_VERSION }}-yarn-
- name: Install Dependencies
uses: ./.github/actions/yarn-install

- name: Restore build artifacts
uses: actions/cache/restore@v4
with:
path: |
common/dist
sdk/qrcode/dist
node_modules
sdk/qrcode/node_modules
common/node_modules
key: qrcode-sdk-build-${{ github.sha }}
fail-on-cache-miss: true

- name: Verify build output
run: |
echo "Checking build output structure..."
ls -la sdk/qrcode/dist/
echo "Checking ESM build..."
ls -la sdk/qrcode/dist/esm/
echo "Checking CJS build..."
ls -la sdk/qrcode/dist/cjs/
echo "Checking type definitions..."
if ! find sdk/qrcode/dist/esm -maxdepth 1 -name '*.d.ts' | grep -q .; then
echo "No .d.ts files found in dist/esm"; exit 1;
fi
find sdk/qrcode/dist/esm -maxdepth 1 -name '*.d.ts' -ls
- name: Test package exports
run: |
echo "Testing package exports..."
node -e "
const pkg = require('./sdk/qrcode/package.json');
console.log('Package exports:', JSON.stringify(pkg.exports, null, 2));
"
- name: Verify bundle size
run: yarn workspace @selfxyz/qrcode size-limit

- name: Log cache status
run: |
echo "Cache hit results:"
echo "- Yarn cache hit: ${{ steps.yarn-cache.outputs.cache-hit }}"
# Integration test job
integration-test:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'

- name: Enable Corepack
run: corepack enable

- name: Cache Yarn dependencies
id: yarn-cache
uses: actions/cache@v4
with:
path: |
.yarn/cache
node_modules
sdk/qrcode/node_modules
common/node_modules
key: ${{ runner.os }}-node${{ env.NODE_VERSION }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-node${{ env.NODE_VERSION }}-yarn-
- name: Install Dependencies
uses: ./.github/actions/yarn-install

- name: Restore build artifacts
uses: actions/cache/restore@v4
with:
path: |
common/dist
sdk/qrcode/dist
node_modules
sdk/qrcode/node_modules
common/node_modules
key: qrcode-sdk-build-${{ github.sha }}
fail-on-cache-miss: true

- name: Run tests
run: yarn workspace @selfxyz/qrcode test

- name: Test package import
run: |
echo "Testing package import..."
node -e "
try {
const { SelfQRcode, SelfQRcodeWrapper, countries } = require('./sdk/qrcode/dist/cjs/index.cjs');
console.log('✅ Package import successful');
console.log('Exported components:', Object.keys({ SelfQRcode, SelfQRcodeWrapper, countries }));
} catch (error) {
console.error('❌ Package import failed:', error.message);
process.exit(1);
}
"
- name: Log cache status
run: |
echo "Cache hit results:"
echo "- Yarn cache hit: ${{ steps.yarn-cache.outputs.cache-hit }}"
4 changes: 2 additions & 2 deletions app/scripts/bundle-analyze-ci.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ function formatBytes(bytes) {
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i];
}

function checkBundleSize(bundleSize, platform) {
const thresholdMB = BUNDLE_THRESHOLDS_MB[platform];
function checkBundleSize(bundleSize, targetPlatform) {
const thresholdMB = BUNDLE_THRESHOLDS_MB[targetPlatform];
const thresholdBytes = thresholdMB * 1024 * 1024;

console.log(`\n📦 Bundle size: ${formatBytes(bundleSize)}`);
Expand Down
8 changes: 4 additions & 4 deletions app/scripts/mobile-deploy-confirm.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ function displayPlatformVersions(platform, versions) {
const currentBuild = versions.ios.build;
const nextBuild = versions.versionJson
? versions.versionJson.ios.build + 1
: parseInt(currentBuild) + 1;
: parseInt(currentBuild, 10) + 1;
const lastDeployed = versions.versionJson
? getTimeAgo(versions.versionJson.ios.lastDeployed)
: 'Unknown';
Expand All @@ -371,7 +371,7 @@ function displayPlatformVersions(platform, versions) {
const currentBuild = versions.android.versionCode;
const nextBuild = versions.versionJson
? versions.versionJson.android.build + 1
: parseInt(currentBuild) + 1;
: parseInt(currentBuild, 10) + 1;
const lastDeployed = versions.versionJson
? getTimeAgo(versions.versionJson.android.lastDeployed)
: 'Unknown';
Expand All @@ -391,7 +391,7 @@ function displayPlatformVersions(platform, versions) {
if (versions.versionJson) {
if (platform === PLATFORMS.IOS || platform === PLATFORMS.BOTH) {
const jsonBuild = versions.versionJson.ios.build;
const actualBuild = parseInt(versions.ios.build);
const actualBuild = parseInt(versions.ios.build, 10);
if (jsonBuild !== actualBuild) {
console.log(
`\n${CONSOLE_SYMBOLS.WARNING} iOS build mismatch: version.json has ${jsonBuild}, but Xcode has ${actualBuild}`,
Expand All @@ -401,7 +401,7 @@ function displayPlatformVersions(platform, versions) {

if (platform === PLATFORMS.ANDROID || platform === PLATFORMS.BOTH) {
const jsonBuild = versions.versionJson.android.build;
const actualBuild = parseInt(versions.android.versionCode);
const actualBuild = parseInt(versions.android.versionCode, 10);
if (jsonBuild !== actualBuild) {
console.log(
`\n${CONSOLE_SYMBOLS.WARNING} Android build mismatch: version.json has ${jsonBuild}, but gradle has ${actualBuild}`,
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/tests/tree-shaking.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
assert(stats.isFile(), `${script} should be a file`);

// Check if file is executable (has execute permission)
const isExecutable = (stats.mode & parseInt('111', 8)) !== 0;
const isExecutable = (stats.mode & 0o111) !== 0;

Check warning on line 27 in app/scripts/tests/tree-shaking.test.cjs

View workflow job for this annotation

GitHub Actions / lint

Unexpected use of '&'
assert(isExecutable, `${script} should be executable`);
});
});
Expand Down
4 changes: 2 additions & 2 deletions app/src/providers/authProvider.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,10 @@ export const AuthProvider = ({
} else {
return { success: false, error: 'No private key provided' };
}
} catch (error: unknown) {
} catch (err: unknown) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
error: err instanceof Error ? err.message : String(err),
};
}
})();
Expand Down
8 changes: 5 additions & 3 deletions app/src/screens/prove/ProofRequestStatusScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ const SuccessScreen: React.FC = () => {
if (isFocused && !countdownStarted && selfApp?.deeplinkCallback) {
if (selfApp?.deeplinkCallback) {
try {
new URL(selfApp.deeplinkCallback);
setCountdown(5);
setCountdownStarted(true);
const url = new URL(selfApp.deeplinkCallback);
if (url) {
setCountdown(5);
setCountdownStarted(true);
}
Comment on lines +89 to +93
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate protocol and avoid redundant if (url); store validated URL for use on redirect.

  • new URL(...) already guarantees a truthy object; the if (url) guard is redundant.
  • For security, explicitly allow only known-safe protocols and store the validated value to use later with Linking.openURL, preventing TOCTOU drift if selfApp.deeplinkCallback changes.

Apply:

-            const url = new URL(selfApp.deeplinkCallback);
-            if (url) {
-              setCountdown(5);
-              setCountdownStarted(true);
-            }
+            const url = new URL(selfApp.deeplinkCallback);
+            const allowedProtocols = new Set(['https:', 'http:', 'self:']); // adjust as needed
+            if (allowedProtocols.has(url.protocol)) {
+              setCountdown(5);
+              setCountdownStarted(true);
+              setValidatedDeeplink(url.toString());
+            } else {
+              console.warn('Unsupported deep link protocol:', url.protocol);
+            }

Also update outside this hunk:

  • Add state near other state hooks:
const [validatedDeeplink, setValidatedDeeplink] = useState<string | null>(null);
  • Use it on redirect:
if (validatedDeeplink) {
  Linking.openURL(validatedDeeplink).catch(err => { /* ... */ });
}
  • And for UI display:
deeplinkCallback={validatedDeeplink?.replace(/^https?:\/\//, '')}
🤖 Prompt for AI Agents
In app/src/screens/prove/ProofRequestStatusScreen.tsx around lines 89-93, the if
(url) check is redundant and the deeplinkCallback should be validated and stored
to avoid TOCTOU and unsafe protocols; replace the redundant guard by validating
new URL(selfApp.deeplinkCallback) for allowed protocols (e.g. http, https, your
app-scheme) and on success call setValidatedDeeplink with the sanitized string
and start the countdown (setCountdown/setCountdownStarted); add a new state near
the other hooks: validatedDeeplink (string | null) as suggested, use
validatedDeeplink for Linking.openURL in the redirect (with proper catch) and
for UI display (strip protocol when rendering) so the code always uses the
pre-validated URL rather than reading selfApp.deeplinkCallback later.

} catch (error) {
console.warn(
'Invalid deep link URL provided:',
Expand Down
7 changes: 5 additions & 2 deletions app/tests/src/providers/passportDataProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,15 @@ describe('PassportDataProvider', () => {
);

const updateCount = getByTestId('update-count');
const initialCount = parseInt(updateCount.props.children);
const initialCount = parseInt(updateCount.props.children, 10);

// Wait for updates to occur
await waitFor(
() => {
const newCount = parseInt(getByTestId('update-count').props.children);
const newCount = parseInt(
getByTestId('update-count').props.children,
10,
);
expect(newCount).toBeGreaterThan(initialCount);
},
{ timeout: 1000 },
Expand Down
1 change: 1 addition & 0 deletions common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {
ID_CARD_ATTESTATION_ID,
PASSPORT_ATTESTATION_ID,
PCR0_MANAGER_ADDRESS,
REDIRECT_URL,
RPC_URL,
TREE_URL,
TREE_URL_STAGING,
Expand Down
Loading
Loading