chore(auth): redirect sign-in/sign-up to app.cameleer.io
All checks were successful
ci / build-test (push) Successful in 3m41s
ci / build-test (pull_request) Successful in 4m12s

Both auth flows now navigate to the app domain rather than the
auth.cameleer.io subdomain:

  PUBLIC_AUTH_SIGNIN_URL → https://app.cameleer.io/sign-in
  PUBLIC_AUTH_SIGNUP_URL → https://app.cameleer.io/sign-in?first_screen=register

Updated:
- .env.example (the canonical reference values)
- OPERATOR-CHECKLIST.md (deploy-time secret values)
- src/config/auth.test.ts (test fixtures)
- src/middleware.ts (CSP-comment about <a> navigation target)
- src/pages/privacy.astro (visitor-facing external-links section
  in §6 of the privacy policy)

The auth.ts validator stays strict-https — the new URLs are still
absolute https URLs, just on a different host.  Logto itself may
still run at auth.cameleer.io as the OIDC backend; only the
visitor-facing /sign-in entry point moved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-25 09:28:02 +02:00
parent 203e4bfb41
commit fa12df8ec6
5 changed files with 20 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
# Logto auth endpoints — the marketing site only performs <a href> navigations to these. # Logto auth endpoints — the marketing site only performs <a href> navigations to these.
# No tokens, no cookies, no XHR — these are plain hyperlinks. # No tokens, no cookies, no XHR — these are plain hyperlinks.
PUBLIC_AUTH_SIGNIN_URL=https://auth.cameleer.io/sign-in PUBLIC_AUTH_SIGNIN_URL=https://app.cameleer.io/sign-in
PUBLIC_AUTH_SIGNUP_URL=https://auth.cameleer.io/sign-in?first_screen=register PUBLIC_AUTH_SIGNUP_URL=https://app.cameleer.io/sign-in?first_screen=register
PUBLIC_SALES_EMAIL=sales@cameleer.io PUBLIC_SALES_EMAIL=sales@cameleer.io

View File

@@ -75,8 +75,8 @@ Add these under Repository settings → Actions → Secrets (or variables):
| `SFTP_PATH` | secret | Absolute path to the Apache vhost docroot configured in konsoleH (typically `/usr/www/users/<login>/public_html`). Mismatch → 404 on origin. | | `SFTP_PATH` | secret | Absolute path to the Apache vhost docroot configured in konsoleH (typically `/usr/www/users/<login>/public_html`). Mismatch → 404 on origin. |
| `SFTP_KEY` | secret | Contents of `~/.ssh/cameleer-website-deploy` (private key, PEM) | | `SFTP_KEY` | secret | Contents of `~/.ssh/cameleer-website-deploy` (private key, PEM) |
| `SFTP_KNOWN_HOSTS` | secret | Contents of `hetzner-known-hosts.txt` (captured via `ssh-keyscan`) | | `SFTP_KNOWN_HOSTS` | secret | Contents of `hetzner-known-hosts.txt` (captured via `ssh-keyscan`) |
| `PUBLIC_AUTH_SIGNIN_URL` | secret | `https://auth.cameleer.io/sign-in` | | `PUBLIC_AUTH_SIGNIN_URL` | secret | `https://app.cameleer.io/sign-in` |
| `PUBLIC_AUTH_SIGNUP_URL` | secret | `https://auth.cameleer.io/sign-in?first_screen=register` | | `PUBLIC_AUTH_SIGNUP_URL` | secret | `https://app.cameleer.io/sign-in?first_screen=register` |
| `PUBLIC_SALES_EMAIL` | secret | `sales@cameleer.io` (or whatever sales alias you set up) | | `PUBLIC_SALES_EMAIL` | secret | `sales@cameleer.io` (or whatever sales alias you set up) |
These three are not actually secret (they end up in the built HTML), but Gitea's These three are not actually secret (they end up in the built HTML), but Gitea's

View File

