diff --git a/.env.example b/.env.example
deleted file mode 100644
index b4214c8..0000000
--- a/.env.example
+++ /dev/null
@@ -1,6 +0,0 @@
-# Auth0 SPA configuration.
-# These are PUBLIC, non-secret identifiers (they ship to the browser in the
-# Authorization Code + PKCE flow). Copy this file to `.env` for local dev, and
-# set the same variables in your Cloudflare build environment for deploys.
-PUBLIC_AUTH0_DOMAIN=id.letsbuilda.dev
-PUBLIC_AUTH0_CLIENT_ID=THoRnVJxMOfLxMAW0kaNW8nuP1q8q3qQ
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
index 5347c62..5263e55 100644
--- a/.github/dependabot.yaml
+++ b/.github/dependabot.yaml
@@ -19,5 +19,15 @@ updates:
javascript-dependencies:
patterns:
- "*"
+ # @astrojs/check declares a peer range of typescript ^5 || ^6, so
+ # TypeScript 7+ breaks `astro check`. Keep it out of the group so one
+ # incompatible major does not fail the whole grouped update.
+ exclude-patterns:
+ - typescript
+ ignore:
+ # @types/node follows the Node major pinned in .nvmrc (24).
+ - dependency-name: "@types/node"
+ update-types:
+ - version-update:semver-major
cooldown:
default-days: 7
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index de57cb1..7aa531c 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -1,26 +1,61 @@
name: CI
on:
+ pull_request:
+ # Docs-only changes skip CI; nothing here reads Markdown.
+ paths-ignore:
+ - "**/*.md"
+ merge_group:
push:
branches:
- main
- pull_request:
permissions:
contents: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ # Cancel superseded PR runs only; every merge to main runs in full.
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
jobs:
- biome:
- name: Biome
+ lint:
runs-on: ubuntu-latest
+ timeout-minutes: 5
+
steps:
- - name: Checkout
+ - name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- - name: Setup Biome
+ - name: Set up Biome
uses: biomejs/setup-biome@4c91541eaada48f67d7dbd7833600ce162b68f51 # v2.7.1
- - name: Run Biome
+ - name: Lint
run: biome ci . --reporter=github
+
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Set up Node.js
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version-file: .nvmrc
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Type check
+ run: npm run typecheck
+
+ - name: Build
+ run: npm run build
diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml
new file mode 100644
index 0000000..a932ce7
--- /dev/null
+++ b/.github/workflows/dependency-review.yaml
@@ -0,0 +1,24 @@
+name: Dependency Review
+
+on:
+ pull_request:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ dependency-review:
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Review dependency changes
+ uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
diff --git a/.gitignore b/.gitignore
index 28f22ee..da233d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,26 +1,20 @@
# build output
dist/
-.wrangler/
-# generated types
.astro/
-worker-configuration.d.ts
+.wrangler/
# dependencies
node_modules/
# logs
npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-
# environment variables
.env
-.env.production
+.env.*
+.dev.vars*
-# macOS-specific files
+# editor and OS files
.DS_Store
-
-# jetbrains setting folder
.idea/
+.vscode/
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..d9ca886
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+engine-strict=true
+save-exact=true
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..248216a
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+24.12.0
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..1113520
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,8 @@
+# Agent notes
+
+- Static Astro site, no adapter and no UI framework. Page behavior is plain `
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 3802978..9d250fe 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -4,87 +4,69 @@ import {
type User,
} from "@auth0/auth0-spa-js";
-// Both values are public, non-secret identifiers for this SPA's Authorization
-// Code + PKCE flow. They are inlined at build time from PUBLIC_* environment
-// variables (see .env.example).
-const domain = import.meta.env.PUBLIC_AUTH0_DOMAIN;
-const clientId = import.meta.env.PUBLIC_AUTH0_CLIENT_ID;
+// Public identifiers for this single-page app; neither is a secret. The tenant
+// is served from a custom domain on the same site as letsbuilda.dev, so the
+// SDK can restore a session from Auth0's cookie on each page load without
+// keeping tokens in localStorage.
+const AUTH0_DOMAIN = "id.letsbuilda.dev";
+const AUTH0_CLIENT_ID = "THoRnVJxMOfLxMAW0kaNW8nuP1q8q3qQ";
-if (!domain || !clientId) {
- throw new Error(
- "Missing Auth0 configuration. Set PUBLIC_AUTH0_DOMAIN and PUBLIC_AUTH0_CLIENT_ID.",
- );
-}
-
-// State carried across the login redirect so we can return the user to the
-// page they started from (Auth0 always redirects back to the origin).
+// Carried across the login redirect so the visitor lands back on the page
+// they started from (Auth0 always redirects to the site root).
interface AppState {
returnTo?: string;
}
let clientPromise: Promise | null = null;
-// Finish the login redirect if this page received it, then forward the user to
-// wherever they began. Safe to call on every page load.
+// Finish a login redirect if this page load is one, then send the visitor on
+// to where they started. A failed or cancelled login comes back as `?error=`
+// and leaves them signed out on the home page.
async function handleRedirect(client: Auth0Client): Promise {
- const query = window.location.search;
- if (!query.includes("code=") || !query.includes("state=")) {
+ const params = new URLSearchParams(window.location.search);
+ if (!params.has("state") || !(params.has("code") || params.has("error"))) {
return;
}
- const { appState } = await client.handleRedirectCallback();
- window.history.replaceState({}, document.title, window.location.pathname);
-
- const returnTo = appState?.returnTo;
- if (returnTo && returnTo !== window.location.pathname) {
- window.location.assign(returnTo);
+ try {
+ const { appState } = await client.handleRedirectCallback();
+ const returnTo = appState?.returnTo;
+ if (returnTo?.startsWith("/") && returnTo !== window.location.pathname) {
+ window.location.replace(returnTo);
+ return;
+ }
+ } catch (error) {
+ console.error("Login failed", error);
}
+ window.history.replaceState({}, document.title, window.location.pathname);
}
-// Create the client once per page load and finish any login redirect before the
-// promise resolves, so every caller observes the post-login state regardless of
-// the order in which page scripts run.
async function bootstrap(): Promise {
+ // Tokens stay in memory (the SDK default). Creating the client restores an
+ // existing session silently from Auth0's session cookie.
const client = await createAuth0Client({
- domain,
- clientId,
- // Persist the session across this multi-page site's full page reloads and
- // renew it with rotating refresh tokens instead of the hidden-iframe flow
- // that modern browsers increasingly block.
- useRefreshTokens: true,
- cacheLocation: "localstorage",
+ domain: AUTH0_DOMAIN,
+ clientId: AUTH0_CLIENT_ID,
authorizationParams: {
redirect_uri: window.location.origin,
- scope: "openid profile email",
},
});
-
await handleRedirect(client);
return client;
}
+// One client per page load, created on first use.
function getClient(): Promise {
- if (!clientPromise) {
- clientPromise = bootstrap();
- }
+ clientPromise ??= bootstrap();
return clientPromise;
}
-// Initialise auth for the current page (processing any login redirect) and
-// report whether the visitor is logged in. Call once per page from the layout.
-export async function initPage(): Promise {
- const client = await getClient();
- return client.isAuthenticated();
-}
-
export async function isAuthenticated(): Promise {
- const client = await getClient();
- return client.isAuthenticated();
+ return (await getClient()).isAuthenticated();
}
export async function getUser(): Promise {
- const client = await getClient();
- return client.getUser();
+ return (await getClient()).getUser();
}
// Redirect to Auth0 to log in, returning to `returnTo` afterwards.
@@ -95,7 +77,6 @@ export async function login(
await client.loginWithRedirect({ appState: { returnTo } });
}
-// Log out and return to the site root.
export async function logout(): Promise {
const client = await getClient();
await client.logout({ logoutParams: { returnTo: window.location.origin } });
diff --git a/src/pages/account.astro b/src/pages/account.astro
index 2388fb1..0f01a5f 100644
--- a/src/pages/account.astro
+++ b/src/pages/account.astro
@@ -7,7 +7,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
-
+ Your account
- Email
diff --git a/tsconfig.json b/tsconfig.json
index 056e310..cef5a78 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,11 +1,5 @@
{
"extends": "astro/tsconfigs/strictest",
- "compilerOptions": {
- "paths": {
- "@/*": ["./src/*"]
- },
- "types": ["./worker-configuration.d.ts", "node"],
- "jsx": "react-jsx",
- "jsxImportSource": "react"
- }
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
}
diff --git a/wrangler.jsonc b/wrangler.jsonc
index fa4db8c..a40ba67 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -1,23 +1,11 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "letsbuilda-dev",
- "main": "@astrojs/cloudflare/entrypoints/server",
- "compatibility_date": "2026-04-10",
- "compatibility_flags": ["nodejs_compat"],
- "version_metadata": {
- "binding": "CF_VERSION_METADATA"
- },
+ "compatibility_date": "2026-10-01",
+ // A static site: the Worker only serves the files Astro builds into dist/.
"assets": {
- "binding": "ASSETS",
"directory": "./dist"
},
- "observability": {
- "enabled": true
- },
- "placement": {
- "mode": "smart"
- },
- "upload_source_maps": true,
"routes": [
{
"pattern": "letsbuilda.dev",