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
24 changes: 23 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ on:

permissions:
contents: read
deployments: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
permissions:
contents: read
deployments: write
# Match the OS supported by the pinned Playwright version.
runs-on: ubuntu-24.04
steps:
Expand Down Expand Up @@ -70,3 +72,23 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy ./out --project-name=connectbot-website
gitHubToken: ${{ secrets.GITHUB_TOKEN }}

webauthn:
permissions:
contents: read
deployments: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Validate WebAuthn association
run: python3 scripts/check-webauthn.py
- name: Package WebAuthn site
run: python3 scripts/prepare-webauthn.py
- name: Deploy WebAuthn association
if: github.ref == 'refs/heads/develop' && github.repository == 'connectbot/connectbot.github.io' && github.event_name == 'push'
uses: cloudflare/wrangler-action@953926a2e2182532811c01a25e53647d93bf07c0 # v4.1.3
with:
apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy ./local/webauthn --project-name=connectbot-webauthn --branch=main
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
6 changes: 4 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@ pnpm run analyze

## Deployment

The site is automatically deployed to GitHub Pages when changes are pushed to the `develop` branch. The CI workflow:
The site is automatically deployed to Cloudflare Pages when changes are pushed to the `develop` branch. The CI workflow:
1. Runs linting and type checking
2. Builds the static site
3. Deploys to GitHub Pages
3. Deploys to Cloudflare Pages

You can preview the production build locally by serving the `./out` directory:
```bash
npx serve ./out
```

See [WebAuthn hosting](webauthn.md) for the independent passkey association deployment.
13 changes: 13 additions & 0 deletions public/brand.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
:root {
--site-background: #fafbfc;
--site-text: #1e293b;
--site-link-color: #1d4ed8;
--site-link-hover-color: #1e40af;
}

.dark {
--site-background: #0f172a;
--site-text: #e2e8f0;
--site-link-color: #93c5fd;
--site-link-hover-color: #bfdbfe;
}
29 changes: 29 additions & 0 deletions scripts/check-webauthn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Validate the release identities published by the WebAuthn RP domain."""
import hashlib
import json
from pathlib import Path
import ssl

ROOT = Path(__file__).resolve().parents[1]
CERTIFICATES = (
'connectbot-app-signing-original.pem',
'connectbot-play-app-signing-intermediate.pem',
'connectbot-app-signing-2026.pem',
)
fingerprints = []
for certificate in CERTIFICATES:
pem = (ROOT / 'public/certificates' / certificate).read_text()
digest = hashlib.sha256(ssl.PEM_cert_to_DER_cert(pem)).hexdigest().upper()
fingerprints.append(':'.join(digest[i:i + 2] for i in range(0, 64, 2)))
expected = [{
'relation': ['delegate_permission/common.get_login_creds'],
'target': {
'namespace': 'android_app',
'package_name': 'org.connectbot',
'sha256_cert_fingerprints': fingerprints,
},
}]
actual = json.loads((ROOT / 'webauthn/.well-known/assetlinks.json').read_text())
if actual != expected:
raise SystemExit('WebAuthn association must match the three official release certificates')
print('WebAuthn association matches all three official release certificates')
14 changes: 14 additions & 0 deletions scripts/prepare-webauthn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Package the static RP site with canonical shared brand assets."""
from pathlib import Path
import shutil

root = Path(__file__).resolve().parents[1]
out = root / 'local/webauthn'
if out.exists():
shutil.rmtree(out)
shutil.copytree(root / 'webauthn', out)
for asset in ('brand.css', 'logo.png', 'fonts/logo-font.woff2'):
target = out / asset
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(root / 'public' / asset, target)
print(f'Prepared {out}')
5 changes: 1 addition & 4 deletions src/app/styles.css
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
@import 'nextra-theme-docs/style.css';
@import '../../public/brand.css';

:root {
--site-header-height: var(--nextra-navbar-height);
--site-link-color: #1d4ed8;
--site-link-hover-color: #1e40af;
--x-color-primary-400: #93c5fd;
--x-color-primary-600: var(--site-link-color);
}

