Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,26 @@ jobs:
run: npm run build
- name: Test
run: npm test -- --run --passWithNoTests

e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Install Playwright browser
run: npm run test:e2e:install
- name: Run deterministic Playwright suite
run: npm run test:e2e
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Upload Playwright artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
Comment on lines +32 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Find mutable action refs in workflow:"
rg -n 'uses:\s*actions\/[^@]+@v[0-9]+' .github/workflows/ci.yml || true

echo
echo "2) Show checkout step context to verify persist-credentials hardening:"
rg -n -A6 -B2 'uses:\s*actions/checkout@' .github/workflows/ci.yml || true

echo
echo "Expected:"
echo "- No actions/* refs pinned only to `@v`* tags (use full commit SHAs)."
echo "- Every checkout step includes: with: persist-credentials: false"

Repository: dashpay/dash-bridge

Length of output: 981


🏁 Script executed:

#!/bin/bash
set -euo pipefail

WF=".github/workflows/ci.yml"

echo "=== File excerpt around reported lines (24-60) ==="
nl -ba "$WF" | sed -n '24,60p'

echo
echo "=== All action uses in this workflow ==="
rg -n '^\s*-\s*uses:\s*actions\/' "$WF" || true

echo
echo "=== Any persist-credentials setting ==="
rg -n 'persist-credentials' "$WF" || true

echo
echo "=== Checkout steps with their immediate context ==="
rg -n -A8 -B1 'uses:\s*actions/checkout@' "$WF" || true

echo
echo "=== Attempt to detect unpinned actions/*`@v`* refs (fixed regex) ==="
rg -n '^\s*-\s*uses:\s*actions\/[^@]+@v[0-9]+' "$WF" || true

Repository: dashpay/dash-bridge

Length of output: 175


🏁 Script executed:

#!/bin/bash
set -euo pipefail

WF=".github/workflows/ci.yml"

echo "=== File excerpt around reported lines (24-60) ==="
awk 'NR>=24 && NR<=60 {printf "%6d: %s\n", NR, $0}' "$WF"

echo
echo "=== All action uses in this workflow (actions/* only) ==="
rg -n '^\s*-\s*uses:\s*actions\/' "$WF" || true

echo
echo "=== Any persist-credentials setting ==="
rg -n 'persist-credentials' "$WF" || true

echo
echo "=== Checkout steps with immediate context ==="
rg -n -A8 -B1 'uses:\s*actions/checkout@' "$WF" || true

echo
echo "=== Attempt to detect unpinned actions/*`@v`* refs (fixed regex) ==="
# match: uses: actions/<name>`@v`<number> (e.g., `@v4`, `@v3`)
rg -n '^\s*-\s*uses:\s*actions\/[^@]+@v[0-9]+' "$WF" || true

Repository: dashpay/dash-bridge

Length of output: 2168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

WF=".github/workflows/ci.yml"

echo "=== Search: uses actions/*`@v`<number> (unpinned) ==="
rg -n 'uses:\s*actions/.*`@v`[0-9]+' "$WF" || true

echo
echo "=== Search: persist-credentials ==="
rg -n 'persist-credentials' "$WF" || true

echo
echo "=== Search: checkout step with surrounding lines ==="
rg -n -A8 -B1 'uses:\s*actions/checkout@' "$WF" || true

Repository: dashpay/dash-bridge

Length of output: 1018


🌐 Web query:

actions/checkout persist-credentials default value

💡 Result:

The default value of actions/checkout input persist-credentials is true (i.e., credentials are persisted unless you set persist-credentials: false). [1][2] Evidence: the actions/checkout README and inputs documentation describe persist-credentials as “Default: true,” and indicate you can opt out by setting persist-credentials: false. [2][3] For security context, GitHub notes that the auth token is persisted locally to enable authenticated git commands and is removed during post-job cleanup; setting persist-credentials: false opts out. [2][3]

Citations:


