From 150b8c8d8422f2609e511c3c2edf4ce03436a1ca Mon Sep 17 00:00:00 2001 From: JuliDi Date: Tue, 14 Jul 2026 14:48:09 +0200 Subject: [PATCH 1/3] Add justfile, add CI workflow Run fmt on all files --- .github/workflows/ci.yml | 66 ++++++++++++++++++++++++++++++++++++++++ .prettierignore | 10 ++++++ .prettierrc.json | 12 ++++++++ README.md | 14 ++++----- justfile | 33 ++++++++++++++++++++ svelte/README.md | 6 ++-- svelte/package.json | 4 ++- svelte/pnpm-lock.yaml | 10 ++++++ svelte/src/auth.ts | 12 ++++++-- svelte/src/fetcher.ts | 29 ++++++++++++------ svelte/src/index.ts | 30 ++++++++++++++---- 11 files changed, 198 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 justfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d4113c6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust: + name: Rust (fmt, check, test) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: cargo fmt --all --check + + - name: Check code + run: cargo check --all-features --all-targets + + - name: Run tests + run: cargo test --all-features + + frontend: + name: Frontend (prettier, check) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 11 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: svelte/pnpm-lock.yaml + + - name: Install dependencies + working-directory: svelte + run: pnpm install --frozen-lockfile + + - name: Check formatting + run: ./svelte/node_modules/.bin/prettier --check . + + - name: Check code + working-directory: svelte + run: pnpm run check diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..07ad33b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +# Rust build artifacts +/target/ + +# Node +svelte/node_modules/ +svelte/dist/ + +# Lockfiles (managed by their own tools) +svelte/pnpm-lock.yaml +Cargo.lock diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..bc676e4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "tabWidth": 4, + "singleQuote": true, + "overrides": [ + { + "files": ["*.yml", "*.yaml", "*.md"], + "options": { + "tabWidth": 2 + } + } + ] +} diff --git a/README.md b/README.md index e661ebe..6974a5a 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ OIDC building blocks for Rust services. Two independent halves, selected by feature; everything is off by default. -| Feature | Contents | -|---|---| -| `validator` | Local JWT validation against a cached, background-refreshed JWKS (resource servers). Framework-free. | -| `dropshot` | `Authed` extractor on top of `validator` for dropshot servers. | -| `bff` | axum-oidc + tower-sessions SSO stack: login flow, signed session cookie, CSRF layer, `/auth/*` routes. | -| `proxy` | Forward requests to a backend API with the session's bearer token (implies `bff`). | -| `signed-request` | HMAC-SHA256 `{timestamp}.{body}` signing/verification for inter-service calls. | +| Feature | Contents | +| ---------------- | ------------------------------------------------------------------------------------------------------ | +| `validator` | Local JWT validation against a cached, background-refreshed JWKS (resource servers). Framework-free. | +| `dropshot` | `Authed` extractor on top of `validator` for dropshot servers. | +| `bff` | axum-oidc + tower-sessions SSO stack: login flow, signed session cookie, CSRF layer, `/auth/*` routes. | +| `proxy` | Forward requests to a backend API with the session's bearer token (implies `bff`). | +| `signed-request` | HMAC-SHA256 `{timestamp}.{body}` signing/verification for inter-service calls. | ## Resource server (dropshot) diff --git a/justfile b/justfile new file mode 100644 index 0000000..3910f9a --- /dev/null +++ b/justfile @@ -0,0 +1,33 @@ +# Path to the prettier binary installed by the svelte package. +# Run `just install` first if it is missing. +prettier := "./svelte/node_modules/.bin/prettier" + +# List available recipes. +default: + @just --list + +# Install the frontend toolchain (pnpm deps, incl. prettier & tsc). +install: + cd svelte && pnpm install + +# Format everything: Rust with cargo fmt, the rest with prettier. +fmt: + cargo fmt --all + {{prettier}} --write . + +# Check formatting without modifying files (what CI runs). +fmt-check: + cargo fmt --all --check + {{prettier}} --check . + +# Type-check the code: cargo check (all features) + tsc. +check: + cargo check --all-features --all-targets + cd svelte && pnpm run check + +# Run all Rust tests (all features). +test: + cargo test --all-features + +# Run the full CI suite locally. +ci: fmt-check check test diff --git a/svelte/README.md b/svelte/README.md index 68373b3..9fe7c4d 100644 --- a/svelte/README.md +++ b/svelte/README.md @@ -19,9 +19,9 @@ One client per app, e.g. `src/lib/auth.ts`: import { createAuthClient } from '@systemscape/oidc-stack-svelte'; export const auth = createAuthClient({ - proxyPrefix: '/api/', - onProxyRejected: (url) => console.error(`backend rejected token for ${url}`), - onForbidden: () => forbiddenError.set(true) // render inline, not a toast + proxyPrefix: '/api/', + onProxyRejected: (url) => console.error(`backend rejected token for ${url}`), + onForbidden: () => forbiddenError.set(true), // render inline, not a toast }); ``` diff --git a/svelte/package.json b/svelte/package.json index 81b698d..058d565 100644 --- a/svelte/package.json +++ b/svelte/package.json @@ -29,7 +29,8 @@ }, "scripts": { "build": "tsc", - "prepare": "tsc" + "prepare": "tsc", + "check": "tsc --noEmit" }, "peerDependencies": { "svelte": "^5.0.0" @@ -40,6 +41,7 @@ } }, "devDependencies": { + "prettier": "^3.4.0", "svelte": "^5.0.0", "typescript": "^5.6.0" } diff --git a/svelte/pnpm-lock.yaml b/svelte/pnpm-lock.yaml index c7f8b36..c7756a3 100644 --- a/svelte/pnpm-lock.yaml +++ b/svelte/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + prettier: + specifier: ^3.4.0 + version: 3.9.5 svelte: specifier: ^5.0.0 version: 5.56.4 @@ -84,6 +87,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + prettier@3.9.5: + resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} + engines: {node: '>=14'} + hasBin: true + svelte@5.56.4: resolution: {integrity: sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==} engines: {node: '>=18'} @@ -151,6 +159,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + prettier@3.9.5: {} + svelte@5.56.4: dependencies: '@jridgewell/remapping': 2.3.5 diff --git a/svelte/src/auth.ts b/svelte/src/auth.ts index f8ead52..bb3317b 100644 --- a/svelte/src/auth.ts +++ b/svelte/src/auth.ts @@ -55,7 +55,7 @@ export function createAuthStore(options: AuthStoreOptions = {}): AuthStore { sub: data.sub, email: data.email ?? null, name: data.name ?? null, - groups: data.groups ?? [] + groups: data.groups ?? [], }; user.set(userData); accessDenied.set(false); @@ -91,5 +91,13 @@ export function createAuthStore(options: AuthStoreOptions = {}): AuthStore { window.location.href = clearUrl; } - return { user, authLoading, accessDenied, checkAuth, login, logout, clearSessionAndLogin }; + return { + user, + authLoading, + accessDenied, + checkAuth, + login, + logout, + clearSessionAndLogin, + }; } diff --git a/svelte/src/fetcher.ts b/svelte/src/fetcher.ts index f6f1832..3b50e1e 100644 --- a/svelte/src/fetcher.ts +++ b/svelte/src/fetcher.ts @@ -51,7 +51,9 @@ export interface AuthFetcher { */ apiFetch: ( url: string, - options?: RequestInit & { on401?: (url: string, res: Response) => void } + options?: RequestInit & { + on401?: (url: string, res: Response) => void; + }, ) => Promise; /** * Fetch function in the shape Orval expects from a `mutator`: returns @@ -65,11 +67,14 @@ export interface AuthFetcher { } /** Build the fetch helpers, wiring 401/403 handling to the given hooks. */ -export function createAuthFetcher(options: AuthFetcherOptions = {}): AuthFetcher { +export function createAuthFetcher( + options: AuthFetcherOptions = {}, +): AuthFetcher { /** Prevent multiple concurrent session-expired redirects. */ let redirecting = false; - const recoverUrl = options.recoverUrl === undefined ? '/auth/me' : options.recoverUrl; + const recoverUrl = + options.recoverUrl === undefined ? '/auth/me' : options.recoverUrl; const onSessionExpired = options.onSessionExpired ?? @@ -84,7 +89,11 @@ export function createAuthFetcher(options: AuthFetcherOptions = {}): AuthFetcher } function handle401(url: string, res: Response): void { - if (options.proxyPrefix && url.startsWith(options.proxyPrefix) && options.onProxyRejected) { + if ( + options.proxyPrefix && + url.startsWith(options.proxyPrefix) && + options.onProxyRejected + ) { options.onProxyRejected(url, res); } else { handleSessionExpired(); @@ -108,7 +117,7 @@ export function createAuthFetcher(options: AuthFetcherOptions = {}): AuthFetcher try { const res = await fetch(recoverUrl, { credentials: 'include', - redirect: 'manual' + redirect: 'manual', }); return res.status === 200; } catch { @@ -122,7 +131,9 @@ export function createAuthFetcher(options: AuthFetcherOptions = {}): AuthFetcher async function apiFetch( url: string, - fetchOptions?: RequestInit & { on401?: (url: string, res: Response) => void } + fetchOptions?: RequestInit & { + on401?: (url: string, res: Response) => void; + }, ): Promise { const { on401, ...init } = fetchOptions ?? {}; @@ -130,7 +141,7 @@ export function createAuthFetcher(options: AuthFetcherOptions = {}): AuthFetcher const res = await fetch(url, { ...init, credentials: 'include', - redirect: 'manual' + redirect: 'manual', }); if (isSessionExpiry(res)) { @@ -154,7 +165,7 @@ export function createAuthFetcher(options: AuthFetcherOptions = {}): AuthFetcher const res = await fetch(url, { ...init, credentials: 'include', - redirect: 'manual' + redirect: 'manual', }); if (isSessionExpiry(res) && !isRetry && (await recoverSession())) { return run(true); @@ -188,7 +199,7 @@ export function createAuthFetcher(options: AuthFetcherOptions = {}): AuthFetcher return { data, status: res.status, - headers: res.headers + headers: res.headers, } as T; } diff --git a/svelte/src/index.ts b/svelte/src/index.ts index a5fde01..9537cae 100644 --- a/svelte/src/index.ts +++ b/svelte/src/index.ts @@ -5,15 +5,32 @@ * everywhere; `customFetch` is shaped for use as an Orval mutator. */ -import { createAuthFetcher, type AuthFetcher, type AuthFetcherOptions } from './fetcher.js'; -import { createAuthStore, type AuthStore, type AuthStoreOptions } from './auth.js'; +import { + createAuthFetcher, + type AuthFetcher, + type AuthFetcherOptions, +} from './fetcher.js'; +import { + createAuthStore, + type AuthStore, + type AuthStoreOptions, +} from './auth.js'; export type { AuthUser } from './types.js'; export { hasGroup } from './types.js'; -export { createAuthFetcher, type AuthFetcher, type AuthFetcherOptions } from './fetcher.js'; -export { createAuthStore, type AuthStore, type AuthStoreOptions } from './auth.js'; +export { + createAuthFetcher, + type AuthFetcher, + type AuthFetcherOptions, +} from './fetcher.js'; +export { + createAuthStore, + type AuthStore, + type AuthStoreOptions, +} from './auth.js'; -export interface AuthClientOptions extends AuthFetcherOptions, AuthStoreOptions {} +export interface AuthClientOptions + extends AuthFetcherOptions, AuthStoreOptions {} export type AuthClient = AuthStore & AuthFetcher; @@ -25,7 +42,8 @@ export function createAuthClient(options: AuthClientOptions = {}): AuthClient { const store = createAuthStore(options); const fetcher = createAuthFetcher({ ...options, - onSessionExpired: options.onSessionExpired ?? (() => store.clearSessionAndLogin()) + onSessionExpired: + options.onSessionExpired ?? (() => store.clearSessionAndLogin()), }); return { ...store, ...fetcher }; } From cc10f4f96d20041c49711395a845640643f740b9 Mon Sep 17 00:00:00 2001 From: JuliDi Date: Tue, 14 Jul 2026 14:51:49 +0200 Subject: [PATCH 2/3] Bump ci workflow actions verions --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4113c6..71bb846 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: name: Rust (fmt, check, test) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -40,15 +40,15 @@ jobs: name: Frontend (prettier, check) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: version: 11 - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: 22 cache: pnpm From daef214b63c9ba05be08419a390c847d58a3c0e7 Mon Sep 17 00:00:00 2001 From: JuliDi Date: Tue, 14 Jul 2026 14:55:45 +0200 Subject: [PATCH 3/3] Fix https://github.com/pnpm/action-setup/issues/276 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71bb846..ea8cedb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v6 with: - version: 11 + version: 11.13 - name: Install Node.js uses: actions/setup-node@v7