.dark {
--site-link-color: #93c5fd;
--site-link-hover-color: #bfdbfe;
--x-color-primary-600: var(--site-link-color);
}

Expand Down
16 changes: 16 additions & 0 deletions webauthn/.well-known/assetlinks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "org.connectbot",
"sha256_cert_fingerprints": [
"AA:43:74:6A:46:12:78:42:16:9F:81:76:41:2A:1F:4B:1B:1E:2F:8A:D9:E1:5A:BA:21:7C:38:22:DB:EB:2D:5F",
"2F:EF:9C:FE:29:BE:E1:93:20:61:A8:1B:FB:B8:B5:7C:5C:65:88:54:B8:2C:C9:7B:BF:26:52:65:DD:B2:27:7C",
"D2:49:13:B6:92:DB:31:A9:29:CD:30:AE:86:18:DF:97:71:88:82:B6:EA:A1:ED:6E:12:4B:C4:3D:02:8E:7F:BE"
]
}
}
]
16 changes: 16 additions & 0 deletions webauthn/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page not found | ConnectBot</title>
<link rel="icon" href="/logo.png">
<link rel="stylesheet" href="/brand.css">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<header><a class="brand" href="https://connectbot.org/"><img src="/logo.png" alt="" width="40" height="40"><span>ConnectBot</span></a></header>
<main><h1>Page not found</h1><p>The page you requested could not be found.</p><p><a href="https://connectbot.org/">Visit ConnectBot →</a></p></main>
<footer>Open-source SSH and Mosh client for Android</footer>
</body>
</html>
2 changes: 2 additions & 0 deletions webauthn/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.well-known/assetlinks.json
Content-Type: application/json
16 changes: 16 additions & 0 deletions webauthn/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebAuthn association | ConnectBot</title>
<link rel="icon" href="/logo.png">
<link rel="stylesheet" href="/brand.css">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<header><a class="brand" href="https://connectbot.org/"><img src="/logo.png" alt="" width="40" height="40"><span>ConnectBot</span></a></header>
<main><h1>WebAuthn association</h1><p>This domain publishes the Android app association for ConnectBot’s passkey support.</p><div class="notice"><p>This site does not receive or store your private keys. Passkeys are managed by your selected credential provider.</p></div><p><a href="https://connectbot.org/">Visit ConnectBot →</a></p></main>
<footer>Open-source SSH and Mosh client for Android</footer>
</body>
</html>
2 changes: 2 additions & 0 deletions webauthn/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
82 changes: 82 additions & 0 deletions webauthn/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
@font-face {
font-family: ConnectBot;
src: url('/fonts/logo-font.woff2') format('woff2');
font-weight: 100;
font-display: optional;
}

* {
box-sizing: border-box;
}
html {
color-scheme: dark;
}
body {
margin: 0;
background: var(--site-background);
color: var(--site-text);
font:
1rem/1.65 Arial,
sans-serif;
}
header,
main,
footer {
max-width: 48rem;
margin: auto;
padding: 1.5rem;
}
header {
border-bottom: 1px solid #8884;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.75rem;
color: inherit;
text-decoration: none;
font:
100 1.5rem/1 ConnectBot,
Arial,
sans-serif;
}
.brand img {
width: 40px;
height: 40px;
}
main {
padding-block: clamp(2rem, 8vw, 5rem);
}
h1 {
font-size: clamp(1.75rem, 5vw, 2.5rem);
line-height: 1.2;
letter-spacing: -0.025em;
}
p {
max-width: 38rem;
}
.notice {
margin-block: 2rem;
padding: 1.25rem;
border: 1px solid #8884;
border-radius: 0.875rem;
background: #8881;
}
.notice p {
margin: 0;
}
a {
color: var(--site-link-color);
text-underline-offset: 0.25em;
}
a:hover {
color: var(--site-link-hover-color);
}
a:focus-visible {
outline: 3px solid #438be5;
outline-offset: 4px;
}
footer {
border-top: 1px solid #8884;
font-size: 0.875rem;
}