@@ -4,57 +4,57 @@ import { resolveAuthConfig } from './auth';
describe('resolveAuthConfig', () => { describe('resolveAuthConfig', () => {
it('returns both URLs and sales email from env', () => { it('returns both URLs and sales email from env', () => {
const cfg = resolveAuthConfig({ const cfg = resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
}); });
expect(cfg.signInUrl).toBe('https://auth.cameleer.io/sign-in'); expect(cfg.signInUrl).toBe('https://app.cameleer.io/sign-in');
expect(cfg.signUpUrl).toBe('https://auth.cameleer.io/sign-in?first_screen=register'); expect(cfg.signUpUrl).toBe('https://app.cameleer.io/sign-in?first_screen=register');
expect(cfg.salesEmail).toBe('sales@cameleer.io'); expect(cfg.salesEmail).toBe('sales@cameleer.io');
}); });
it('throws if PUBLIC_AUTH_SIGNIN_URL is missing', () => { it('throws if PUBLIC_AUTH_SIGNIN_URL is missing', () => {
expect(() => resolveAuthConfig({ expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/PUBLIC_AUTH_SIGNIN_URL/); })).toThrow(/PUBLIC_AUTH_SIGNIN_URL/);
}); });
it('throws if a URL is not https', () => { it('throws if a URL is not https', () => {
expect(() => resolveAuthConfig({ expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'http://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'http://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/must be https/); })).toThrow(/must be https/);
}); });
it('throws if sales email is not a valid mailto target', () => { it('throws if sales email is not a valid mailto target', () => {
expect(() => resolveAuthConfig({ expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'not-an-email', PUBLIC_SALES_EMAIL: 'not-an-email',
})).toThrow(/PUBLIC_SALES_EMAIL/); })).toThrow(/PUBLIC_SALES_EMAIL/);
}); });
it('throws if PUBLIC_AUTH_SIGNUP_URL is missing', () => { it('throws if PUBLIC_AUTH_SIGNUP_URL is missing', () => {
expect(() => resolveAuthConfig({ expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/PUBLIC_AUTH_SIGNUP_URL/); })).toThrow(/PUBLIC_AUTH_SIGNUP_URL/);
}); });
it('throws if PUBLIC_AUTH_SIGNUP_URL is not https', () => { it('throws if PUBLIC_AUTH_SIGNUP_URL is not https', () => {
expect(() => resolveAuthConfig({ expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'http://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'http://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/must be https/); })).toThrow(/must be https/);
}); });
it('exposes signUpUrl distinct from signInUrl', () => { it('exposes signUpUrl distinct from signInUrl', () => {
const cfg = resolveAuthConfig({ const cfg = resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
}); });
expect(cfg.signUpUrl).not.toBe(cfg.signInUrl); expect(cfg.signUpUrl).not.toBe(cfg.signInUrl);

View File

@@ -20,7 +20,7 @@ export function buildSecurityHeaders(): Record<string, string> {
"connect-src 'self'", "connect-src 'self'",
"frame-ancestors 'none'", "frame-ancestors 'none'",
"base-uri 'self'", "base-uri 'self'",
// No forms on this marketing site today (all auth redirects go to auth.cameleer.io // No forms on this marketing site today (all auth redirects go to app.cameleer.io
// as plain <a> navigations). If a future form is added, relax to 'self' or an allow-list. // as plain <a> navigations). If a future form is added, relax to 'self' or an allow-list.
"form-action 'none'", "form-action 'none'",
"object-src 'none'", "object-src 'none'",

View File

@@ -67,7 +67,7 @@ const lastUpdated = '2026-04-24';
<section class="mb-10"> <section class="mb-10">
<h2 class="text-lg font-bold text-text mb-3">6. External links</h2> <h2 class="text-lg font-bold text-text mb-3">6. External links</h2>
<p class="text-text-muted leading-relaxed"> <p class="text-text-muted leading-relaxed">
Sign-in and sign-up links on this site navigate you to <span class="font-mono text-accent">auth.cameleer.io</span> (Logto identity service) and subsequently <span class="font-mono text-accent">platform.cameleer.io</span>. Those services have their own privacy policies, which apply from the moment you arrive there. Sign-in and sign-up links on this site navigate you to <span class="font-mono text-accent">app.cameleer.io</span> (the Cameleer app, where authentication is handled by Logto). That service has its own privacy policy, which applies from the moment you arrive there.
</p> </p>
</section> </section>