Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ jobs:
# (e.g. ESM/CJS interop regressions in new deps).
run: npm run build

- name: Install browser for recovery-alert layout checks
if: matrix.node == '22'
run: npx playwright install --with-deps chromium

- name: Check recovery-alert browser flow and small windows
if: matrix.node == '22'
run: npm run test:recovery-browser

- name: Preserve recovery-alert screenshots and failure traces
if: always() && matrix.node == '22'
uses: actions/upload-artifact@v4
with:
name: recovery-alert-browser
path: test-results/sentry/
if-no-files-found: ignore

deploy:
name: Deploy production
needs: [test]
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ https://sysnode.info/gov/* -> backend
https://sysnode.info/mnstats -> backend
https://sysnode.info/mncount -> backend
https://sysnode.info/govlist -> backend
https://sysnode.info/api/recovery/* -> backend
```

The backend aggregates data from a Syscoin Core node, Sentry Node RPC responses, market APIs, and supporting network datasets. For a fork or private production deployment, keep the same-origin shape and point the reverse proxy at that deployment's backend. This keeps the CSP `connect-src` policy, host-only cookies, and CSRF model in lockstep.
Expand All @@ -51,6 +52,25 @@ REACT_APP_API_BASE=https://your-backend.example npm start

The value is read at build time by both `src/lib/apiClient.js` (authenticated surface) and `src/lib/api.js` (anonymous surface — superblock timing, governance feed, masternode stats) outside production. Without it, development requests use `http://localhost:3001`; production requests use same-origin relative paths. Keeping the API under the SPA origin preserves host-only `Secure; SameSite=Lax` cookies and lets the SPA mirror the CSRF cookie into `X-CSRF-Token` without cross-site credentialed fetches.

## Recovery email alerts

Open **Account → Recovery alerts**, connect a deployed smart account on a
supported network, and sign the enrollment message in the wallet. Alerts go to
the signed-in user's verified email; private keys never leave the wallet.
The backend verifies the account's ERC-1271 signature and monitors independently
of this page being open. The initial supported chain is zkTanenbaum (57057).

Check the displayed monitoring status and on-chain execution time. Delivery is
best effort; a delayed monitor is not evidence that an account is safe. Email
links open the review page, never authorize cancellation or a transaction.
Disabling an alert subscription requires the current Sysnode password and TOTP
when configured; it does not disable the on-chain guardian policy.

The public `/api/recovery/` prefix uses the existing `/api/` reverse proxy.
Backends also accept `/recovery/` when that proxy strips `/api`, while local
development can use `/api/recovery/` directly. Keep both on the same origin as
the frontend and do not replace the credentialed cookie/CSRF boundary.

## Getting Started

Install dependencies:
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"test": "react-scripts test",
"test:ci": "react-scripts test --watchAll=false --maxWorkers=50%",
"test:sentry-browser": "playwright test --config playwright.config.cjs",
"test:recovery-browser": "playwright test --config playwright.config.cjs tests/browser/recovery-alerts.spec.cjs",
"test:production-runtime": "node --test tests/productionRuntime.test.cjs",
"test:deployment": "node --test tests/deploymentRequest.test.cjs",
"eject": "react-scripts eject"
Expand Down
2 changes: 2 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -2010,6 +2010,8 @@

.auth-kv__row dd {
margin: 0;
min-width: 0;
overflow-wrap: anywhere;
font-weight: 500;
color: var(--text);
}
Expand Down
180 changes: 180 additions & 0 deletions src/components/RecoveryAlertsCard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
.recovery-alerts {
scroll-margin-top: 100px;
min-width: 0;
}
.recovery-alerts h2 {
display: flex;
align-items: center;
gap: 10px;
}
.recovery-alerts h2 svg {
width: 22px;
height: 22px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
flex: 0 0 auto;
}
.recovery-alerts h3 {
margin: 0;
font-size: 1rem;
font-weight: 650;
}
.recovery-alerts .auth-input {
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.recovery-alerts .auth-alert {
overflow-wrap: anywhere;
}
.recovery-email {
overflow-wrap: anywhere;
}
.recovery-note {
margin: 0;
color: var(--muted);
font-size: 0.82rem;
line-height: 1.6;
}
.recovery-subscriptions {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}
.recovery-subscription,
.recovery-remove {
display: flex;
flex-direction: column;
gap: 12px;
border: 1px solid var(--panel-outline);
border-radius: 16px;
padding: 18px;
min-width: 0;
}
.recovery-subscription__heading {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.recovery-subscription__heading h3 {
overflow-wrap: anywhere;
flex: 1 1 130px;
}
.recovery-subscription .status-chip {
font-size: 0.72rem;
}
.recovery-address {
display: flex;
align-items: flex-start;
gap: 8px;
min-width: 0;
}
.recovery-address code {
overflow-wrap: anywhere;
word-break: break-all;
font-size: 0.8rem;
line-height: 1.7;
min-width: 0;
}
.recovery-copy {
flex: 0 0 auto;
min-height: 36px;
border: 1px solid var(--panel-outline);
color: var(--muted);
background: transparent;
border-radius: 7px;
padding: 4px 6px;
cursor: pointer;
font: inherit;
font-size: 0.74rem;
}
.recovery-copy:hover,
.recovery-copy:focus-visible {
background: var(--accent-soft);
color: var(--text);
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.recovery-operation {
display: flex;
flex-direction: column;
gap: 6px;
border-left: 3px solid var(--accent);
padding: 8px 0 8px 12px;
font-size: 0.85rem;
line-height: 1.5;
overflow-wrap: anywhere;
}
.recovery-empty {
margin: 0;
padding: 16px;
border: 1px dashed var(--panel-outline);
border-radius: 14px;
color: var(--muted);
font-size: 0.9rem;
line-height: 1.6;
}
.recovery-enroll {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}
.recovery-network {
margin: 0;
color: var(--text);
font-size: 0.9rem;
font-weight: 600;
}
.recovery-link-details summary {
cursor: pointer;
font-weight: 600;
line-height: 1.6;
padding: 10px 0;
color: var(--text);
}
.recovery-link-details summary:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
border-radius: 5px;
}
.recovery-link-details[open] summary {
margin-bottom: 12px;
}
.recovery-disable,
.recovery-refresh {
align-self: flex-start;
text-align: left;
line-height: 1.6;
}
.recovery-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.recovery-actions .button {
flex: 1 1 140px;
min-width: 0;
}
.recovery-alerts .button {
white-space: normal;
min-width: 0;
}
.recovery-account-select {
font-size: 0.75rem;
}
@media (max-width: 400px) {
.recovery-subscription,
.recovery-remove {
padding: 14px;
}
.recovery-address {
flex-wrap: wrap;
}
.recovery-address code {
flex-basis: 100%;
}
}
Loading
Loading