Pin GitHub Actions to commit SHAs and harden actions/checkout credentials

  • .github/workflows/ci.yml (e2e job, lines ~32-44) uses mutable action tags: actions/checkout@v4, actions/setup-node@v4, and actions/upload-artifact@v4; pin these to full commit SHAs.
  • The actions/checkout step in this job does not set with: persist-credentials: false (and persist-credentials is not set anywhere in the workflow). Default is true, so the GitHub token is persisted in the runner workspace.
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 32-32: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 32-32: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 33-33: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 44-44: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 32 - 44, Update the CI workflow to pin
mutable action tags to their full commit SHAs for actions/checkout,
actions/setup-node, and actions/upload-artifact (replace actions/checkout@v4,
actions/setup-node@v4, actions/upload-artifact@v4 with their corresponding
commit SHA refs) and harden the checkout step by adding with:
persist-credentials: false to the actions/checkout invocation so the
GITHUB_TOKEN is not left in the runner workspace; ensure the same checkout step
still includes any existing settings like node-version/cache if applicable.

with:
name: playwright-artifacts
path: |
playwright-report/
test-results/
if-no-files-found: warn
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ dist/
.claude/
*.log
.DS_Store
playwright-report/
test-results/
.env.playwright-live
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,63 @@ npm run build
npm run preview
```

## Playwright E2E

This repository includes two E2E modes:

- Deterministic UI mode (CI-safe): runs against `?e2e=mock` with no live funds/keys required.
- Optional live testnet mode: runs against testnet endpoints with real identity/key validation and realistic click-through.

### Install browser for E2E

```bash
npm run test:e2e:install
```

### Deterministic E2E (used in CI)

```bash
# Headless
npm run test:e2e

# Headed (local debugging)
npm run test:e2e:headed
```

### Optional live testnet E2E

Live suite is opt-in and skips when required variables are missing.
Never paste real private keys directly inline in shell commands. Inline secrets are saved in shell history.

```bash
# Safer approach: store sensitive values in a protected env file and source it
cat > .env.playwright-live <<'EOF'
PW_E2E_LIVE=1
PW_LIVE_TOPUP_IDENTITY_ID=<44-char-base58-id>
PW_LIVE_DPNS_IDENTITY_ID=<44-char-base58-id>
PW_LIVE_DPNS_PRIVATE_KEY_WIF=<wif-auth-key>
PW_LIVE_MANAGE_IDENTITY_ID=<44-char-base58-id>
PW_LIVE_MANAGE_PRIVATE_KEY_WIF=<wif-master-key>
EOF

chmod 600 .env.playwright-live
set -a
source .env.playwright-live
set +a
npm run test:e2e:live
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Environment variables used by the live suite:

- `PW_E2E_LIVE`: set to `1` to enable live tests.
- `PW_LIVE_TOPUP_IDENTITY_ID`: identity ID used for top-up navigation check.
- `PW_LIVE_DPNS_IDENTITY_ID`: identity ID used for DPNS key validation.
- `PW_LIVE_DPNS_PRIVATE_KEY_WIF`: DPNS AUTHENTICATION key for that identity. Prefer CRITICAL; HIGH is also accepted.
- `PW_LIVE_MANAGE_IDENTITY_ID`: identity ID used for key-management validation.
- `PW_LIVE_MANAGE_PRIVATE_KEY_WIF`: MASTER key for identity-management validation.
- `PLAYWRIGHT_BASE_URL` (optional): override app URL if running against a pre-started server.
- `PLAYWRIGHT_HOST` / `PLAYWRIGHT_PORT` (optional): host/port used by Playwright-managed dev server.

## Deployment

This project automatically deploys to GitHub Pages on push to `main` via GitHub Actions.
Expand Down
124 changes: 124 additions & 0 deletions e2e/deterministic.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { expect, test } from '@playwright/test';
import {
E2E_MOCK_DPNS_WIF,
E2E_MOCK_IDENTITY_ID,
E2E_MOCK_MANAGE_WIF,
} from '../src/e2e-mock-constants';

const MOCK_QUERY = '/?network=testnet&e2e=mock';

test.describe('Deterministic UI E2E (mock mode)', () => {
test('create identity flow transitions to completion and DPNS registration', async ({ page }) => {
await page.goto(MOCK_QUERY);

await page.click('#mode-create-btn');
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible();

await page.click('#continue-btn');

await expect(page.locator('.deposit-headline')).toBeVisible();
await expect.poll(async () => {
return page.evaluate(() => typeof (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance);
}).toBe('function');
await page.evaluate(() => (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance?.());

await expect(page.getByText('Save your keys')).toBeVisible();
await expect(page.locator('.contract-id-section', { hasText: 'Your Identity ID' }).locator('.identity-id')).toHaveText(E2E_MOCK_IDENTITY_ID);

await page.click('#dpns-from-identity-btn');
await expect(page.getByText('Choose Your Usernames')).toBeVisible();

const usernameInput = page.locator('.dpns-username-input').first();
await usernameInput.fill('alpha');
await page.click('#check-availability-btn');

await expect(page.getByText('Review Usernames')).toBeVisible();
await expect(page.getByText('Available (Contested)')).toBeVisible();
await expect(page.locator('#dpns-contested-checkbox')).toBeVisible();
await expect(page.locator('#register-dpns-btn')).toBeDisabled();

await page.locator('#dpns-contested-checkbox').click({ force: true });
await expect(page.locator('#register-dpns-btn')).toBeEnabled();

await page.click('#register-dpns-btn');
await expect(page.getByText('Registration Complete!')).toBeVisible();
});

test('top up flow validates identity input and reaches completion', async ({ page }) => {
await page.goto(MOCK_QUERY);

await page.click('#mode-topup-btn');
await expect(page.locator('#identity-id-input')).toBeVisible();

await page.click('#continue-topup-btn');
await expect(page.locator('#validation-msg')).toContainText('Please enter a valid identity ID');

await page.fill('#identity-id-input', E2E_MOCK_IDENTITY_ID);
await page.click('#continue-topup-btn');

await expect(page.locator('.deposit-headline')).toBeVisible();
await expect.poll(async () => {
return page.evaluate(() => typeof (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance);
}).toBe('function');
await page.evaluate(() => (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance?.());

await expect(page.getByText('Top-up complete!')).toBeVisible();
await expect(page.locator('.contract-id-section', { hasText: 'Identity ID' }).locator('.identity-id')).toHaveText(E2E_MOCK_IDENTITY_ID);
});

test('manage identity flow validates key and applies changes', async ({ page }) => {
await page.goto(MOCK_QUERY);

await page.click('#mode-manage-btn');
await expect(page.locator('#manage-identity-id-input')).toBeVisible();

await page.fill('#manage-identity-id-input', E2E_MOCK_IDENTITY_ID);
await page.locator('#manage-identity-id-input').press('Tab');
await expect(page.getByText('Identity found with 2 keys')).toBeVisible();

await page.fill('#manage-private-key-input', 'bad-key');
await page.locator('#manage-private-key-input').press('Tab');
await expect(page.getByText('Mock mode: use the configured test private key')).toBeVisible();

await page.fill('#manage-private-key-input', E2E_MOCK_MANAGE_WIF);
await page.locator('#manage-private-key-input').blur();
await expect(page.getByText('Manage Keys')).toBeVisible();

await page.click('#add-manage-key-btn');
await page.locator('.manage-disable-key-checkbox').first().click({ force: true });
await expect(page.getByText('Will add 1 key, disable 1 key')).toBeVisible();

await page.click('#apply-manage-btn');
await expect(page.getByText('Update Complete!')).toBeVisible();
});

test('standalone DPNS flow validates identity + key and completes registration', async ({ page }) => {
await page.goto(MOCK_QUERY);

await page.click('#mode-dpns-btn');
await expect(page.getByText('Register a Username')).toBeVisible();

await page.click('#dpns-choose-existing-btn');
await expect(page.locator('#dpns-identity-id-input')).toBeVisible();

await page.fill('#dpns-identity-id-input', E2E_MOCK_IDENTITY_ID);
await page.locator('#dpns-identity-id-input').press('Tab');
await expect(page.getByText('Identity found with 2 keys')).toBeVisible();

await page.fill('#dpns-private-key-input', E2E_MOCK_DPNS_WIF);
await page.locator('#dpns-private-key-input').press('Tab');
await expect(page.getByText('Key matches key #1 (CRITICAL level)')).toBeVisible();

await page.click('#dpns-identity-continue-btn');
await expect(page.getByText('Choose Your Usernames')).toBeVisible();

await page.locator('.dpns-username-input').first().fill('noncontested123456789012345');
await page.click('#check-availability-btn');

await expect(page.getByText('Review Usernames')).toBeVisible();
await expect(page.locator('#register-dpns-btn')).toBeEnabled();

await page.click('#register-dpns-btn');
await expect(page.getByText('Registration Complete!')).toBeVisible();
});
});
80 changes: 80 additions & 0 deletions e2e/live.testnet.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect, test } from '@playwright/test';

const LIVE_ENABLED = process.env.PW_E2E_LIVE === '1';
const LIVE_TOPUP_IDENTITY_ID = process.env.PW_LIVE_TOPUP_IDENTITY_ID;
const LIVE_DPNS_IDENTITY_ID = process.env.PW_LIVE_DPNS_IDENTITY_ID;
const LIVE_DPNS_PRIVATE_KEY_WIF = process.env.PW_LIVE_DPNS_PRIVATE_KEY_WIF;
const LIVE_MANAGE_IDENTITY_ID = process.env.PW_LIVE_MANAGE_IDENTITY_ID;
const LIVE_MANAGE_PRIVATE_KEY_WIF = process.env.PW_LIVE_MANAGE_PRIVATE_KEY_WIF;

test.describe('Live testnet E2E (optional)', () => {
test.skip(!LIVE_ENABLED, 'Set PW_E2E_LIVE=1 to enable live testnet checks');

test('create mode renders real deposit details on testnet', async ({ page }) => {
await page.goto('/?network=testnet&e2e=live');

await page.click('#mode-create-btn');
await page.click('#continue-btn');

await expect(page.locator('.deposit-headline')).toBeVisible({ timeout: 15000 });
await expect(page.getByRole('button', { name: 'Request Testnet Funds' })).toBeVisible({ timeout: 15000 });
await expect(page.locator('.address')).toBeVisible({ timeout: 15000 });
});

test('top up mode validates and renders testnet deposit step', async ({ page }) => {
test.skip(!LIVE_TOPUP_IDENTITY_ID, 'Set PW_LIVE_TOPUP_IDENTITY_ID to run top-up live check');

await page.goto('/?network=testnet&e2e=live');

await page.click('#mode-topup-btn');
await page.fill('#identity-id-input', LIVE_TOPUP_IDENTITY_ID!);
await page.click('#continue-topup-btn');

await expect(page.locator('.deposit-headline')).toContainText('Top up', { timeout: 15000 });
await expect(page.locator('.address')).toBeVisible({ timeout: 15000 });
});

test('dpns existing identity key validation works against testnet', async ({ page }) => {
test.skip(
!LIVE_DPNS_IDENTITY_ID || !LIVE_DPNS_PRIVATE_KEY_WIF,
'Set PW_LIVE_DPNS_IDENTITY_ID and PW_LIVE_DPNS_PRIVATE_KEY_WIF to run DPNS live check'
);

await page.goto('/?network=testnet&e2e=live');

await page.click('#mode-dpns-btn');
await page.click('#dpns-choose-existing-btn');

await page.fill('#dpns-identity-id-input', LIVE_DPNS_IDENTITY_ID!);
await page.locator('#dpns-identity-id-input').press('Tab');

await expect(page.locator('.identity-status.success')).toContainText('Identity found with', { timeout: 90000 });

await page.fill('#dpns-private-key-input', LIVE_DPNS_PRIVATE_KEY_WIF!);
await page.locator('#dpns-private-key-input').press('Tab');

await expect(page.locator('.key-status.success')).toContainText('Key matches key', { timeout: 30000 });
await expect(page.locator('#dpns-identity-continue-btn')).toBeEnabled();
});

test('manage identity key validation works against testnet', async ({ page }) => {
test.skip(
!LIVE_MANAGE_IDENTITY_ID || !LIVE_MANAGE_PRIVATE_KEY_WIF,
'Set PW_LIVE_MANAGE_IDENTITY_ID and PW_LIVE_MANAGE_PRIVATE_KEY_WIF to run manage live check'
);

await page.goto('/?network=testnet&e2e=live');

await page.click('#mode-manage-btn');
await page.fill('#manage-identity-id-input', LIVE_MANAGE_IDENTITY_ID!);
await page.locator('#manage-identity-id-input').press('Tab');

await expect(page.locator('.identity-status.success')).toContainText('Identity found with', { timeout: 90000 });

await page.fill('#manage-private-key-input', LIVE_MANAGE_PRIVATE_KEY_WIF!);
await page.locator('#manage-private-key-input').press('Tab');

await expect(page.locator('.key-status.success')).toContainText('MASTER level', { timeout: 30000 });
await expect(page.locator('#manage-identity-continue-btn')).toBeEnabled();
});
});
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading