diff --git a/skills/workos-authkit-sveltekit/SKILL.md b/skills/workos-authkit-sveltekit/SKILL.md new file mode 100644 index 0000000..59f4731 --- /dev/null +++ b/skills/workos-authkit-sveltekit/SKILL.md @@ -0,0 +1,160 @@ +--- +name: workos-authkit-sveltekit +description: Integrate WorkOS AuthKit with SvelteKit. Server-side authentication with hooks and file-based routing. +--- + +# WorkOS AuthKit for SvelteKit + +## Step 1: Fetch SDK Documentation (BLOCKING) + +**STOP. Do not proceed until complete.** + +WebFetch: `https://github.com/workos/authkit-sveltekit/blob/main/README.md` + +The README is the source of truth. If this skill conflicts with README, follow README. + +## Step 2: Pre-Flight Validation + +### Project Structure + +- Confirm `svelte.config.js` (or `svelte.config.ts`) exists +- Confirm `package.json` contains `@sveltejs/kit` dependency +- Confirm `src/routes/` directory exists + +### Environment Variables + +Check `.env` or `.env.local` for: + +- `WORKOS_API_KEY` - starts with `sk_` +- `WORKOS_CLIENT_ID` - starts with `client_` +- `WORKOS_REDIRECT_URI` - valid callback URL +- `WORKOS_COOKIE_PASSWORD` - 32+ characters + +SvelteKit uses `$env/static/private` and `$env/dynamic/private` natively. The agent should write env vars to `.env` (SvelteKit's default) or `.env.local`. + +## Step 3: Install SDK + +Detect package manager, install SDK package from README. + +``` +pnpm-lock.yaml? → pnpm add @workos-inc/authkit-sveltekit +yarn.lock? → yarn add @workos-inc/authkit-sveltekit +bun.lockb? → bun add @workos-inc/authkit-sveltekit +else → npm install @workos-inc/authkit-sveltekit +``` + +**Verify:** SDK package exists in node_modules before continuing. + +## Step 4: Configure Server Hooks + +SvelteKit uses `src/hooks.server.ts` for server-side middleware. This is where the AuthKit handler is registered. + +Create or update `src/hooks.server.ts` with the authkit handle function from the README. + +### Existing Hooks (IMPORTANT) + +If `src/hooks.server.ts` already exists with custom logic, use SvelteKit's `sequence()` helper to compose hooks: + +```typescript +import { sequence } from '@sveltejs/kit/hooks'; +import { authkitHandle } from '@workos-inc/authkit-sveltekit'; // Check README for exact export + +export const handle = sequence(authkitHandle, yourExistingHandle); +``` + +Check README for the exact export name and usage pattern. + +## Step 5: Create Callback Route + +Parse `WORKOS_REDIRECT_URI` to determine route path: + +``` +URI path --> Route location +/callback --> src/routes/callback/+server.ts +/auth/callback --> src/routes/auth/callback/+server.ts +``` + +Use the SDK's callback handler from the README. Do not write custom OAuth logic. + +**Critical:** SvelteKit uses `+server.ts` for API routes, not `+page.server.ts`. + +## Step 6: Layout Setup + +Update `src/routes/+layout.server.ts` to load the auth session and pass it to all pages. + +Check README for the exact pattern — typically a `load` function that returns the user session from locals. + +```typescript +// src/routes/+layout.server.ts +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async (event) => { + // Check README for exact API — session is typically on event.locals + return { + user: event.locals.user, // or similar from README + }; +}; +``` + +## Step 7: UI Integration + +Add auth UI to `src/routes/+page.svelte` using the session data from the layout. + +- Show user info when authenticated +- Show sign-in link/button when not authenticated +- Add sign-out functionality + +Check README for sign-in URL generation and sign-out patterns. + +## Verification Checklist (ALL MUST PASS) + +Run these commands to confirm integration. **Do not mark complete until all pass:** + +```bash +# 1. Check hooks.server.ts exists and has authkit +grep -i "workos\|authkit" src/hooks.server.ts || echo "FAIL: authkit missing from hooks.server.ts" + +# 2. Check callback route exists +find src/routes -name "+server.ts" -path "*/callback/*" + +# 3. Check layout loads auth session +grep -i "user\|auth\|session" src/routes/+layout.server.ts || echo "FAIL: auth session missing from layout" + +# 4. Build succeeds +pnpm build || npm run build +``` + +## Error Recovery + +### "Cannot find module '@workos-inc/authkit-sveltekit'" + +- Check: SDK installed before writing imports +- Check: SDK package directory exists in node_modules +- Re-run install if missing + +### hooks.server.ts not taking effect + +- Check: File is at `src/hooks.server.ts`, not `src/hooks.ts` or elsewhere +- Check: Named export is `handle` (SvelteKit requirement) +- Check: If using `sequence()`, all handles are properly composed + +### Callback route not found (404) + +- Check: File uses `+server.ts` (not `+page.server.ts`) +- Check: Route path matches `WORKOS_REDIRECT_URI` path exactly +- Check: Exports `GET` handler (SvelteKit convention) + +### "locals" type errors + +- Check: App.Locals interface is augmented in `src/app.d.ts` +- Check README for TypeScript setup instructions + +### Cookie password error + +- Verify `WORKOS_COOKIE_PASSWORD` is 32+ characters +- Generate new: `openssl rand -base64 32` + +### Auth state not available in pages + +- Check: `+layout.server.ts` load function returns user data +- Check: Pages access data via `export let data` (Svelte 4) or `$page.data` (Svelte 5) diff --git a/skills/workos-dotnet/SKILL.md b/skills/workos-dotnet/SKILL.md new file mode 100644 index 0000000..d9342cc --- /dev/null +++ b/skills/workos-dotnet/SKILL.md @@ -0,0 +1,163 @@ +--- +name: workos-dotnet +description: Integrate WorkOS AuthKit with .NET (ASP.NET Core). Backend authentication with DI registration, auth endpoints, and appsettings configuration. +--- + +# WorkOS AuthKit for .NET (ASP.NET Core) + +## Step 1: Fetch SDK Documentation (BLOCKING) + +**STOP. Do not proceed until complete.** + +WebFetch: `https://raw.githubusercontent.com/workos/workos-dotnet/main/README.md` + +The README is the source of truth for SDK API usage. If this skill conflicts with README, follow README. + +## Step 2: Pre-Flight Validation + +### Project Structure + +- Confirm a `*.csproj` file exists in the project root +- Detect project style: + - **Minimal API** (modern): `Program.cs` with `WebApplication.CreateBuilder()` — .NET 6+ + - **Startup pattern** (older): `Startup.cs` with `ConfigureServices()` / `Configure()` — .NET 5 and earlier + +This detection determines WHERE to register WorkOS services and middleware. + +### Environment Variables + +Check `appsettings.Development.json` for: + +- `WORKOS_API_KEY` — starts with `sk_` +- `WORKOS_CLIENT_ID` — starts with `client_` + +## Step 3: Install SDK + +```bash +dotnet add package WorkOS.net +``` + +**Verify:** Check the `*.csproj` file contains a `", + "ClientId": "", + "RedirectUri": "http://localhost:5000/auth/callback" + } +} +``` + +Use the actual credential values provided in the environment context. + +**Important:** Do NOT put secrets in `appsettings.json` (committed to git). Use `appsettings.Development.json` (gitignored) or `dotnet user-secrets`. + +## Step 7: Verification + +Run these checks — **do not mark complete until all pass:** + +```bash +# 1. Check WorkOS.net is in csproj +grep -i "WorkOS" *.csproj + +# 2. Check auth endpoints exist +grep -r "auth/login\|auth/callback\|auth/logout" *.cs + +# 3. Build succeeds +dotnet build +``` + +**If build fails:** Read the error output carefully. Common issues: + +- Missing `using` statements for WorkOS namespaces +- Incorrect DI registration order +- Missing session/cookie middleware registration + +## Error Recovery + +### "dotnet: command not found" + +- .NET SDK is not installed. Inform the user to install from https://dotnet.microsoft.com/download + +### NuGet restore failures + +- Check internet connectivity +- Try `dotnet restore` explicitly before `dotnet build` + +### "No project file found" + +- Ensure you're in the correct directory with a `*.csproj` file + +### Build errors after integration + +- Check that all `using` statements are correct +- Verify DI registration order (services before middleware) +- Ensure `app.UseSession()` is called before mapping auth endpoints diff --git a/skills/workos-elixir/SKILL.md b/skills/workos-elixir/SKILL.md new file mode 100644 index 0000000..4ded541 --- /dev/null +++ b/skills/workos-elixir/SKILL.md @@ -0,0 +1,194 @@ +--- +name: workos-elixir +description: Integrate WorkOS AuthKit with Elixir/Phoenix applications. Server-side authentication with Phoenix controllers and routes. +--- + +# WorkOS AuthKit for Elixir (Phoenix) + +## Step 1: Fetch SDK Documentation (BLOCKING) + +**STOP. Do not proceed until complete.** + +WebFetch: `https://raw.githubusercontent.com/workos/workos-elixir/main/README.md` + +The README is the source of truth for SDK API usage. If this skill conflicts with README, follow README. + +## Step 2: Pre-Flight Validation + +### Project Structure + +- Confirm `mix.exs` exists +- Read `mix.exs` to extract the app name (look for `app: :my_app` in `project/0`) +- Confirm `lib/{app}_web/router.ex` exists (Phoenix project marker) +- Confirm `config/runtime.exs` exists + +### Determine App Name + +The app name from `mix.exs` determines all file paths. For example, if `app: :my_app`: + +- Web module: `lib/my_app_web/` +- Router: `lib/my_app_web/router.ex` +- Controllers: `lib/my_app_web/controllers/` + +### Environment Variables + +Check `.env.local` for: + +- `WORKOS_API_KEY` - starts with `sk_` +- `WORKOS_CLIENT_ID` - starts with `client_` + +## Step 3: Install SDK + +Add the `workos` package to `mix.exs` dependencies: + +```elixir +defp deps do + [ + # ... existing deps + {:workos, "~> 1.0"} + ] +end +``` + +Then run: + +```bash +mix deps.get +``` + +**Verify:** Check that `mix deps.get` completed successfully (exit code 0). + +## Step 4: Configure WorkOS + +Add WorkOS configuration to `config/runtime.exs`: + +```elixir +config :workos, + api_key: System.get_env("WORKOS_API_KEY"), + client_id: System.get_env("WORKOS_CLIENT_ID") +``` + +This ensures credentials are loaded from environment variables at runtime, not compiled into the release. + +## Step 5: Create Auth Controller + +### Prerequisite: Verify `{AppName}Web` module exists + +The controller uses `use {AppName}Web, :controller`. Confirm `lib/{app}_web.ex` exists and defines the `:controller` macro. If it doesn't exist (minimal Phoenix projects may lack it), create it: + +```elixir +defmodule {AppName}Web do + def controller do + quote do + use Phoenix.Controller, formats: [:html, :json] + import Plug.Conn + end + end + + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end +``` + +### Create controller + +Create `lib/{app}_web/controllers/auth_controller.ex`: + +```elixir +defmodule {AppName}Web.AuthController do + use {AppName}Web, :controller + + def sign_in(conn, _params) do + client_id = Application.get_env(:workos, :client_id) + redirect_uri = "http://localhost:4000/auth/callback" + + authorization_url = WorkOS.UserManagement.get_authorization_url(%{ + provider: "authkit", + client_id: client_id, + redirect_uri: redirect_uri + }) + + case authorization_url do + {:ok, url} -> redirect(conn, external: url) + {:error, reason} -> conn |> put_status(500) |> text("Auth error: #{inspect(reason)}") + end + end + + def callback(conn, %{"code" => code}) do + client_id = Application.get_env(:workos, :client_id) + + case WorkOS.UserManagement.authenticate_with_code(%{ + code: code, + client_id: client_id + }) do + {:ok, auth_response} -> + conn + |> put_session(:user, auth_response.user) + |> redirect(to: "/") + + {:error, reason} -> + conn |> put_status(401) |> text("Authentication failed: #{inspect(reason)}") + end + end + + def sign_out(conn, _params) do + conn + |> clear_session() + |> redirect(to: "/") + end +end +``` + +**IMPORTANT:** Adapt the module name and API calls based on the README. The WorkOS Elixir SDK API may differ from the pseudocode above. Always follow the README for exact function names, parameter shapes, and return types. + +## Step 6: Add Routes + +Add auth routes to `lib/{app}_web/router.ex`. Add these routes inside or outside the existing pipeline scope as appropriate: + +```elixir +scope "/auth", {AppName}Web do + pipe_through :browser + + get "/sign-in", AuthController, :sign_in + get "/callback", AuthController, :callback + post "/sign-out", AuthController, :sign_out +end +``` + +## Step 7: Verification + +Run the following to confirm the integration compiles: + +```bash +mix compile +``` + +**If compilation fails:** + +1. Read the error message carefully +2. Check that the WorkOS SDK module names match what's in the README +3. Verify the app name is consistent across all files +4. Fix the issue and re-run `mix compile` + +## Error Recovery + +### "could not compile dependency :workos" + +- Check Elixir version compatibility (1.15+ recommended) +- Try `mix deps.clean workos && mix deps.get` + +### "module WorkOS.UserManagement is not available" + +- The SDK API may use different module paths — re-read the README +- Check if the SDK uses `WorkOS.SSO` or another module instead + +### "undefined function" in controller + +- Verify `use {AppName}Web, :controller` is correct +- Check that the SDK functions match the README exactly + +### Route conflicts + +- Check existing routes in router.ex for `/auth` prefix conflicts +- Adjust the scope path if needed (e.g., `/workos-auth`) diff --git a/skills/workos-go/SKILL.md b/skills/workos-go/SKILL.md new file mode 100644 index 0000000..6eda694 --- /dev/null +++ b/skills/workos-go/SKILL.md @@ -0,0 +1,191 @@ +--- +name: workos-go +description: Integrate WorkOS AuthKit with Go applications. Supports Gin and stdlib net/http. +--- + +# WorkOS AuthKit for Go + +## Step 1: Fetch SDK Documentation (BLOCKING) + +**STOP. Do not proceed until complete.** + +WebFetch: `https://github.com/workos/workos-go/blob/main/README.md` + +The README is the source of truth for SDK API usage. If this skill conflicts with README, follow README. + +## Step 2: Pre-Flight Validation + +### Project Structure + +- Confirm `go.mod` exists in the project root +- Confirm Go module is initialized (module path declared in `go.mod`) + +### Environment Variables + +Check `.env` for: + +- `WORKOS_API_KEY` - starts with `sk_` +- `WORKOS_CLIENT_ID` - starts with `client_` +- `WORKOS_REDIRECT_URI` - valid callback URL (e.g., `http://localhost:8080/auth/callback`) + +### Framework Detection + +Read `go.mod` to detect web framework: + +``` +go.mod contains github.com/gin-gonic/gin? + | + +-- Yes --> Use Gin router patterns + | + +-- No --> Use stdlib net/http patterns +``` + +## Step 3: Install SDK + +Run: + +```bash +go get github.com/workos/workos-go/v4 +``` + +**Verify:** Check that `go.mod` now contains `github.com/workos/workos-go/v4`. Both `go.mod` and `go.sum` will be modified — this is expected. + +## Step 4: Configure Authentication + +### 4a: Create Auth Handler File + +Create an auth handler file. Respect existing project structure: + +- If `internal/` directory exists, create `internal/auth/handlers.go` +- If `handlers/` directory exists, create `handlers/auth.go` +- Otherwise, create `auth/handlers.go` + +The file must: + +- Declare a package matching the directory name +- Import `github.com/workos/workos-go/v4` packages as needed +- Read env vars with `os.Getenv("WORKOS_API_KEY")`, `os.Getenv("WORKOS_CLIENT_ID")`, `os.Getenv("WORKOS_REDIRECT_URI")` + +### 4b: Implement Handlers + +Implement these three handlers following the redirect-based auth flow from the README: + +**Login handler** (`/auth/login`): + +- Get the authorization URL from WorkOS using `usermanagement.GetAuthorizationURL()` +- Set `Provider` to the string `"authkit"` (it's a plain string, not a constant) +- Include `ClientID` and `RedirectURI` from env vars +- Redirect the user to the returned URL + +**Callback handler** (`/auth/callback`): + +- Extract the `code` query parameter from the redirect +- Call `usermanagement.AuthenticateWithCode()` with the code and `ClientID` +- Store user info in session/cookie (or return as JSON for API-first apps) +- Redirect to homepage or return user data + +**Logout handler** (`/auth/logout`): + +- Clear session data +- Redirect to homepage + +**CRITICAL:** Use idiomatic Go error handling throughout: + +```go +result, err := someFunction() +if err != nil { + http.Error(w, "Error message", http.StatusInternalServerError) + return +} +``` + +### 4c: Wire Handlers into Router + +#### If using Gin: + +```go +r := gin.Default() +r.GET("/auth/login", handleLogin) +r.GET("/auth/callback", handleCallback) +r.GET("/auth/logout", handleLogout) +``` + +#### If using stdlib net/http: + +```go +http.HandleFunc("/auth/login", handleLogin) +http.HandleFunc("/auth/callback", handleCallback) +http.HandleFunc("/auth/logout", handleLogout) +``` + +Wire these routes into the existing router setup in `main.go` or wherever routes are defined. Do NOT replace existing routes — add alongside them. + +### 4d: Initialize WorkOS Client + +In the appropriate init location (package-level `init()` or `main()`), initialize the WorkOS client: + +```go +import "github.com/workos/workos-go/v4/pkg/usermanagement" + +func init() { + usermanagement.SetAPIKey(os.Getenv("WORKOS_API_KEY")) +} +``` + +Follow the README for the exact initialization pattern — it may differ from above. + +## Step 5: Environment Setup + +The `.env` file should already contain the required variables (written by the installer). Verify it contains: + +``` +WORKOS_API_KEY=sk_... +WORKOS_CLIENT_ID=client_... +WORKOS_REDIRECT_URI=http://localhost:8080/auth/callback +``` + +**Note for production:** Go does not have a built-in .env convention. In production, set real OS environment variables. The `.env` file is for development only. If using a `.env` loader like `github.com/joho/godotenv`, the agent may install it and add `godotenv.Load()` to `main()`. + +## Step 6: Verification + +Run these commands. **Do not mark complete until all pass:** + +```bash +# 1. Go module is tidy +go mod tidy + +# 2. Build succeeds +go build ./... + +# 3. Vet passes (catches common mistakes) +go vet ./... +``` + +If build fails: + +- Check import paths match the SDK version in `go.mod` +- Ensure all new files have correct package declarations +- Run `go mod tidy` to resolve dependency issues + +## Error Recovery + +### "cannot find module providing package github.com/workos/workos-go/v4/..." + +- Run `go mod tidy` to sync dependencies +- Check that `go get` completed successfully +- Verify the import path matches exactly (v4 suffix required) + +### "undefined: usermanagement.SetAPIKey" or similar + +- SDK API may have changed — refer to the fetched README +- Check the correct subpackage import path + +### Build fails with type errors + +- Ensure handler function signatures match the framework (Gin uses `*gin.Context`, stdlib uses `http.ResponseWriter, *http.Request`) +- Check that error return values are handled + +### "package X is not in std" + +- Run `go mod tidy` after adding new imports +- Ensure `go get` was run before writing import statements diff --git a/skills/workos-kotlin/SKILL.md b/skills/workos-kotlin/SKILL.md new file mode 100644 index 0000000..4f0e687 --- /dev/null +++ b/skills/workos-kotlin/SKILL.md @@ -0,0 +1,161 @@ +--- +name: workos-kotlin +description: Integrate WorkOS AuthKit with Kotlin/Spring Boot. Backend authentication with Gradle. +--- + +# WorkOS AuthKit for Kotlin (Spring Boot) + +## Step 1: Fetch SDK Documentation (BLOCKING) + +**STOP. Do not proceed until complete.** + +WebFetch: `https://raw.githubusercontent.com/workos/workos-kotlin/main/README.md` + +The README is the source of truth. If this skill conflicts with README, follow README. + +## Step 2: Pre-Flight Validation + +### Project Structure + +- Confirm `build.gradle.kts` exists (Kotlin DSL) or `build.gradle` (Groovy DSL) +- Confirm Spring Boot plugin is present (`org.springframework.boot`) +- Detect Gradle wrapper: check if `./gradlew` exists + +### Gradle Wrapper + +```bash +# If gradlew exists, ensure it's executable +if [ -f ./gradlew ]; then chmod +x ./gradlew; fi +``` + +Use `./gradlew` if wrapper exists, otherwise fall back to `gradle`. + +### Environment Variables + +Check `application.properties` or `application.yml` for: + +- `workos.api-key` or `WORKOS_API_KEY` +- `workos.client-id` or `WORKOS_CLIENT_ID` + +## Step 3: Install SDK + +Add the WorkOS Kotlin SDK dependency to `build.gradle.kts`: + +```kotlin +dependencies { + implementation("com.workos:workos-kotlin:4.18.1") + // ... existing dependencies +} +``` + +Check the README for the latest version number — use the version from the README if it differs from above. + +**JVM target**: Ensure `jvmTarget` in `build.gradle.kts` matches the JDK on the system. Check with `java -version`. Common values: `"17"`, `"21"`. If `kotlin { jvmToolchain(...) }` is set, ensure it matches too. + +**Verify:** Run `./gradlew dependencies` or `gradle dependencies` to confirm the dependency resolves. + +## Step 4: Configure Authentication + +### 4a: Application Properties + +Add WorkOS configuration to `src/main/resources/application.properties`: + +```properties +workos.api-key=${WORKOS_API_KEY} +workos.client-id=${WORKOS_CLIENT_ID} +workos.redirect-uri=http://localhost:8080/auth/callback +``` + +Or if the project uses `application.yml`, add the equivalent YAML. + +### 4b: Create WorkOS Configuration Bean + +Create a configuration class that initializes the WorkOS client: + +```kotlin +@Configuration +class WorkOSConfig { + @Value("\${workos.api-key}") + lateinit var apiKey: String + + @Bean + fun workos(): WorkOS = WorkOS(apiKey) +} +``` + +Adapt based on the SDK README — the exact client initialization may vary. + +### 4c: Create Auth Controller + +Create a Spring `@RestController` with these endpoints: + +1. **GET /auth/login** — Redirect user to WorkOS AuthKit hosted login + - Use `workos.userManagement.getAuthorizationUrl()` — this returns a URL string + - Parameters: `clientId`, `redirectUri`, `provider = "authkit"` + - The method uses a builder pattern: `.provider("authkit").redirectUri(uri).build()` + +2. **GET /auth/callback** — Exchange authorization code for user profile + - Extract `code` query parameter + - Call `workos.userManagement.authenticateWithCode()` with the code and clientId + - Store user session (use Spring's `HttpSession`) + - Redirect to home page + +3. **GET /auth/logout** — Clear session and redirect + - Invalidate `HttpSession` + - Redirect to home page or WorkOS logout URL + +**Follow the README for exact API method names and parameters.** + +## Step 5: Session Management + +Use Spring's built-in `HttpSession` for session management: + +- Store user profile in session after callback +- Check session in protected routes +- Clear session on logout + +If Spring Security is already configured, integrate with the existing security filter chain rather than replacing it. + +## Step 6: Verification + +Run the build to verify everything compiles: + +```bash +./gradlew build +``` + +**If build fails:** + +- Check dependency resolution: `./gradlew dependencies | grep workos` +- Check for missing imports in the auth controller +- Verify application.properties syntax +- Gradle builds can be slow (30-60s) — be patient + +### Checklist + +- [ ] WorkOS SDK dependency in build.gradle.kts +- [ ] Application properties configured +- [ ] Auth controller with login, callback, logout endpoints +- [ ] Build succeeds (`./gradlew build`) + +## Error Recovery + +### Dependency resolution failure + +- Check Maven Central is accessible +- Verify the artifact coordinates match README exactly +- Ensure `mavenCentral()` is in the `repositories` block of build.gradle.kts + +### "Could not resolve com.workos:workos-kotlin" + +- The package may use a different group ID — check README +- Ensure repositories block includes `mavenCentral()` + +### Build fails with missing Spring Boot annotations + +- Verify `org.springframework.boot` plugin is applied +- Check Spring Boot starter dependencies are present + +### Gradle wrapper permission denied + +- Run `chmod +x ./gradlew` before building diff --git a/skills/workos-node/SKILL.md b/skills/workos-node/SKILL.md new file mode 100644 index 0000000..13f9d52 --- /dev/null +++ b/skills/workos-node/SKILL.md @@ -0,0 +1,164 @@ +--- +name: workos-node +description: Integrate WorkOS AuthKit with Node.js backend applications. Adapts to Express, Fastify, Hono, Koa, or vanilla Node.js http. Server-side authentication with redirect-based OAuth flow. +--- + +# WorkOS AuthKit for Node.js + +## Step 1: Fetch SDK Documentation (BLOCKING) + +**STOP - Do not proceed until complete.** + +WebFetch: `https://raw.githubusercontent.com/workos/workos-node/main/README.md` + +Also fetch the AuthKit quickstart for reference: +WebFetch: `https://workos.com/docs/authkit/vanilla/nodejs` + +README is the source of truth for all SDK patterns. **README overrides this skill if conflict.** + +## Step 2: Detect Framework & Project Structure + +``` +package.json has 'express'? → Express +package.json has 'fastify'? → Fastify +package.json has 'hono'? → Hono +package.json has 'koa'? → Koa +None of the above? → Vanilla Node.js http (use Express quickstart pattern) + +tsconfig.json exists? → TypeScript (.ts files) +"type": "module" in package.json? → ESM (import/export) +else → CJS (require/module.exports) +``` + +Detect entry point: `src/index.ts`, `src/app.ts`, `app.js`, `server.js`, `index.js` + +Detect package manager: `pnpm-lock.yaml` → `yarn.lock` → `bun.lockb` → npm + +**Adapt all subsequent steps to the detected framework and module system.** + +## Step 3: Install SDK + +``` +pnpm-lock.yaml → pnpm add @workos-inc/node dotenv cookie-parser +yarn.lock → yarn add @workos-inc/node dotenv cookie-parser +bun.lockb → bun add @workos-inc/node dotenv cookie-parser +else → npm install @workos-inc/node dotenv cookie-parser +``` + +For TypeScript, also install types: `pnpm add -D @types/cookie-parser` + +**Verify:** `@workos-inc/node` in package.json dependencies + +## Step 4: Initialize WorkOS Client + +Adapt to detected module system (ESM vs CJS): + +**ESM/TypeScript:** + +```typescript +import { WorkOS } from '@workos-inc/node'; +const workos = new WorkOS(process.env.WORKOS_API_KEY, { + clientId: process.env.WORKOS_CLIENT_ID, +}); +``` + +**CJS:** + +```javascript +const { WorkOS } = require('@workos-inc/node'); +const workos = new WorkOS(process.env.WORKOS_API_KEY, { + clientId: process.env.WORKOS_CLIENT_ID, +}); +``` + +## Step 5: Integrate Authentication + +### If Express + +Follow the quickstart pattern: + +1. **`/login` route** — call `workos.userManagement.getAuthorizationUrl({ provider: 'authkit', redirectUri: ..., clientId: ... })`, redirect +2. **`/callback` route** — call `workos.userManagement.authenticateWithCode({ code, clientId })`, store session via sealed session or express-session +3. **`/logout` route** — clear session cookie, redirect +4. **Cookie middleware** — `app.use(cookieParser())` +5. **Session-aware home route** — read session, display user info + +**Session handling options (pick one):** + +- **Sealed sessions** (recommended, from quickstart): use `sealSession: true` in authenticateWithCode, store sealed cookie, use `loadSealedSession` for verification +- **express-session**: install `express-session`, configure middleware before routes, store user in `req.session` + +### If Fastify + +1. Register `@fastify/cookie` plugin +2. Create `/login`, `/callback`, `/logout` routes using Fastify route syntax +3. Use `reply.redirect()` for redirects +4. Store session in signed cookie + +### If Hono + +1. Create `/login`, `/callback`, `/logout` routes using Hono router +2. Use `c.redirect()` for redirects +3. Use Hono's cookie helpers for session + +### If Koa + +1. Install `koa-router` if not present +2. Create auth routes on router +3. Use `ctx.redirect()` for redirects +4. Use `koa-session` for session management + +### If Vanilla Node.js (no framework detected) + +Install Express and follow the Express pattern above. This matches the official quickstart. + +## Step 6: Environment Setup + +Create `.env` if it doesn't exist. Do NOT overwrite existing values: + +``` +WORKOS_API_KEY=sk_... +WORKOS_CLIENT_ID=client_... +WORKOS_REDIRECT_URI=http://localhost:3000/callback +WORKOS_COOKIE_PASSWORD= +``` + +Ensure `.env` is in `.gitignore`. + +## Step 7: Verification + +**TypeScript:** `npx tsc --noEmit` +**JavaScript:** `node --check ` + +### Checklist + +- [ ] SDK installed (`@workos-inc/node` in package.json) +- [ ] WorkOS client initialized +- [ ] Login route redirects to AuthKit +- [ ] Callback route exchanges code for user +- [ ] Logout route clears session +- [ ] `.env` has required variables +- [ ] Build/syntax check passes + +## Error Recovery + +### Module not found: @workos-inc/node + +Re-run install for detected package manager. + +### Session not persisting + +If using express-session: ensure middleware registered BEFORE routes. +If using sealed sessions: ensure cookie is being set with correct options (httpOnly, secure in prod, sameSite: 'lax'). + +### Callback returns 404 + +Route path must match WORKOS_REDIRECT_URI exactly. + +### ESM/CJS mismatch + +Check `"type"` field in package.json — `"module"` = ESM (import/export), absent = CJS (require). + +### TypeScript errors + +Install missing types: `@types/express`, `@types/cookie-parser`, `@types/express-session`. diff --git a/skills/workos-php-laravel/SKILL.md b/skills/workos-php-laravel/SKILL.md new file mode 100644 index 0000000..7b96954 --- /dev/null +++ b/skills/workos-php-laravel/SKILL.md @@ -0,0 +1,147 @@ +--- +name: workos-php-laravel +description: Integrate WorkOS AuthKit with Laravel applications. Uses the dedicated workos-php-laravel SDK with service provider, middleware, and config publishing. +--- + +# WorkOS AuthKit for Laravel + +## Step 1: Fetch SDK Documentation (BLOCKING) + +**STOP. Do not proceed until complete.** + +WebFetch: `https://github.com/workos/workos-php-laravel/blob/main/README.md` + +The README is the source of truth. If this skill conflicts with README, follow README. + +## Step 2: Pre-Flight Validation + +### Project Structure + +- Confirm `artisan` file exists at project root +- Confirm `composer.json` contains `laravel/framework` dependency +- Confirm `app/` and `routes/` directories exist + +### Environment Variables + +Check `.env` for: + +- `WORKOS_API_KEY` - starts with `sk_` +- `WORKOS_CLIENT_ID` - starts with `client_` +- `WORKOS_REDIRECT_URI` - valid callback URL (e.g., `http://localhost:8000/auth/callback`) + +If `.env` exists but is missing these variables, append them. If `.env` doesn't exist, copy `.env.example` and add them. + +## Step 3: Install SDK + +```bash +composer require workos/workos-php-laravel +``` + +**Verify:** Check `composer.json` contains `workos/workos-php-laravel` in require section before continuing. + +## Step 4: Publish Configuration + +```bash +php artisan vendor:publish --provider="WorkOS\Laravel\WorkOSServiceProvider" +``` + +This creates `config/workos.php`. Verify the file exists after publishing. + +If the artisan command fails, check README for the correct provider class name — it may differ. + +## Step 5: Configure Environment + +Ensure `.env` contains: + +``` +WORKOS_API_KEY=sk_... +WORKOS_CLIENT_ID=client_... +WORKOS_REDIRECT_URI=http://localhost:8000/auth/callback +``` + +Also ensure `config/workos.php` reads these env vars correctly. Check README for exact config structure. + +## Step 6: Create Auth Controller + +Create `app/Http/Controllers/AuthController.php` with methods for: + +- `login()` — Redirect to WorkOS AuthKit authorization URL +- `callback()` — Handle OAuth callback, exchange code for user profile +- `logout()` — Clear session and redirect + +Use SDK methods from README. Do NOT construct OAuth URLs manually. + +## Step 7: Add Routes + +Add to `routes/web.php`: + +```php +use App\Http\Controllers\AuthController; + +Route::get('/login', [AuthController::class, 'login'])->name('login'); +Route::get('/auth/callback', [AuthController::class, 'callback']); +Route::get('/logout', [AuthController::class, 'logout'])->name('logout'); +``` + +Ensure the callback route path matches `WORKOS_REDIRECT_URI`. + +## Step 8: Add Middleware (if applicable) + +Check README for any authentication middleware the SDK provides. If available: + +1. Register middleware in `app/Http/Kernel.php` or `bootstrap/app.php` (Laravel 11+) +2. Apply to routes that require authentication + +For Laravel 11+, middleware is registered in `bootstrap/app.php` instead of `Kernel.php`. + +## Step 9: Add UI Integration + +Update the home page or dashboard view to show: + +- Sign in link when user is not authenticated +- User info and sign out link when authenticated + +Use Blade directives or SDK helpers from README. + +## Verification Checklist (ALL MUST PASS) + +```bash +# 1. Config file exists +ls config/workos.php + +# 2. Controller exists +ls app/Http/Controllers/AuthController.php + +# 3. Routes registered +php artisan route:list | grep -E "login|callback|logout" + +# 4. SDK installed +composer show workos/workos-php-laravel + +# 5. Lint check +php -l app/Http/Controllers/AuthController.php +``` + +## Error Recovery + +### "Class WorkOS\Laravel\WorkOSServiceProvider not found" + +- Verify `composer require` completed successfully +- Run `composer dump-autoload` +- Check `vendor/workos/` directory exists + +### "Route not defined" + +- Verify routes are in `routes/web.php` +- Run `php artisan route:clear && php artisan route:cache` + +### Config not loading + +- Verify `config/workos.php` exists +- Run `php artisan config:clear` +- Check `.env` variables match config keys + +### Middleware issues (Laravel 11+) + +- Laravel 11 removed `Kernel.php` — register middleware in `bootstrap/app.php` +- Check README for Laravel version-specific instructions diff --git a/skills/workos-php/SKILL.md b/skills/workos-php/SKILL.md new file mode 100644 index 0000000..7839ac2 --- /dev/null +++ b/skills/workos-php/SKILL.md @@ -0,0 +1,127 @@ +--- +name: workos-php +description: Integrate WorkOS AuthKit with generic PHP applications. Uses the workos-php SDK directly with standalone auth endpoint files. +--- + +# WorkOS AuthKit for PHP + +## Step 1: Fetch SDK Documentation (BLOCKING) + +**STOP. Do not proceed until complete.** + +WebFetch: `https://github.com/workos/workos-php/blob/main/README.md` + +The README is the source of truth. If this skill conflicts with README, follow README. + +## Step 2: Pre-Flight Validation + +### Project Structure + +- Confirm `composer.json` exists at project root +- If `composer.json` doesn't exist, create a minimal one with `composer init --no-interaction` + +### Environment Variables + +Check for `.env` file with: + +- `WORKOS_API_KEY` - starts with `sk_` +- `WORKOS_CLIENT_ID` - starts with `client_` +- `WORKOS_REDIRECT_URI` - valid callback URL (e.g., `http://localhost:8000/callback.php`) + +If `.env` doesn't exist, create it with the required variables. + +## Step 3: Install SDK + +```bash +composer require workos/workos-php +``` + +**Verify:** Check `composer.json` contains `workos/workos-php` in require section. + +Also install a dotenv library if not present: + +```bash +composer require vlucas/phpdotenv +``` + +## Step 4: Create Bootstrap File + +Create a bootstrap or config file (e.g., `config.php` or `bootstrap.php`) that: + +1. Requires Composer autoloader: `require_once __DIR__ . '/vendor/autoload.php';` +2. Loads `.env` using phpdotenv +3. Initializes the WorkOS SDK client with API key + +Use SDK initialization from README. Do NOT hardcode credentials. + +## Step 5: Create Auth Endpoint Files + +### `login.php` + +- Initialize WorkOS client (include bootstrap) +- Generate authorization URL using SDK +- Redirect user to WorkOS AuthKit + +### `callback.php` + +- Initialize WorkOS client (include bootstrap) +- Exchange authorization code from `$_GET['code']` for user profile using SDK +- Start session, store user data +- Redirect to home/dashboard + +### `logout.php` + +- Destroy session +- Redirect to home page + +Use SDK methods from README for all WorkOS API calls. Do NOT construct OAuth URLs manually. + +## Step 6: Create Home Page + +Create or update `index.php` to show: + +- Sign in link (`login.php`) when no session +- User info and sign out link (`logout.php`) when session exists + +## Verification Checklist (ALL MUST PASS) + +```bash +# 1. SDK installed +composer show workos/workos-php + +# 2. Auth files exist +ls login.php callback.php logout.php + +# 3. No syntax errors +php -l login.php +php -l callback.php +php -l logout.php +php -l index.php + +# 4. Autoloader exists +ls vendor/autoload.php +``` + +## Error Recovery + +### "Class WorkOS\WorkOS not found" + +- Verify `composer require` completed successfully +- Check `vendor/autoload.php` is required in bootstrap +- Run `composer dump-autoload` + +### Session issues + +- Ensure `session_start()` is called before any session access +- Check PHP session configuration (`session.save_path`) + +### Redirect URI mismatch + +- Compare callback file path to `WORKOS_REDIRECT_URI` in `.env` +- URLs must match exactly (including trailing slash) + +### Environment variables not loading + +- Verify `.env` file exists in project root +- Verify phpdotenv is installed and loaded in bootstrap +- Check file permissions on `.env` diff --git a/skills/workos-python/SKILL.md b/skills/workos-python/SKILL.md new file mode 100644 index 0000000..a408d32 --- /dev/null +++ b/skills/workos-python/SKILL.md @@ -0,0 +1,159 @@ +--- +name: workos-python +description: Integrate WorkOS AuthKit with Python applications. Adapts to Django, Flask, FastAPI, or vanilla Python. Server-side authentication with redirect-based OAuth flow. +--- + +# WorkOS AuthKit for Python + +## Step 1: Fetch SDK Documentation (BLOCKING) + +**STOP. Do not proceed until complete.** + +WebFetch: `https://raw.githubusercontent.com/workos/workos-python/main/README.md` + +Also fetch the AuthKit quickstart for reference: +WebFetch: `https://workos.com/docs/authkit/vanilla/python` + +The README is the source of truth for SDK API usage. If this skill conflicts with README, follow README. + +## Step 2: Detect Framework + +Examine the project to determine which Python web framework is in use: + +``` +manage.py exists? → Django + settings.py has django imports? → Confirmed Django + +Gemfile/requirements has 'fastapi'? → FastAPI + main.py has FastAPI() instance? → Confirmed FastAPI + +requirements has 'flask'? → Flask + server.py/app.py has Flask() instance? → Confirmed Flask + +None of the above? → Vanilla Python (use Flask quickstart pattern) +``` + +**Adapt all subsequent steps to the detected framework.** Do not force one framework onto another. + +## Step 3: Pre-Flight Validation + +### Package Manager Detection + +``` +uv.lock exists? → uv add +pyproject.toml has [tool.poetry]? → poetry add +Pipfile exists? → pipenv install +requirements.txt exists? → pip install (+ append to requirements.txt) +else → pip install +``` + +### Environment Variables + +Check `.env` for: + +- `WORKOS_API_KEY` - starts with `sk_` +- `WORKOS_CLIENT_ID` - starts with `client_` + +## Step 4: Install SDK + +Install using the detected package manager: + +```bash +# uv +uv add workos python-dotenv + +# poetry +poetry add workos python-dotenv + +# pip +pip install workos python-dotenv +``` + +If using `requirements.txt`, also append `workos` and `python-dotenv` to it. + +**Verify:** `python -c "import workos; print('OK')"` + +## Step 5: Integrate Authentication + +### If Django + +1. **Configure settings.py** — add `import os` + `from dotenv import load_dotenv` + `load_dotenv()` at top. Add `WORKOS_API_KEY` and `WORKOS_CLIENT_ID` from `os.environ.get()`. +2. **Create auth views** — create `auth_views.py` (or add to existing views): + - `login_view`: call SDK's `get_authorization_url()` with `provider='authkit'`, redirect + - `callback_view`: call `authenticate_with_code()` with the code param, store user in `request.session` + - `logout_view`: flush session, redirect +3. **Add URL patterns** — add `auth/login/`, `auth/callback/`, `auth/logout/` to `urls.py` +4. **Update templates** — add login/logout links using `{% url %}` tags + +### If Flask + +Follow the quickstart pattern exactly: + +1. **Initialize WorkOS client** in `server.py` / `app.py`: + ```python + from workos import WorkOSClient + workos = WorkOSClient(api_key=os.getenv("WORKOS_API_KEY"), client_id=os.getenv("WORKOS_CLIENT_ID")) + ``` +2. **Create `/login` route** — call `workos.user_management.get_authorization_url(provider="authkit", redirect_uri="...")`, redirect +3. **Create `/callback` route** — call `workos.user_management.authenticate_with_code(code=code)`, set session cookie +4. **Create `/logout` route** — clear session, redirect +5. **Update home route** — show user info if session exists + +### If FastAPI + +1. **Initialize WorkOS client** in main app file +2. **Create `/login` endpoint** — generate auth URL, return `RedirectResponse` +3. **Create `/callback` endpoint** — exchange code, store in session/cookie +4. **Create `/logout` endpoint** — clear session +5. Use `Depends()` for auth middleware on protected routes + +### If Vanilla Python (no framework detected) + +Install Flask and follow the Flask pattern above. This matches the official quickstart. + +## Step 6: Environment Setup + +Create/update `.env` with WorkOS credentials. Do NOT overwrite existing values. + +``` +WORKOS_API_KEY=sk_... +WORKOS_CLIENT_ID=client_... +``` + +## Step 7: Verification Checklist + +```bash +# 1. SDK importable +python -c "import workos; print('OK')" + +# 2. Credentials configured +python -c " +from dotenv import load_dotenv; import os; load_dotenv() +assert os.environ.get('WORKOS_API_KEY','').startswith('sk_'), 'Missing WORKOS_API_KEY' +assert os.environ.get('WORKOS_CLIENT_ID','').startswith('client_'), 'Missing WORKOS_CLIENT_ID' +print('Credentials OK') +" + +# 3. Framework-specific check +# Django: python manage.py check +# Flask: python -m py_compile server.py +# FastAPI: python -m py_compile main.py +``` + +## Error Recovery + +### "ModuleNotFoundError: No module named 'workos'" + +Re-run the install command for the detected package manager. + +### Django: "CSRF verification failed" + +Auth callback receives GET requests from WorkOS. Ensure callback view uses GET, not POST. Or add `@csrf_exempt`. + +### Flask: Session not persisting + +Ensure `app.secret_key` is set (required for Flask sessions). + +### Virtual environment not active + +Check for `.venv/`, `venv/`, or poetry-managed environments. Activate before running install. diff --git a/skills/workos-ruby/SKILL.md b/skills/workos-ruby/SKILL.md new file mode 100644 index 0000000..8295a9c --- /dev/null +++ b/skills/workos-ruby/SKILL.md @@ -0,0 +1,163 @@ +--- +name: workos-ruby +description: Integrate WorkOS AuthKit with Ruby applications. Adapts to Rails, Sinatra, or vanilla Ruby. Server-side authentication with redirect-based OAuth flow. +--- + +# WorkOS AuthKit for Ruby + +## Step 1: Fetch SDK Documentation (BLOCKING) + +**STOP — Do not proceed until this fetch is complete.** + +WebFetch: `https://raw.githubusercontent.com/workos/workos-ruby/main/README.md` + +Also fetch the AuthKit quickstart for reference: +WebFetch: `https://workos.com/docs/authkit/vanilla/ruby` + +The README is the **source of truth** for gem API usage. If this skill conflicts with the README, **follow the README**. + +## Step 2: Detect Framework + +Examine the project to determine which Ruby web framework is in use: + +``` +config/routes.rb exists? → Rails + Gemfile has 'rails' gem? → Confirmed Rails + +Gemfile has 'sinatra' gem? → Sinatra + server.rb/app.rb has Sinatra routes? → Confirmed Sinatra + +None of the above? → Vanilla Ruby (use Sinatra quickstart pattern) +``` + +**Adapt all subsequent steps to the detected framework.** Do not force Rails on a Sinatra project or vice versa. + +## Step 3: Install WorkOS Gem + +```bash +bundle add workos +``` + +If `dotenv` is not in the Gemfile: + +```bash +# Rails +bundle add dotenv-rails --group development,test + +# Sinatra / other +bundle add dotenv +``` + +**Verify:** `bundle show workos` + +## Step 4: Integrate Authentication + +### If Rails + +1. **Create initializer** — `config/initializers/workos.rb`: + + ```ruby + WorkOS.configure do |config| + config.api_key = ENV.fetch("WORKOS_API_KEY") + config.client_id = ENV.fetch("WORKOS_CLIENT_ID") + end + ``` + +2. **Create AuthController** — `app/controllers/auth_controller.rb`: + - `login` action: call `WorkOS::UserManagement.get_authorization_url(provider: "authkit", redirect_uri: ...)`, redirect + - `callback` action: call `WorkOS::UserManagement.authenticate_with_code(code: params[:code])`, store user in session + - `logout` action: clear session, redirect + +3. **Add routes** to `config/routes.rb`: + + ```ruby + get "/auth/login", to: "auth#login" + get "/auth/callback", to: "auth#callback" + get "/auth/logout", to: "auth#logout" + ``` + +4. **Add current_user helper** to `ApplicationController` (optional): + + ```ruby + helper_method :current_user + def current_user + @current_user ||= session[:user] && JSON.parse(session[:user]) + end + ``` + +5. **Verify:** `bundle exec rails routes | grep auth` + +### If Sinatra + +Follow the quickstart pattern exactly: + +1. **Configure WorkOS** in `server.rb`: + + ```ruby + require "dotenv/load" + require "workos" + require "sinatra" + + WorkOS.configure do |config| + config.key = ENV["WORKOS_API_KEY"] + end + ``` + +2. **Create `/login` route** — call `WorkOS::UserManagement.authorization_url(provider: "authkit", client_id: ..., redirect_uri: ...)`, redirect + +3. **Create `/callback` route** — call `WorkOS::UserManagement.authenticate_with_code(client_id: ..., code: ...)`, store in session cookie + +4. **Create `/logout` route** — clear session cookie, redirect + +5. **Update home route** — read session, show user info if present + +6. **Verify:** `ruby -c server.rb` + +### If Vanilla Ruby (no framework detected) + +Install Sinatra and follow the Sinatra pattern above. This matches the official quickstart. + +## Step 5: Environment Setup + +Create/update `.env` with WorkOS credentials. Do NOT overwrite existing values. + +``` +WORKOS_API_KEY=sk_... +WORKOS_CLIENT_ID=client_... +``` + +## Step 6: Verification + +### Rails + +```bash +bundle show workos +bundle exec rails routes | grep auth +grep WORKOS .env +``` + +### Sinatra + +```bash +bundle show workos +ruby -c server.rb +grep WORKOS .env +``` + +## Error Recovery + +### "uninitialized constant WorkOS" + +Gem not loaded. Verify `bundle show workos` succeeds. For Rails, ensure initializer exists. For Sinatra, ensure `require "workos"` is at top of server file. + +### "NoMethodError" on WorkOS methods + +SDK API may differ from this skill. Re-read the README (Step 1) and use exact method names. + +### Routes not working (Rails) + +Run `bundle exec rails routes | grep auth`. Verify routes are inside `Rails.application.routes.draw` block. + +### Session not persisting (Sinatra) + +Enable sessions: `enable :sessions` in server.rb, or use `rack-session` gem. diff --git a/src/bin.ts b/src/bin.ts index 4d9f3ef..ee7efcf 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -9,7 +9,7 @@ if (process.argv.includes('--local') || process.env.INSTALLER_DEV) { import { satisfies } from 'semver'; import { red } from './utils/logging.js'; -import { getConfig } from './lib/settings.js'; +import { getConfig, getVersion } from './lib/settings.js'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; @@ -124,7 +124,6 @@ const installerOptions = { }, integration: { describe: 'Integration to set up', - choices: ['nextjs', 'react', 'tanstack-start', 'react-router', 'vanilla-js'] as const, type: 'string' as const, }, 'force-install': { @@ -238,6 +237,6 @@ yargs(hideBin(process.argv)) .strict() .help() .alias('help', 'h') - .version() + .version(getVersion()) .alias('version', 'v') .wrap(process.stdout.isTTY && process.stdout.columns ? process.stdout.columns : 80).argv; diff --git a/src/integrations/dotnet/index.ts b/src/integrations/dotnet/index.ts new file mode 100644 index 0000000..76b87ab --- /dev/null +++ b/src/integrations/dotnet/index.ts @@ -0,0 +1,200 @@ +/* .NET (ASP.NET Core) integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; +import { SPINNER_MESSAGE } from '../../lib/framework-config.js'; +import { getOrAskForWorkOSCredentials } from '../../utils/clack-utils.js'; +import { analytics } from '../../utils/analytics.js'; +import { INSTALLER_INTERACTION_EVENT_NAME } from '../../lib/constants.js'; +import { initializeAgent, runAgent } from '../../lib/agent-interface.js'; +import { autoConfigureWorkOSEnvironment } from '../../lib/workos-management.js'; +import { validateInstallation } from '../../lib/validation/index.js'; + +export const config: FrameworkConfig = { + metadata: { + name: '.NET (ASP.NET Core)', + integration: 'dotnet', + docsUrl: 'https://github.com/workos/workos-dotnet', + skillName: 'workos-dotnet', + language: 'dotnet', + stability: 'experimental', + priority: 35, + packageManager: 'dotnet', + manifestFile: '*.csproj', + }, + + detection: { + // Detection handled by language-detection.ts globExists('*.csproj'). + // These fields satisfy the FrameworkDetection interface but aren't used for non-JS SDKs. + packageName: 'WorkOS.net', + packageDisplayName: '.NET (ASP.NET Core)', + getVersion: () => undefined, + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your ASP.NET Core project structure', + 'Installed WorkOS.net NuGet package', + 'Created authentication endpoints (login, callback, logout)', + 'Configured WorkOS in appsettings', + ], + getOutroNextSteps: () => [ + 'Run `dotnet run` to start your development server', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +/** + * Custom run function for .NET — bypasses runAgentInstaller's JS-centric assumptions + * (no package.json, no .env.local, no TypeScript detection, no JS port detection). + */ +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + options.emitter?.emit('status', { + message: `Setting up WorkOS AuthKit for ${config.metadata.name}`, + }); + + analytics.capture(INSTALLER_INTERACTION_EVENT_NAME, { + action: 'started agent integration', + integration: config.metadata.integration, + }); + + const { apiKey, clientId } = await getOrAskForWorkOSCredentials(options, config.environment.requiresApiKey); + + // Auto-configure WorkOS environment (redirect URI, CORS, homepage) + const callerHandledConfig = Boolean(options.apiKey || options.clientId); + if (!callerHandledConfig && apiKey) { + const port = 5000; // ASP.NET Core default HTTP port + await autoConfigureWorkOSEnvironment(apiKey, config.metadata.integration, port, { + homepageUrl: options.homepageUrl, + redirectUri: options.redirectUri, + }); + } + + // Build prompt — credentials are passed via prompt context since .NET doesn't use .env.local + const skillName = config.metadata.skillName!; + const redirectUri = options.redirectUri || 'http://localhost:5000/auth/callback'; + + const prompt = `You are integrating WorkOS AuthKit into this ASP.NET Core application. + +## Project Context + +- Framework: ASP.NET Core +- Language: C# +- Package manager: dotnet (NuGet) + +## Environment + +The following WorkOS credentials should be configured in appsettings.Development.json: +- WORKOS_API_KEY: ${apiKey || '(not provided)'} +- WORKOS_CLIENT_ID: ${clientId} +- WORKOS_REDIRECT_URI: ${redirectUri} + +## Your Task + +Use the \`${skillName}\` skill to integrate WorkOS AuthKit into this application. + +The skill contains step-by-step instructions including: +1. Fetching the SDK documentation +2. Installing the WorkOS.net NuGet package +3. Configuring DI registration +4. Creating authentication endpoints +5. Setting up appsettings configuration + +Report your progress using [STATUS] prefixes. + +Begin by invoking the ${skillName} skill.`; + + const agent = await initializeAgent( + { + workingDirectory: options.installDir, + workOSApiKey: apiKey, + workOSApiHost: 'https://api.workos.com', + }, + options, + ); + + const agentResult = await runAgent( + agent, + prompt, + options, + { + spinnerMessage: SPINNER_MESSAGE, + successMessage: config.ui.successMessage, + errorMessage: 'Integration failed', + }, + options.emitter, + ); + + if (agentResult.error) { + await analytics.shutdown('error'); + const message = agentResult.errorMessage || agentResult.error; + throw new Error(`Agent SDK error: ${message}`); + } + + // Post-installation validation + if (!options.noValidate) { + options.emitter?.emit('validation:start', { framework: config.metadata.integration }); + + const validationResult = await validateInstallation(config.metadata.integration, options.installDir, { + runBuild: true, + }); + + if (validationResult.issues.length > 0) { + options.emitter?.emit('validation:issues', { issues: validationResult.issues }); + } + + options.emitter?.emit('validation:complete', { + passed: validationResult.passed, + issueCount: validationResult.issues.length, + durationMs: validationResult.durationMs, + }); + } + + const envVars = config.environment.getEnvVars(apiKey, clientId); + + const changes = [ + ...config.ui.getOutroChanges({}), + Object.keys(envVars).length > 0 ? 'Configured WorkOS credentials in appsettings' : '', + ].filter(Boolean); + + const nextSteps = config.ui.getOutroNextSteps({}); + + const lines: string[] = [ + 'Successfully installed WorkOS AuthKit!', + '', + 'What the agent did:', + ...changes.map((c) => `• ${c}`), + '', + 'Next steps:', + ...nextSteps.map((s) => `• ${s}`), + '', + `Learn more: ${config.metadata.docsUrl}`, + '', + 'Note: This installer uses an LLM agent to analyze and modify your project. Please review the changes made.', + ]; + + await analytics.shutdown('success'); + + return lines.join('\n'); +} diff --git a/src/integrations/elixir/index.ts b/src/integrations/elixir/index.ts new file mode 100644 index 0000000..ea55f34 --- /dev/null +++ b/src/integrations/elixir/index.ts @@ -0,0 +1,183 @@ +/* Elixir/Phoenix integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; +import { SPINNER_MESSAGE } from '../../lib/framework-config.js'; +import { analytics } from '../../utils/analytics.js'; +import { INSTALLER_INTERACTION_EVENT_NAME } from '../../lib/constants.js'; +import { getOrAskForWorkOSCredentials } from '../../utils/clack-utils.js'; +import { initializeAgent, runAgent } from '../../lib/agent-interface.js'; +import { writeEnvLocal } from '../../lib/env-writer.js'; + +export const config: FrameworkConfig = { + metadata: { + name: 'Elixir (Phoenix)', + integration: 'elixir', + docsUrl: 'https://github.com/workos/workos-elixir', + skillName: 'workos-elixir', + language: 'elixir', + stability: 'experimental', + priority: 30, + packageManager: 'mix', + manifestFile: 'mix.exs', + }, + + detection: { + // Required by FrameworkDetection interface — stubs for non-JS integration. + // Actual detection uses language-detection.ts (mix.exs) + registry manifestFile check. + packageName: 'workos', + packageDisplayName: 'WorkOS Elixir', + getVersion: () => undefined, + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your Phoenix project structure', + 'Installed workos Hex package', + 'Configured WorkOS in config/runtime.exs', + 'Created auth controller and routes', + ], + getOutroNextSteps: () => [ + 'Run `mix phx.server` to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +/** + * Custom run function for Elixir — bypasses runAgentInstaller() which assumes + * package.json exists. Directly calls initializeAgent/runAgent instead. + */ +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + options.emitter?.emit('status', { + message: `Setting up WorkOS AuthKit for ${config.metadata.name}`, + }); + + analytics.capture(INSTALLER_INTERACTION_EVENT_NAME, { + action: 'started agent integration', + integration: config.metadata.integration, + }); + + // Get WorkOS credentials + const { apiKey, clientId } = await getOrAskForWorkOSCredentials(options, config.environment.requiresApiKey); + + // Write env vars to .env.local for the agent to reference + const callerHandledConfig = Boolean(options.apiKey || options.clientId); + if (!callerHandledConfig) { + const port = 4000; // Phoenix default + const callbackPath = '/auth/callback'; + const redirectUri = options.redirectUri || `http://localhost:${port}${callbackPath}`; + + writeEnvLocal(options.installDir, { + ...(apiKey ? { WORKOS_API_KEY: apiKey } : {}), + WORKOS_CLIENT_ID: clientId, + WORKOS_REDIRECT_URI: redirectUri, + }); + } + + // Build Elixir-specific prompt + const integrationPrompt = buildElixirPrompt(); + + // Initialize and run agent + const agent = await initializeAgent( + { + workingDirectory: options.installDir, + workOSApiKey: apiKey, + workOSApiHost: 'https://api.workos.com', + }, + options, + ); + + const agentResult = await runAgent( + agent, + integrationPrompt, + options, + { + spinnerMessage: SPINNER_MESSAGE, + successMessage: config.ui.successMessage, + errorMessage: 'Integration failed', + }, + options.emitter, + ); + + if (agentResult.error) { + await analytics.shutdown('error'); + const message = agentResult.errorMessage || agentResult.error; + throw new Error(`Agent SDK error: ${message}`); + } + + // Build summary + const changes = config.ui.getOutroChanges({}); + const nextSteps = config.ui.getOutroNextSteps({}); + + const lines: string[] = [ + 'Successfully installed WorkOS AuthKit!', + '', + 'What the agent did:', + ...changes.map((c) => `• ${c}`), + '', + 'Next steps:', + ...nextSteps.map((s) => `• ${s}`), + '', + `Learn more: ${config.metadata.docsUrl}`, + '', + 'Note: This installer uses an LLM agent to analyze and modify your project. Please review the changes made.', + ]; + + await analytics.shutdown('success'); + return lines.join('\n'); +} + +function buildElixirPrompt(): string { + return `You are integrating WorkOS AuthKit into this Elixir/Phoenix application. + +## Project Context + +- Framework: Phoenix (Elixir) +- Package manager: mix (Hex) + +## Environment + +The following environment variables have been configured in .env.local: +- WORKOS_API_KEY +- WORKOS_CLIENT_ID +- WORKOS_REDIRECT_URI + +Note: For Elixir/Phoenix, these should be read via System.get_env() in config/runtime.exs rather than from .env.local directly. + +## Your Task + +Use the \`workos-elixir\` skill to integrate WorkOS AuthKit into this application. + +The skill contains step-by-step instructions including: +1. Fetching the SDK documentation +2. Validating the Phoenix project structure +3. Installing the workos Hex package +4. Configuring WorkOS in runtime.exs +5. Creating auth controller and routes +6. Verification with mix compile + +Report your progress using [STATUS] prefixes. + +Begin by invoking the workos-elixir skill.`; +} diff --git a/src/integrations/go/index.ts b/src/integrations/go/index.ts new file mode 100644 index 0000000..bf2e6fe --- /dev/null +++ b/src/integrations/go/index.ts @@ -0,0 +1,263 @@ +/* Go integration — auto-discovered by registry */ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import { SPINNER_MESSAGE } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; +import { analytics } from '../../utils/analytics.js'; +import { INSTALLER_INTERACTION_EVENT_NAME } from '../../lib/constants.js'; +import { initializeAgent, runAgent } from '../../lib/agent-interface.js'; +import { getOrAskForWorkOSCredentials } from '../../utils/clack-utils.js'; +import { autoConfigureWorkOSEnvironment } from '../../lib/workos-management.js'; +import { validateInstallation } from '../../lib/validation/index.js'; +import { parseEnvFile } from '../../utils/env-parser.js'; + +/** Default port for Go HTTP servers */ +const GO_DEFAULT_PORT = 8080; +const GO_CALLBACK_PATH = '/auth/callback'; + +/** + * Detect whether go.mod includes the Gin web framework. + */ +function detectGoFramework(installDir: string): 'gin' | 'stdlib' { + try { + const goMod = readFileSync(join(installDir, 'go.mod'), 'utf-8'); + return goMod.includes('github.com/gin-gonic/gin') ? 'gin' : 'stdlib'; + } catch { + return 'stdlib'; + } +} + +/** + * Write environment variables to .env (Go convention, not .env.local). + * Merges with existing .env if present. + */ +function writeGoEnv(installDir: string, envVars: Record): void { + const envPath = join(installDir, '.env'); + let existing: Record = {}; + + if (existsSync(envPath)) { + existing = parseEnvFile(readFileSync(envPath, 'utf-8')); + } + + const merged = { ...existing, ...envVars }; + const content = Object.entries(merged) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + + writeFileSync(envPath, content + '\n'); +} + +export const config: FrameworkConfig = { + metadata: { + name: 'Go', + integration: 'go', + docsUrl: 'https://workos.com/docs/authkit/vanilla/go', + skillName: 'workos-go', + language: 'go', + stability: 'experimental', + priority: 50, + packageManager: 'go', + manifestFile: 'go.mod', + gatherContext: async (options) => { + return { framework: detectGoFramework(options.installDir) }; + }, + }, + + detection: { + packageName: 'github.com/workos/workos-go/v4', + packageDisplayName: 'Go', + getVersion: () => undefined, + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: (context: any) => (context?.framework ? { 'go-framework': context.framework } : {}), + }, + + prompts: { + getAdditionalContextLines: (context: any) => [ + ...(context?.framework ? [`Go web framework: ${context.framework}`] : []), + ], + }, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your Go project structure', + 'Installed workos-go SDK', + 'Created authentication handlers', + 'Configured environment variables', + ], + getOutroNextSteps: () => [ + 'Run `go run .` to start your server', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +/** + * Run the Go integration. + * + * Custom flow that bypasses runAgentInstaller because the universal runner + * assumes package.json exists (getPackageDotJson aborts without it) and + * port-detection/env-writer are JS-specific. + */ +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + options.emitter?.emit('status', { + message: `Setting up WorkOS AuthKit for ${config.metadata.name}`, + }); + + analytics.capture(INSTALLER_INTERACTION_EVENT_NAME, { + action: 'started agent integration', + integration: config.metadata.integration, + }); + + // Get WorkOS credentials + const { apiKey, clientId } = await getOrAskForWorkOSCredentials(options, config.environment.requiresApiKey); + + // Auto-configure WorkOS environment (redirect URI, CORS) + const callerHandledConfig = Boolean(options.apiKey || options.clientId); + if (!callerHandledConfig && apiKey) { + const redirectUri = options.redirectUri || `http://localhost:${GO_DEFAULT_PORT}${GO_CALLBACK_PATH}`; + await autoConfigureWorkOSEnvironment(apiKey, config.metadata.integration, GO_DEFAULT_PORT, { + homepageUrl: options.homepageUrl, + redirectUri, + }); + } + + // Gather Go-specific context + const frameworkContext = config.metadata.gatherContext ? await config.metadata.gatherContext(options) : {}; + + // Write .env (not .env.local — Go convention) + if (!callerHandledConfig) { + const redirectUri = options.redirectUri || `http://localhost:${GO_DEFAULT_PORT}${GO_CALLBACK_PATH}`; + writeGoEnv(options.installDir, { + ...(apiKey ? { WORKOS_API_KEY: apiKey } : {}), + WORKOS_CLIENT_ID: clientId, + WORKOS_REDIRECT_URI: redirectUri, + }); + } + + // Set analytics tags + const contextTags = config.analytics.getTags(frameworkContext); + Object.entries(contextTags).forEach(([key, value]) => { + analytics.setTag(key, value); + }); + + // Build prompt + const additionalLines = config.prompts.getAdditionalContextLines + ? config.prompts.getAdditionalContextLines(frameworkContext) + : []; + const additionalContext = + additionalLines.length > 0 ? '\n' + additionalLines.map((line) => `- ${line}`).join('\n') : ''; + + const skillName = config.metadata.skillName!; + const integrationPrompt = `You are integrating WorkOS AuthKit into this ${config.metadata.name} application. + +## Project Context + +- Language: Go +- Framework: ${frameworkContext.framework === 'gin' ? 'Gin' : 'stdlib net/http'}${additionalContext} + +## Environment + +The following environment variables have been configured in .env: +- WORKOS_API_KEY +- WORKOS_CLIENT_ID +- WORKOS_REDIRECT_URI + +## Your Task + +Use the \`${skillName}\` skill to integrate WorkOS AuthKit into this application. + +The skill contains step-by-step instructions including: +1. Fetching the SDK documentation +2. Installing the SDK +3. Detecting Gin vs stdlib +4. Creating authentication handlers +5. Wiring handlers into the router +6. Verification with go build and go vet + +Report your progress using [STATUS] prefixes. + +Begin by invoking the ${skillName} skill.`; + + // Initialize and run agent + const agent = await initializeAgent( + { + workingDirectory: options.installDir, + workOSApiKey: apiKey, + workOSApiHost: 'https://api.workos.com', + }, + options, + ); + + const agentResult = await runAgent( + agent, + integrationPrompt, + options, + { + spinnerMessage: SPINNER_MESSAGE, + successMessage: config.ui.successMessage, + errorMessage: 'Integration failed', + }, + options.emitter, + ); + + if (agentResult.error) { + await analytics.shutdown('error'); + const message = agentResult.errorMessage || agentResult.error; + throw new Error(`Agent SDK error: ${message}`); + } + + // Post-installation validation (gracefully skips — no rules file for Go) + if (!options.noValidate) { + options.emitter?.emit('validation:start', { framework: config.metadata.integration }); + + const validationResult = await validateInstallation(config.metadata.integration, options.installDir, { + runBuild: true, + }); + + if (validationResult.issues.length > 0) { + options.emitter?.emit('validation:issues', { issues: validationResult.issues }); + } + + options.emitter?.emit('validation:complete', { + passed: validationResult.passed, + issueCount: validationResult.issues.length, + durationMs: validationResult.durationMs, + }); + } + + // Build summary + const changes = config.ui.getOutroChanges(frameworkContext).filter(Boolean); + const nextSteps = config.ui.getOutroNextSteps(frameworkContext).filter(Boolean); + + const lines: string[] = [ + 'Successfully installed WorkOS AuthKit!', + '', + ...(changes.length > 0 ? ['What the agent did:', ...changes.map((c) => `• ${c}`), ''] : []), + ...(nextSteps.length > 0 ? ['Next steps:', ...nextSteps.map((s) => `• ${s}`), ''] : []), + `Learn more: ${config.metadata.docsUrl}`, + '', + 'Note: This installer uses an LLM agent to analyze and modify your project. Please review the changes made.', + ]; + + await analytics.shutdown('success'); + + return lines.join('\n'); +} diff --git a/src/integrations/kotlin/index.ts b/src/integrations/kotlin/index.ts new file mode 100644 index 0000000..102077e --- /dev/null +++ b/src/integrations/kotlin/index.ts @@ -0,0 +1,63 @@ +/* Kotlin (Spring Boot) integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; + +export const config: FrameworkConfig = { + metadata: { + name: 'Kotlin (Spring Boot)', + integration: 'kotlin', + docsUrl: 'https://github.com/workos/workos-kotlin', + skillName: 'workos-kotlin', + language: 'kotlin', + stability: 'experimental', + priority: 40, + packageManager: 'gradle', + manifestFile: 'build.gradle.kts', + }, + + detection: { + packageName: 'com.workos:workos-kotlin', + packageDisplayName: 'Kotlin (Spring Boot)', + getVersion: () => undefined, + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your Kotlin/Spring Boot project structure', + 'Added WorkOS Kotlin SDK dependency to build.gradle.kts', + 'Created authentication controller with login, callback, and logout endpoints', + 'Configured application.properties with WorkOS credentials', + ], + getOutroNextSteps: () => [ + 'Run ./gradlew bootRun to start your application', + 'Visit http://localhost:8080/auth/login to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + const { runAgentInstaller } = await import('../../lib/agent-runner.js'); + return runAgentInstaller(config, options); +} diff --git a/src/integrations/nextjs/index.ts b/src/integrations/nextjs/index.ts new file mode 100644 index 0000000..6e1bcdc --- /dev/null +++ b/src/integrations/nextjs/index.ts @@ -0,0 +1,106 @@ +/* Next.js integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; +import { getPackageVersion } from '../../utils/package-json.js'; +import { getPackageDotJson } from '../../utils/clack-utils.js'; +import clack from '../../utils/clack.js'; +import chalk from 'chalk'; +import * as semver from 'semver'; +import { getNextJsRouter, getNextJsVersionBucket, getNextJsRouterName, NextJsRouter } from './utils.js'; + +const MINIMUM_NEXTJS_VERSION = '15.3.0'; + +export const config: FrameworkConfig = { + metadata: { + name: 'Next.js', + integration: 'nextjs', + docsUrl: 'https://workos.com/docs/user-management/authkit/nextjs', + unsupportedVersionDocsUrl: 'https://workos.com/docs/user-management/authkit/nextjs', + skillName: 'workos-authkit-nextjs', + language: 'javascript', + stability: 'stable', + priority: 100, + gatherContext: async (options: InstallerOptions) => { + const router = await getNextJsRouter(options); + return { router }; + }, + }, + + detection: { + packageName: 'next', + packageDisplayName: 'Next.js', + getVersion: (packageJson: any) => getPackageVersion('next', packageJson), + getVersionBucket: getNextJsVersionBucket, + }, + + environment: { + uploadToHosting: true, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: (context: any) => { + const router = context.router as NextJsRouter; + return { + router: router === NextJsRouter.APP_ROUTER ? 'app' : 'pages', + }; + }, + }, + + prompts: { + getAdditionalContextLines: (context: any) => { + const router = context.router as NextJsRouter; + const routerType = router === NextJsRouter.APP_ROUTER ? 'app' : 'pages'; + return [`Router: ${routerType}`]; + }, + }, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: (context: any) => { + const router = context.router as NextJsRouter; + const routerName = getNextJsRouterName(router); + return [ + `Analyzed your Next.js project structure (${routerName})`, + `Created and configured WorkOS AuthKit`, + `Integrated authentication into your application`, + ]; + }, + getOutroNextSteps: () => [ + 'Start your development server to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + // Check Next.js version - agent wizard requires >= 15.3.0 + const packageJson = await getPackageDotJson(options); + const nextVersion = getPackageVersion('next', packageJson); + + if (nextVersion) { + const coercedVersion = semver.coerce(nextVersion); + if (coercedVersion && semver.lt(coercedVersion, MINIMUM_NEXTJS_VERSION)) { + const docsUrl = config.metadata.unsupportedVersionDocsUrl ?? config.metadata.docsUrl; + + clack.log.warn( + `Sorry: the installer can't help you with Next.js ${nextVersion}. Upgrade to Next.js ${MINIMUM_NEXTJS_VERSION} or later, or check out the manual setup guide.`, + ); + clack.log.info(`Setup Next.js manually: ${chalk.cyan(docsUrl)}`); + clack.outro('WorkOS AuthKit installer will see you next time!'); + return ''; + } + } + + const { runAgentInstaller } = await import('../../lib/agent-runner.js'); + return runAgentInstaller(config, options); +} diff --git a/src/integrations/nextjs/utils.ts b/src/integrations/nextjs/utils.ts new file mode 100644 index 0000000..e627faa --- /dev/null +++ b/src/integrations/nextjs/utils.ts @@ -0,0 +1,66 @@ +import fg from 'fast-glob'; +import { abortIfCancelled } from '../../utils/clack-utils.js'; +import clack from '../../utils/clack.js'; +import { getVersionBucket } from '../../utils/semver.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { IGNORE_PATTERNS } from '../../lib/constants.js'; + +export function getNextJsVersionBucket(version: string | undefined): string { + return getVersionBucket(version, 11); +} + +export enum NextJsRouter { + APP_ROUTER = 'app-router', + PAGES_ROUTER = 'pages-router', +} + +export async function getNextJsRouter({ installDir }: Pick): Promise { + const pagesMatches = await fg('**/pages/_app.@(ts|tsx|js|jsx)', { + dot: true, + cwd: installDir, + ignore: IGNORE_PATTERNS, + }); + + const hasPagesDir = pagesMatches.length > 0; + + const appMatches = await fg('**/app/**/layout.@(ts|tsx|js|jsx)', { + dot: true, + cwd: installDir, + ignore: IGNORE_PATTERNS, + }); + + const hasAppDir = appMatches.length > 0; + + if (hasPagesDir && !hasAppDir) { + clack.log.info(`Detected ${getNextJsRouterName(NextJsRouter.PAGES_ROUTER)} 📃`); + return NextJsRouter.PAGES_ROUTER; + } + + if (hasAppDir && !hasPagesDir) { + clack.log.info(`Detected ${getNextJsRouterName(NextJsRouter.APP_ROUTER)} 📱`); + return NextJsRouter.APP_ROUTER; + } + + const result: NextJsRouter = await abortIfCancelled( + clack.select({ + message: 'What router are you using?', + options: [ + { + label: getNextJsRouterName(NextJsRouter.APP_ROUTER), + value: NextJsRouter.APP_ROUTER, + }, + { + label: getNextJsRouterName(NextJsRouter.PAGES_ROUTER), + value: NextJsRouter.PAGES_ROUTER, + }, + ], + }), + 'nextjs', + ); + + return result; +} + +export const getNextJsRouterName = (router: NextJsRouter) => { + return router === NextJsRouter.APP_ROUTER ? 'app router' : 'pages router'; +}; diff --git a/src/integrations/node/index.ts b/src/integrations/node/index.ts new file mode 100644 index 0000000..7745ae0 --- /dev/null +++ b/src/integrations/node/index.ts @@ -0,0 +1,62 @@ +/* Node.js (Express) integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; +import { getPackageVersion } from '../../utils/package-json.js'; + +export const config: FrameworkConfig = { + metadata: { + name: 'Node.js (Express)', + integration: 'node', + docsUrl: 'https://workos.com/docs/authkit/vanilla/nodejs', + skillName: 'workos-node', + language: 'javascript', + stability: 'experimental', + priority: 70, + }, + + detection: { + packageName: 'express', + packageDisplayName: 'Express', + getVersion: (packageJson: any) => getPackageVersion('express', packageJson), + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your Express project structure', + 'Installed and configured @workos-inc/node SDK', + 'Created authentication routes (/auth/login, /auth/callback, /auth/logout)', + 'Configured session management', + ], + getOutroNextSteps: () => [ + 'Start your development server to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + 'For production, replace in-memory sessions with Redis or a database store', + ], + }, +}; + +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + const { runAgentInstaller } = await import('../../lib/agent-runner.js'); + return runAgentInstaller(config, options); +} diff --git a/src/integrations/php-laravel/index.ts b/src/integrations/php-laravel/index.ts new file mode 100644 index 0000000..3001fcd --- /dev/null +++ b/src/integrations/php-laravel/index.ts @@ -0,0 +1,61 @@ +/* PHP Laravel integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; + +export const config: FrameworkConfig = { + metadata: { + name: 'PHP (Laravel)', + integration: 'php-laravel', + docsUrl: 'https://github.com/workos/workos-php-laravel', + skillName: 'workos-php-laravel', + language: 'php', + stability: 'experimental', + priority: 45, + packageManager: 'composer', + manifestFile: 'composer.json', + }, + + detection: { + packageName: 'laravel/framework', + packageDisplayName: 'Laravel', + getVersion: (packageJson: any) => packageJson?.require?.['laravel/framework'], + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your Laravel project structure', + 'Installed and configured WorkOS AuthKit Laravel SDK', + 'Created authentication controller and routes', + ], + getOutroNextSteps: () => [ + 'Run `php artisan serve` to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + const { runAgentInstaller } = await import('../../lib/agent-runner.js'); + return runAgentInstaller(config, options); +} diff --git a/src/integrations/php/index.ts b/src/integrations/php/index.ts new file mode 100644 index 0000000..608e6f3 --- /dev/null +++ b/src/integrations/php/index.ts @@ -0,0 +1,61 @@ +/* Generic PHP integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; + +export const config: FrameworkConfig = { + metadata: { + name: 'PHP', + integration: 'php', + docsUrl: 'https://github.com/workos/workos-php', + skillName: 'workos-php', + language: 'php', + stability: 'experimental', + priority: 44, + packageManager: 'composer', + manifestFile: 'composer.json', + }, + + detection: { + packageName: 'php', + packageDisplayName: 'PHP', + getVersion: () => undefined, + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your PHP project structure', + 'Installed and configured WorkOS PHP SDK', + 'Created authentication endpoint files', + ], + getOutroNextSteps: () => [ + 'Start your PHP development server to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + const { runAgentInstaller } = await import('../../lib/agent-runner.js'); + return runAgentInstaller(config, options); +} diff --git a/src/integrations/python/index.ts b/src/integrations/python/index.ts new file mode 100644 index 0000000..00ebe76 --- /dev/null +++ b/src/integrations/python/index.ts @@ -0,0 +1,288 @@ +/* Python/Django integration — auto-discovered by registry */ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; +import { analytics } from '../../utils/analytics.js'; +import { INSTALLER_INTERACTION_EVENT_NAME } from '../../lib/constants.js'; +import { parseEnvFile } from '../../utils/env-parser.js'; + +/** + * Detect which Python package manager the project uses. + */ +function detectPythonPackageManager(installDir: string): { name: string; installCmd: string } { + if (existsSync(join(installDir, 'uv.lock'))) { + return { name: 'uv', installCmd: 'uv add' }; + } + + if (existsSync(join(installDir, 'pyproject.toml'))) { + try { + const content = readFileSync(join(installDir, 'pyproject.toml'), 'utf-8'); + if (content.includes('[tool.poetry]')) { + return { name: 'poetry', installCmd: 'poetry add' }; + } + } catch { + /* ignore */ + } + } + + if (existsSync(join(installDir, 'Pipfile'))) { + return { name: 'pipenv', installCmd: 'pipenv install' }; + } + + return { name: 'pip', installCmd: 'pip install' }; +} + +/** + * Detect if this is a Django project. + */ +function isDjangoProject(installDir: string): boolean { + if (existsSync(join(installDir, 'manage.py'))) return true; + + const pyprojectPath = join(installDir, 'pyproject.toml'); + if (existsSync(pyprojectPath)) { + try { + const content = readFileSync(pyprojectPath, 'utf-8'); + if (/django/i.test(content)) return true; + } catch { + /* ignore */ + } + } + + const reqPath = join(installDir, 'requirements.txt'); + if (existsSync(reqPath)) { + try { + const content = readFileSync(reqPath, 'utf-8'); + if (/^django/im.test(content)) return true; + } catch { + /* ignore */ + } + } + + return false; +} + +/** + * Write .env file for Python projects (not .env.local). + * Merges with existing .env. No cookie password generation. + */ +function writeEnvFile(installDir: string, envVars: Record): void { + const envPath = join(installDir, '.env'); + + let existingEnv: Record = {}; + if (existsSync(envPath)) { + try { + existingEnv = parseEnvFile(readFileSync(envPath, 'utf-8')); + } catch { + /* ignore */ + } + } + + const merged = { ...existingEnv, ...envVars }; + const content = Object.entries(merged) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + + writeFileSync(envPath, content + '\n'); +} + +export const config: FrameworkConfig = { + metadata: { + name: 'Python (Django)', + integration: 'python', + docsUrl: 'https://workos.com/docs/user-management/authkit/vanilla/python', + skillName: 'workos-python', + language: 'python', + stability: 'experimental', + priority: 60, + packageManager: 'pip', + manifestFile: 'pyproject.toml', + gatherContext: async (options: InstallerOptions) => { + const pkgMgr = detectPythonPackageManager(options.installDir); + return { + packageManager: pkgMgr.name, + installCommand: pkgMgr.installCmd, + isDjango: isDjangoProject(options.installDir), + }; + }, + }, + + detection: { + // Dummy values for FrameworkDetection interface compat — Python doesn't use package.json + packageName: 'workos', + packageDisplayName: 'Python (Django)', + getVersion: () => undefined, + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: (context: any) => ({ + 'python-package-manager': context?.packageManager || 'unknown', + 'python-is-django': String(context?.isDjango ?? false), + }), + }, + + prompts: { + getAdditionalContextLines: (context: any) => { + const lines: string[] = []; + if (context?.packageManager) lines.push(`Package manager: ${context.packageManager}`); + if (context?.installCommand) lines.push(`Install command: ${context.installCommand}`); + if (context?.isDjango) lines.push('Framework: Django'); + return lines; + }, + }, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your Python/Django project structure', + 'Installed WorkOS Python SDK', + 'Created authentication views (login, callback, logout)', + 'Configured URL routing and environment variables', + ], + getOutroNextSteps: () => [ + 'Run `python manage.py runserver` to test authentication', + 'Visit http://localhost:8000/auth/login to test the login flow', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +/** + * Build the agent prompt for Python/Django integration. + */ +function buildPythonPrompt(frameworkContext: Record): string { + const contextLines = ['- Framework: Python (Django)']; + if (frameworkContext.packageManager) contextLines.push(`- Package manager: ${frameworkContext.packageManager}`); + if (frameworkContext.installCommand) contextLines.push(`- Install command: ${frameworkContext.installCommand}`); + + const skillName = config.metadata.skillName!; + + return `You are integrating WorkOS AuthKit into this Python/Django application. + +## Project Context + +${contextLines.join('\n')} + +## Environment + +The following environment variables have been configured in .env: +- WORKOS_API_KEY +- WORKOS_CLIENT_ID + +## Your Task + +Use the \`${skillName}\` skill to integrate WorkOS AuthKit into this application. + +The skill contains step-by-step instructions including: +1. Fetching the SDK documentation +2. Installing the SDK and python-dotenv +3. Configuring Django settings +4. Creating authentication views +5. Setting up URL routing +6. Adding authentication UI + +Report your progress using [STATUS] prefixes. + +Begin by invoking the ${skillName} skill.`; +} + +/** + * Custom run function that bypasses runAgentInstaller. + * Calls initializeAgent + runAgent directly, handling Python-specific + * env writing and prompt building. + */ +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + options.emitter?.emit('status', { + message: 'Setting up WorkOS AuthKit for Python (Django)', + }); + + const apiKey = options.apiKey || ''; + const clientId = options.clientId || ''; + + // Gather Python-specific context + const frameworkContext = config.metadata.gatherContext ? await config.metadata.gatherContext(options) : {}; + + analytics.capture(INSTALLER_INTERACTION_EVENT_NAME, { + action: 'started agent integration', + integration: 'python', + }); + + // Set analytics tags + const contextTags = config.analytics.getTags(frameworkContext); + for (const [key, value] of Object.entries(contextTags)) { + analytics.setTag(key, value); + } + + // Write .env (not .env.local) with WorkOS credentials + writeEnvFile(options.installDir, { + ...(apiKey ? { WORKOS_API_KEY: apiKey } : {}), + WORKOS_CLIENT_ID: clientId, + }); + + // Build Python-specific prompt + const prompt = buildPythonPrompt(frameworkContext); + + // Initialize and run agent directly (bypass runAgentInstaller) + const { initializeAgent, runAgent } = await import('../../lib/agent-interface.js'); + + const agentConfig = await initializeAgent( + { + workingDirectory: options.installDir, + workOSApiKey: apiKey, + workOSApiHost: 'https://api.workos.com', + }, + options, + ); + + const result = await runAgent( + agentConfig, + prompt, + options, + { + spinnerMessage: 'Setting up WorkOS AuthKit for Python/Django...', + successMessage: config.ui.successMessage, + errorMessage: 'Python integration failed', + }, + options.emitter, + ); + + if (result.error) { + await analytics.shutdown('error'); + throw new Error(`Agent error: ${result.errorMessage || result.error}`); + } + + // Build completion summary + const changes = config.ui.getOutroChanges({}); + const nextSteps = config.ui.getOutroNextSteps({}); + + const lines: string[] = [ + 'Successfully installed WorkOS AuthKit!', + '', + 'What the agent did:', + ...changes.map((c) => `• ${c}`), + '', + 'Next steps:', + ...nextSteps.map((s) => `• ${s}`), + '', + `Learn more: ${config.metadata.docsUrl}`, + '', + 'Note: This installer uses an LLM agent to analyze and modify your project. Please review the changes made.', + ]; + + await analytics.shutdown('success'); + return lines.join('\n'); +} diff --git a/src/integrations/react-router/index.ts b/src/integrations/react-router/index.ts new file mode 100644 index 0000000..6423e3a --- /dev/null +++ b/src/integrations/react-router/index.ts @@ -0,0 +1,113 @@ +/* React Router integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; +import { getPackageVersion } from '../../utils/package-json.js'; +import { getPackageDotJson } from '../../utils/clack-utils.js'; +import clack from '../../utils/clack.js'; +import chalk from 'chalk'; +import * as semver from 'semver'; +import { getReactRouterMode, getReactRouterModeName, getReactRouterVersionBucket, ReactRouterMode } from './utils.js'; + +const MINIMUM_REACT_ROUTER_VERSION = '6.0.0'; + +export const config: FrameworkConfig = { + metadata: { + name: 'React Router', + integration: 'react-router', + docsUrl: 'https://workos.com/docs/user-management/authkit/react-router', + unsupportedVersionDocsUrl: 'https://workos.com/docs/user-management/authkit/react-router', + skillName: 'workos-authkit-react-router', + language: 'javascript', + stability: 'stable', + priority: 80, + gatherContext: async (options: InstallerOptions) => { + const routerMode = await getReactRouterMode(options); + return { routerMode }; + }, + }, + + detection: { + packageName: 'react-router', + packageDisplayName: 'React Router', + getVersion: (packageJson: any) => getPackageVersion('react-router', packageJson), + getVersionBucket: getReactRouterVersionBucket, + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: (context: any) => { + const routerMode = context.routerMode as ReactRouterMode; + return { routerMode: routerMode || 'unknown' }; + }, + }, + + prompts: { + getAdditionalContextLines: (context: any) => { + const routerMode = context.routerMode as ReactRouterMode; + const modeName = routerMode ? getReactRouterModeName(routerMode) : 'unknown'; + + const frameworkIdMap: Record = { + [ReactRouterMode.V6]: 'react-react-router-6', + [ReactRouterMode.V7_FRAMEWORK]: 'react-react-router-7-framework', + [ReactRouterMode.V7_DATA]: 'react-react-router-7-data', + [ReactRouterMode.V7_DECLARATIVE]: 'react-react-router-7-declarative', + }; + + const frameworkId = routerMode ? frameworkIdMap[routerMode] : ReactRouterMode.V7_FRAMEWORK; + + return [`Router mode: ${modeName}`, `Framework docs ID: ${frameworkId}`]; + }, + }, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: (context: any) => { + const routerMode = context.routerMode as ReactRouterMode; + const modeName = routerMode ? getReactRouterModeName(routerMode) : 'React Router'; + return [ + `Analyzed your React Router project structure (${modeName})`, + `Created and configured WorkOS AuthKit`, + `Integrated authentication into your application`, + ]; + }, + getOutroNextSteps: () => [ + 'Start your development server to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + const packageJson = await getPackageDotJson(options); + const reactRouterVersion = getPackageVersion('react-router', packageJson); + + if (reactRouterVersion) { + const coercedVersion = semver.coerce(reactRouterVersion); + if (coercedVersion && semver.lt(coercedVersion, MINIMUM_REACT_ROUTER_VERSION)) { + const docsUrl = config.metadata.unsupportedVersionDocsUrl ?? config.metadata.docsUrl; + + clack.log.warn( + `Sorry: the installer can't help you with React Router ${reactRouterVersion}. Upgrade to React Router ${MINIMUM_REACT_ROUTER_VERSION} or later, or check out the manual setup guide.`, + ); + clack.log.info(`Setup React Router manually: ${chalk.cyan(docsUrl)}`); + clack.outro('WorkOS AuthKit installer will see you next time!'); + return ''; + } + } + + const { runAgentInstaller } = await import('../../lib/agent-runner.js'); + return runAgentInstaller(config, options); +} diff --git a/src/integrations/react-router/utils.ts b/src/integrations/react-router/utils.ts new file mode 100644 index 0000000..3680097 --- /dev/null +++ b/src/integrations/react-router/utils.ts @@ -0,0 +1,173 @@ +import { major } from 'semver'; +import fg from 'fast-glob'; +import { abortIfCancelled, getPackageDotJson } from '../../utils/clack-utils.js'; +import clack from '../../utils/clack.js'; +import { getVersionBucket } from '../../utils/semver.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { IGNORE_PATTERNS } from '../../lib/constants.js'; +import { getPackageVersion } from '../../utils/package-json.js'; +import chalk from 'chalk'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as semver from 'semver'; + +export enum ReactRouterMode { + V6 = 'v6', + V7_FRAMEWORK = 'v7-framework', + V7_DATA = 'v7-data', + V7_DECLARATIVE = 'v7-declarative', +} + +export function getReactRouterVersionBucket(version: string | undefined): string { + return getVersionBucket(version, 6); +} + +async function hasReactRouterConfig({ installDir }: Pick): Promise { + const configMatches = await fg('**/react-router.config.@(ts|js|tsx|jsx)', { + dot: true, + cwd: installDir, + ignore: IGNORE_PATTERNS, + }); + return configMatches.length > 0; +} + +async function hasCreateBrowserRouter({ installDir }: Pick): Promise { + const sourceFiles = await fg('**/*.@(ts|tsx|js|jsx)', { + dot: true, + cwd: installDir, + ignore: IGNORE_PATTERNS, + }); + + for (const file of sourceFiles) { + try { + const filePath = path.join(installDir, file); + const content = fs.readFileSync(filePath, 'utf-8'); + if (content.includes('createBrowserRouter')) { + return true; + } + } catch { + continue; + } + } + return false; +} + +async function hasDeclarativeRouter({ installDir }: Pick): Promise { + const sourceFiles = await fg('**/*.@(ts|tsx|js|jsx)', { + dot: true, + cwd: installDir, + ignore: IGNORE_PATTERNS, + }); + + for (const file of sourceFiles) { + try { + const filePath = path.join(installDir, file); + const content = fs.readFileSync(filePath, 'utf-8'); + if ( + content.includes(' { + const { installDir } = options; + + const packageJson = await getPackageDotJson(options); + const reactRouterVersion = + getPackageVersion('react-router-dom', packageJson) || getPackageVersion('react-router', packageJson); + + if (!reactRouterVersion) { + clack.log.info(`Learn more about React Router modes: ${chalk.cyan('https://reactrouter.com/start/modes')}`); + const result: ReactRouterMode = await abortIfCancelled( + clack.select({ + message: 'What React Router version and mode are you using?', + options: [ + { label: 'React Router v6', value: ReactRouterMode.V6 }, + { label: 'React Router v7 - Framework mode', value: ReactRouterMode.V7_FRAMEWORK }, + { label: 'React Router v7 - Data mode', value: ReactRouterMode.V7_DATA }, + { label: 'React Router v7 - Declarative mode', value: ReactRouterMode.V7_DECLARATIVE }, + ], + }), + 'react-router', + ); + return result; + } + + const coercedVersion = semver.coerce(reactRouterVersion); + const majorVersion = coercedVersion ? major(coercedVersion) : null; + + if (majorVersion === 6) { + clack.log.info('Detected React Router v6'); + return ReactRouterMode.V6; + } + + if (majorVersion === 7) { + const hasConfig = await hasReactRouterConfig({ installDir }); + if (hasConfig) { + clack.log.info('Detected React Router v7 - Framework mode'); + return ReactRouterMode.V7_FRAMEWORK; + } + + const hasDataMode = await hasCreateBrowserRouter({ installDir }); + if (hasDataMode) { + clack.log.info('Detected React Router v7 - Data mode'); + return ReactRouterMode.V7_DATA; + } + + const hasDeclarative = await hasDeclarativeRouter({ installDir }); + if (hasDeclarative) { + clack.log.info('Detected React Router v7 - Declarative mode'); + return ReactRouterMode.V7_DECLARATIVE; + } + + clack.log.info(`Learn more about React Router modes: ${chalk.cyan('https://reactrouter.com/start/modes')}`); + const result: ReactRouterMode = await abortIfCancelled( + clack.select({ + message: 'What React Router v7 mode are you using?', + options: [ + { label: 'Framework mode', value: ReactRouterMode.V7_FRAMEWORK }, + { label: 'Data mode', value: ReactRouterMode.V7_DATA }, + { label: 'Declarative mode', value: ReactRouterMode.V7_DECLARATIVE }, + ], + }), + 'react-router', + ); + return result; + } + + clack.log.info(`Learn more about React Router modes: ${chalk.cyan('https://reactrouter.com/start/modes')}`); + const result: ReactRouterMode = await abortIfCancelled( + clack.select({ + message: 'What React Router version and mode are you using?', + options: [ + { label: 'React Router v6', value: ReactRouterMode.V6 }, + { label: 'React Router v7 - Framework mode', value: ReactRouterMode.V7_FRAMEWORK }, + { label: 'React Router v7 - Data mode', value: ReactRouterMode.V7_DATA }, + { label: 'React Router v7 - Declarative mode', value: ReactRouterMode.V7_DECLARATIVE }, + ], + }), + 'react-router', + ); + return result; +} + +export function getReactRouterModeName(mode: ReactRouterMode): string { + switch (mode) { + case ReactRouterMode.V6: + return 'v6'; + case ReactRouterMode.V7_FRAMEWORK: + return 'v7 Framework mode'; + case ReactRouterMode.V7_DATA: + return 'v7 Data mode'; + case ReactRouterMode.V7_DECLARATIVE: + return 'v7 Declarative mode'; + } +} diff --git a/src/integrations/react/index.ts b/src/integrations/react/index.ts new file mode 100644 index 0000000..4cbebe2 --- /dev/null +++ b/src/integrations/react/index.ts @@ -0,0 +1,59 @@ +/* React SPA integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; + +export const config: FrameworkConfig = { + metadata: { + name: 'React (SPA)', + integration: 'react', + docsUrl: 'https://workos.com/docs/user-management/authkit/react', + unsupportedVersionDocsUrl: 'https://workos.com/docs/user-management/authkit/react', + skillName: 'workos-authkit-react', + language: 'javascript', + stability: 'stable', + priority: 70, + }, + + detection: { + packageName: 'react', + packageDisplayName: 'React', + getVersion: (packageJson: any) => packageJson.dependencies?.react || packageJson.devDependencies?.react, + }, + + environment: { + uploadToHosting: false, + requiresApiKey: false, + getEnvVars: (_apiKey: string, clientId: string) => ({ + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your React project structure', + 'Created and configured WorkOS AuthKit', + 'Integrated authentication into your application', + ], + getOutroNextSteps: () => [ + 'Start your development server to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + const { runAgentInstaller } = await import('../../lib/agent-runner.js'); + return runAgentInstaller(config, options); +} diff --git a/src/integrations/ruby/index.ts b/src/integrations/ruby/index.ts new file mode 100644 index 0000000..bbd4c70 --- /dev/null +++ b/src/integrations/ruby/index.ts @@ -0,0 +1,172 @@ +/* Ruby/Rails integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; +import { SPINNER_MESSAGE } from '../../lib/framework-config.js'; +import { analytics } from '../../utils/analytics.js'; +import { INSTALLER_INTERACTION_EVENT_NAME } from '../../lib/constants.js'; +import { initializeAgent, runAgent } from '../../lib/agent-interface.js'; +import { getOrAskForWorkOSCredentials } from '../../utils/clack-utils.js'; +import { autoConfigureWorkOSEnvironment } from '../../lib/workos-management.js'; + +export const config: FrameworkConfig = { + metadata: { + name: 'Ruby (Rails)', + integration: 'ruby', + docsUrl: 'https://workos.com/docs/authkit/vanilla/ruby', + skillName: 'workos-ruby', + language: 'ruby', + stability: 'experimental', + priority: 55, + packageManager: 'bundle', + manifestFile: 'Gemfile', + }, + + detection: { + packageName: 'rails', + packageDisplayName: 'Rails', + getVersion: () => undefined, + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your Rails project structure', + 'Installed and configured the WorkOS Ruby SDK', + 'Created authentication controller with login, callback, and logout', + 'Added authentication routes to config/routes.rb', + ], + getOutroNextSteps: () => [ + 'Start your Rails server with `rails server` to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +/** + * Custom run function for Ruby/Rails — bypasses runAgentInstaller + * since that assumes a JS project (package.json, node_modules, .env.local). + */ +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + options.emitter?.emit('status', { + message: `Setting up WorkOS AuthKit for ${config.metadata.name}`, + }); + + analytics.capture(INSTALLER_INTERACTION_EVENT_NAME, { + action: 'started agent integration', + integration: config.metadata.integration, + }); + + // Get WorkOS credentials + const { apiKey, clientId } = await getOrAskForWorkOSCredentials(options, config.environment.requiresApiKey); + + // Auto-configure WorkOS environment (redirect URI, CORS, homepage) if not already done + const callerHandledConfig = Boolean(options.apiKey || options.clientId); + if (!callerHandledConfig && apiKey) { + const port = 3000; // Rails default + await autoConfigureWorkOSEnvironment(apiKey, config.metadata.integration, port, { + homepageUrl: options.homepageUrl, + redirectUri: options.redirectUri, + }); + } + + // Build prompt for the agent + const redirectUri = options.redirectUri || 'http://localhost:3000/auth/callback'; + const prompt = `You are integrating WorkOS AuthKit into this Ruby on Rails application. + +## Project Context + +- Framework: Ruby (Rails) +- Language: Ruby + +## Environment + +The following environment variables should be configured in a .env file: +- WORKOS_API_KEY=${apiKey ? '(provided)' : '(not set)'} +- WORKOS_CLIENT_ID=${clientId || '(not set)'} +- WORKOS_REDIRECT_URI=${redirectUri} + +## Your Task + +Use the \`workos-ruby\` skill to integrate WorkOS AuthKit into this application. + +The skill contains step-by-step instructions including: +1. Fetching the SDK documentation +2. Installing the WorkOS Ruby gem +3. Creating the WorkOS initializer +4. Creating the AuthController with login, callback, and logout +5. Adding authentication routes + +Report your progress using [STATUS] prefixes. + +Begin by invoking the workos-ruby skill.`; + + // Initialize and run agent + const agent = await initializeAgent( + { + workingDirectory: options.installDir, + workOSApiKey: apiKey, + workOSApiHost: 'https://api.workos.com', + }, + options, + ); + + const agentResult = await runAgent( + agent, + prompt, + options, + { + spinnerMessage: SPINNER_MESSAGE, + successMessage: config.ui.successMessage, + errorMessage: 'Integration failed', + }, + options.emitter, + ); + + if (agentResult.error) { + await analytics.shutdown('error'); + const message = agentResult.errorMessage || agentResult.error; + throw new Error(`Agent SDK error: ${message}`); + } + + // Build completion summary + const changes = config.ui.getOutroChanges({}); + const nextSteps = config.ui.getOutroNextSteps({}); + + const lines: string[] = [ + 'Successfully installed WorkOS AuthKit!', + '', + 'What the agent did:', + ...changes.map((c) => `• ${c}`), + '', + 'Next steps:', + ...nextSteps.map((s) => `• ${s}`), + '', + `Learn more: ${config.metadata.docsUrl}`, + '', + 'Note: This installer uses an LLM agent to analyze and modify your project. Please review the changes made.', + ]; + + await analytics.shutdown('success'); + + return lines.join('\n'); +} diff --git a/src/integrations/sveltekit/index.ts b/src/integrations/sveltekit/index.ts new file mode 100644 index 0000000..f61862f --- /dev/null +++ b/src/integrations/sveltekit/index.ts @@ -0,0 +1,60 @@ +/* SvelteKit integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; +import { getPackageVersion } from '../../utils/package-json.js'; + +export const config: FrameworkConfig = { + metadata: { + name: 'SvelteKit', + integration: 'sveltekit', + docsUrl: 'https://github.com/workos/authkit-sveltekit', + skillName: 'workos-authkit-sveltekit', + language: 'javascript', + stability: 'experimental', + priority: 85, + }, + + detection: { + packageName: '@sveltejs/kit', + packageDisplayName: 'SvelteKit', + getVersion: (packageJson: any) => getPackageVersion('@sveltejs/kit', packageJson), + }, + + environment: { + uploadToHosting: true, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your SvelteKit project structure', + 'Created and configured WorkOS AuthKit', + 'Integrated authentication into your application', + ], + getOutroNextSteps: () => [ + 'Start your development server to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + const { runAgentInstaller } = await import('../../lib/agent-runner.js'); + return runAgentInstaller(config, options); +} diff --git a/src/integrations/tanstack-start/index.ts b/src/integrations/tanstack-start/index.ts new file mode 100644 index 0000000..046c0ff --- /dev/null +++ b/src/integrations/tanstack-start/index.ts @@ -0,0 +1,61 @@ +/* TanStack Start integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; +import { getPackageVersion } from '../../utils/package-json.js'; + +export const config: FrameworkConfig = { + metadata: { + name: 'TanStack Start', + integration: 'tanstack-start', + docsUrl: 'https://workos.com/docs/user-management/authkit/tanstack-start', + unsupportedVersionDocsUrl: 'https://workos.com/docs/user-management/authkit/tanstack-start', + skillName: 'workos-authkit-tanstack-start', + language: 'javascript', + stability: 'stable', + priority: 90, + }, + + detection: { + packageName: '@tanstack/react-start', + packageDisplayName: 'TanStack Start', + getVersion: (packageJson: any) => getPackageVersion('@tanstack/react-start', packageJson), + }, + + environment: { + uploadToHosting: false, + requiresApiKey: true, + getEnvVars: (apiKey: string, clientId: string) => ({ + WORKOS_API_KEY: apiKey, + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Analyzed your TanStack Start project structure', + 'Created and configured WorkOS AuthKit', + 'Integrated authentication into your application', + ], + getOutroNextSteps: () => [ + 'Start your development server to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + const { runAgentInstaller } = await import('../../lib/agent-runner.js'); + return runAgentInstaller(config, options); +} diff --git a/src/integrations/vanilla-js/index.ts b/src/integrations/vanilla-js/index.ts new file mode 100644 index 0000000..71b8b0c --- /dev/null +++ b/src/integrations/vanilla-js/index.ts @@ -0,0 +1,59 @@ +/* Vanilla JavaScript integration — auto-discovered by registry */ +import type { FrameworkConfig } from '../../lib/framework-config.js'; +import type { InstallerOptions } from '../../utils/types.js'; +import { enableDebugLogs } from '../../utils/debug.js'; + +export const config: FrameworkConfig = { + metadata: { + name: 'Vanilla JavaScript', + integration: 'vanilla-js', + docsUrl: 'https://workos.com/docs/user-management/authkit/javascript', + unsupportedVersionDocsUrl: 'https://workos.com/docs/user-management/authkit/javascript', + skillName: 'workos-authkit-vanilla-js', + language: 'javascript', + stability: 'stable', + priority: 10, // Lowest — fallback for any JS project + }, + + detection: { + packageName: 'workos', + packageDisplayName: 'Vanilla JavaScript', + getVersion: () => undefined, + }, + + environment: { + uploadToHosting: false, + requiresApiKey: false, + getEnvVars: (_apiKey: string, clientId: string) => ({ + WORKOS_CLIENT_ID: clientId, + }), + }, + + analytics: { + getTags: () => ({}), + }, + + prompts: {}, + + ui: { + successMessage: 'WorkOS AuthKit integration complete', + getOutroChanges: () => [ + 'Created WorkOS AuthKit integration', + 'Added authentication to your JavaScript application', + 'Set up login/logout functionality', + ], + getOutroNextSteps: () => [ + 'Start your development server to test authentication', + 'Visit the WorkOS Dashboard to manage users and settings', + ], + }, +}; + +export async function run(options: InstallerOptions): Promise { + if (options.debug) { + enableDebugLogs(); + } + + const { runAgentInstaller } = await import('../../lib/agent-runner.js'); + return runAgentInstaller(config, options); +} diff --git a/src/lib/agent-interface.ts b/src/lib/agent-interface.ts index f7e7e10..9022b3a 100644 --- a/src/lib/agent-interface.ts +++ b/src/lib/agent-interface.ts @@ -85,8 +85,48 @@ type AgentRunConfig = { /** * Package managers that can be used to run commands. + * Includes JS and non-JS ecosystem package managers for multi-SDK support. */ -const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun', 'npx']; +const PACKAGE_MANAGERS = [ + // JavaScript + 'npm', + 'pnpm', + 'yarn', + 'bun', + 'npx', + 'pnpx', + 'bunx', + // Python + 'pip', + 'pip3', + 'poetry', + 'uv', + 'pipx', + 'python', + 'python3', + // Ruby + 'gem', + 'bundle', + 'bundler', + 'ruby', + // PHP + 'composer', + 'php', + // Go + 'go', + // .NET + 'dotnet', + 'nuget', + // Elixir + 'mix', + 'hex', + 'elixir', + // Kotlin/Java + 'gradle', + 'gradlew', + './gradlew', + 'mvn', +]; /** * Safe scripts/commands that can be run with any package manager. @@ -109,6 +149,31 @@ const SAFE_SCRIPTS = [ // Linting/formatting script names (actual tools are in LINTING_TOOLS) 'lint', 'format', + // Common cross-language commands + 'check', + 'test', + 'run', + 'serve', + 'dev', + 'start', + 'compile', + 'vet', + // Python-specific + 'manage.py', + 'pytest', + // Ruby-specific + 'rspec', + 'rake', + 'routes', + // PHP-specific + 'artisan', + 'phpunit', + // Elixir-specific + 'deps.get', + 'credo', + 'dialyzer', + // .NET-specific + 'restore', ]; /** diff --git a/src/lib/config.ts b/src/lib/config.ts index 08f7ab9..d013e95 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,9 +1,23 @@ +/** + * Integration detection configuration. + * + * This module previously held hardcoded INTEGRATION_CONFIG and INTEGRATION_ORDER. + * These are now provided by the auto-discovery registry (src/lib/registry.ts). + * + * This file is kept for backwards compatibility — functions that previously + * used INTEGRATION_CONFIG now delegate to the registry. + */ + import { getPackageDotJson } from '../utils/clack-utils.js'; import { hasPackageInstalled } from '../utils/package-json.js'; import type { InstallerOptions } from '../utils/types.js'; -import { Integration } from './constants.js'; +import type { Integration } from './constants.js'; -type IntegrationConfig = { +/** + * @deprecated Use registry.detectionOrder() + config.detection.detect() instead. + * This type is kept for any code that still references it. + */ +export type IntegrationConfig = { name: string; filterPatterns: string[]; ignorePatterns: string[]; @@ -15,98 +29,30 @@ type IntegrationConfig = { defaultChanges: string; }; -export const INTEGRATION_CONFIG = { - [Integration.nextjs]: { +/** + * Legacy detection configs for existing JS integrations. + * Used by clack-utils.ts for abort/cancel messages. + * New integrations do NOT need to be added here. + */ +export const INTEGRATION_CONFIG: Record = { + nextjs: { name: 'Next.js', - filterPatterns: ['**/*.{tsx,ts,jsx,js,mjs,cjs}'], - ignorePatterns: ['node_modules', 'dist', 'build', 'public', 'static', 'next-env.d.*'], - detect: async (options) => { - const packageJson = await getPackageDotJson(options); - return hasPackageInstalled('next', packageJson); - }, - generateFilesRules: '', - filterFilesRules: '', docsUrl: 'https://workos.com/docs/user-management/authkit/nextjs', - defaultChanges: - '• Installed @workos/authkit-nextjs package\n• Initialized WorkOS AuthKit with your credentials\n• Created authentication routes and callbacks\n• Added login/logout UI components', - nextSteps: - '• Customize the auth UI to match your app design\n• Add protected routes using withAuth() middleware\n• Access user session with getUser() in your components', }, - [Integration.react]: { + react: { name: 'React (SPA)', - filterPatterns: ['**/*.{tsx,ts,jsx,js}'], - ignorePatterns: ['node_modules', 'dist', 'build', 'public', 'static', 'assets'], - detect: async (options) => { - const packageJson = await getPackageDotJson(options); - // Detect React without routing frameworks - const hasReact = hasPackageInstalled('react', packageJson); - const hasNext = hasPackageInstalled('next', packageJson); - const hasReactRouter = hasPackageInstalled('react-router', packageJson); - const hasTanstack = hasPackageInstalled('@tanstack/react-start', packageJson); - return hasReact && !hasNext && !hasReactRouter && !hasTanstack; - }, - generateFilesRules: '', - filterFilesRules: '', docsUrl: 'https://workos.com/docs/user-management/authkit/react', - defaultChanges: - '• Installed @workos/authkit-react package\n• Added AuthKitProvider to wrap your app\n• Created login and callback components', - nextSteps: - '• Use useAuth() hook to access auth state in components\n• Add protected routes with conditional rendering\n• Customize auth UI to match your design', }, - [Integration.tanstackStart]: { + 'tanstack-start': { name: 'TanStack Start', - filterPatterns: ['**/*.{tsx,ts,jsx,js}'], - ignorePatterns: ['node_modules', 'dist', 'build', '.vinxi', '.output'], - detect: async (options) => { - const packageJson = await getPackageDotJson(options); - return hasPackageInstalled('@tanstack/react-start', packageJson); - }, - generateFilesRules: '', - filterFilesRules: '', docsUrl: 'https://workos.com/docs/user-management/authkit/tanstack-start', - defaultChanges: - '• Installed WorkOS AuthKit SDK package\n• Added AuthKit middleware and routes\n• Created authentication components', - nextSteps: - '• Use useAuth() hook to access auth state\n• Add protected routes with authentication checks\n• Customize auth UI to match your app', }, - [Integration.reactRouter]: { + 'react-router': { name: 'React Router', - filterPatterns: ['**/*.{tsx,ts,jsx,js}'], - ignorePatterns: ['node_modules', 'dist', 'build', 'public', 'static', 'assets'], - detect: async (options) => { - const packageJson = await getPackageDotJson(options); - return hasPackageInstalled('react-router', packageJson); - }, - generateFilesRules: '', - filterFilesRules: '', docsUrl: 'https://workos.com/docs/user-management/authkit/react-router', - defaultChanges: - '• Installed @workos/authkit-react-router package\n• Added AuthKitProvider with React Router integration\n• Created auth routes and loaders', - nextSteps: - '• Use useAuth() hook in your components\n• Add protected routes with loader functions\n• Customize auth flow to match your needs', }, - [Integration.vanillaJs]: { + 'vanilla-js': { name: 'Vanilla JavaScript', - filterPatterns: ['**/*.{html,js,ts}'], - ignorePatterns: ['node_modules', 'dist', 'build'], - detect: async (options) => { - // Fallback: if no framework detected, assume vanilla JS - return true; - }, - generateFilesRules: '', - filterFilesRules: '', docsUrl: 'https://workos.com/docs/user-management/authkit/javascript', - defaultChanges: - '• Installed @workos/authkit-js package (or added CDN script)\n• Created auth initialization and callback handling\n• Added login button and auth state display', - nextSteps: - '• Integrate auth state into your existing UI\n• Add logout functionality where needed\n• Protect pages that require authentication', }, -} as const satisfies Record; - -export const INTEGRATION_ORDER = [ - Integration.nextjs, - Integration.tanstackStart, - Integration.reactRouter, - Integration.react, - Integration.vanillaJs, // fallback -] as const; +}; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index e083a15..cc05e97 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,41 +1,23 @@ import { getConfig } from './settings.js'; -export enum Integration { - nextjs = 'nextjs', - react = 'react', - tanstackStart = 'tanstack-start', - reactRouter = 'react-router', - vanillaJs = 'vanilla-js', -} - -export function getIntegrationDescription(type: string): string { - switch (type) { - case Integration.nextjs: - return 'Next.js'; - case Integration.react: - return 'React (SPA)'; - case Integration.tanstackStart: - return 'TanStack Start'; - case Integration.reactRouter: - return 'React Router'; - case Integration.vanillaJs: - return 'Vanilla JavaScript'; - default: - throw new Error(`Unknown integration ${type}`); - } -} - -type IntegrationChoice = { - name: string; - value: string; -}; +/** + * Integration identifier type. + * No longer an enum — each integration self-registers via the auto-discovery registry. + * The string value matches the integration directory name (e.g., 'nextjs', 'react-router'). + */ +export type Integration = string; -export function getIntegrationChoices(): IntegrationChoice[] { - return Object.keys(Integration).map((type: string) => ({ - name: getIntegrationDescription(type), - value: type, - })); -} +/** + * Well-known integration names for backwards compatibility. + * New integrations do NOT need to be added here — they're auto-discovered. + */ +export const KNOWN_INTEGRATIONS = { + nextjs: 'nextjs', + react: 'react', + tanstackStart: 'tanstack-start', + reactRouter: 'react-router', + vanillaJs: 'vanilla-js', +} as const; export interface Args { debug: boolean; @@ -57,7 +39,7 @@ export const OAUTH_PORT = settings.legacy.oauthPort; /** * Common glob patterns to ignore when searching for files. - * Used by both Next.js and React Router integrations. + * Used by multiple integrations. */ export const IGNORE_PATTERNS: string[] = [ '**/node_modules/**', diff --git a/src/lib/framework-config.ts b/src/lib/framework-config.ts index 26f7815..bdb33fb 100644 --- a/src/lib/framework-config.ts +++ b/src/lib/framework-config.ts @@ -1,5 +1,5 @@ -import type { Integration } from './constants.js'; import type { InstallerOptions } from '../utils/types.js'; +import type { Language } from './language-detection.js'; /** * Configuration interface for framework-specific agent integrations. @@ -21,8 +21,8 @@ export interface FrameworkMetadata { /** Display name (e.g., "Next.js", "React") */ name: string; - /** Integration type from constants */ - integration: Integration; + /** Integration identifier (e.g., 'nextjs', 'python'). String, not enum — auto-discovered from registry. */ + integration: string; /** URL to framework-specific WorkOS AuthKit docs */ docsUrl: string; @@ -43,9 +43,23 @@ export interface FrameworkMetadata { /** * Name of the framework-specific skill for agent integration. * Skills are located in .claude/skills/{skillName}/SKILL.md - * Will be populated per-framework in Phase 3. */ skillName?: string; + + /** Language ecosystem this integration belongs to */ + language: Language; + + /** Stability tier: 'stable' for tested integrations, 'experimental' for new ones */ + stability: 'stable' | 'experimental'; + + /** Detection priority — higher numbers are checked first */ + priority: number; + + /** Default package manager command (e.g., 'pip', 'gem', 'go'). Optional for JS integrations. */ + packageManager?: string; + + /** Primary manifest file (e.g., 'pyproject.toml', 'Gemfile'). Optional for JS integrations. */ + manifestFile?: string; } /** diff --git a/src/lib/installer-core.events.spec.ts b/src/lib/installer-core.events.spec.ts index 0941166..b1ba052 100644 --- a/src/lib/installer-core.events.spec.ts +++ b/src/lib/installer-core.events.spec.ts @@ -27,7 +27,6 @@ import { installerMachine } from './installer-core.js'; import { createEventCapture, compareEventSequences, filterDeterministicEvents } from './installer-core.test-utils.js'; import type { InstallerOptions } from '../utils/types.js'; import type { DetectionOutput, GitCheckOutput, AgentOutput, InstallerMachineContext } from './installer-core.types.js'; -import { Integration } from './constants.js'; /** * Creates mock actor implementations for testing. @@ -39,7 +38,7 @@ function createMockActors() { return { checkAuthentication: fromPromise(async () => true), detectIntegration: fromPromise(async () => ({ - integration: Integration.nextjs, + integration: 'nextjs', })), checkGitStatus: fromPromise(async () => ({ isClean: true, diff --git a/src/lib/installer-core.spec.ts b/src/lib/installer-core.spec.ts index 6a402c5..90dfe97 100644 --- a/src/lib/installer-core.spec.ts +++ b/src/lib/installer-core.spec.ts @@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest'; import { createActor, fromPromise } from 'xstate'; import { installerMachine } from './installer-core.js'; import { createInstallerEventEmitter } from './events.js'; -import { Integration } from './constants.js'; import type { InstallerOptions } from '../utils/types.js'; import type { DetectionOutput, @@ -16,7 +15,7 @@ import type { const baseMockActors = { checkAuthentication: fromPromise(async () => true), detectIntegration: fromPromise(async () => ({ - integration: Integration.nextjs, + integration: 'nextjs', })), checkGitStatus: fromPromise(async () => ({ isClean: true, diff --git a/src/lib/language-detection.ts b/src/lib/language-detection.ts new file mode 100644 index 0000000..96c6da8 --- /dev/null +++ b/src/lib/language-detection.ts @@ -0,0 +1,125 @@ +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +/** + * Supported programming languages for framework detection. + */ +export type Language = 'javascript' | 'python' | 'ruby' | 'php' | 'go' | 'kotlin' | 'dotnet' | 'elixir'; + +export interface LanguageSignal { + language: Language; + confidence: number; // 0-1 + manifestFile: string; +} + +export interface LanguageDetectionResult { + primary: Language; + signals: LanguageSignal[]; + ambiguous: boolean; +} + +function fileExists(cwd: string, filename: string): { found: boolean; manifestFile: string } { + const fullPath = join(cwd, filename); + return { found: existsSync(fullPath), manifestFile: filename }; +} + +function globExists(cwd: string, pattern: string): { found: boolean; manifestFile: string } { + // Simple glob for *.ext patterns in the root directory + const ext = pattern.replace('*', ''); + try { + const files = readdirSync(cwd); + const match = files.find((f) => f.endsWith(ext)); + return { found: !!match, manifestFile: match || pattern }; + } catch { + return { found: false, manifestFile: pattern }; + } +} + +function detectPython(cwd: string): { found: boolean; manifestFile: string } { + for (const file of ['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile']) { + if (existsSync(join(cwd, file))) { + return { found: true, manifestFile: file }; + } + } + return { found: false, manifestFile: 'pyproject.toml' }; +} + +function detectKotlin(cwd: string): { found: boolean; manifestFile: string } { + const ktsPath = join(cwd, 'build.gradle.kts'); + if (existsSync(ktsPath)) { + try { + const content = readFileSync(ktsPath, 'utf-8'); + if (/org\.jetbrains\.kotlin/.test(content) || /kotlin\(/.test(content)) { + return { found: true, manifestFile: 'build.gradle.kts' }; + } + } catch { + // Can't read file + } + } + + // Also check build.gradle (Groovy DSL) + const gradlePath = join(cwd, 'build.gradle'); + if (existsSync(gradlePath)) { + try { + const content = readFileSync(gradlePath, 'utf-8'); + if (/kotlin/.test(content)) { + return { found: true, manifestFile: 'build.gradle' }; + } + } catch { + // Can't read file + } + } + + return { found: false, manifestFile: 'build.gradle.kts' }; +} + +/** + * Language detectors ordered by specificity. + * More specific languages are checked first. + * JavaScript is last because many non-JS projects also have package.json. + */ +const LANGUAGE_DETECTORS: Array<{ + language: Language; + detect: (cwd: string) => { found: boolean; manifestFile: string }; +}> = [ + { language: 'elixir', detect: (cwd) => fileExists(cwd, 'mix.exs') }, + { language: 'go', detect: (cwd) => fileExists(cwd, 'go.mod') }, + { language: 'dotnet', detect: (cwd) => globExists(cwd, '*.csproj') }, + { language: 'kotlin', detect: detectKotlin }, + { language: 'ruby', detect: (cwd) => fileExists(cwd, 'Gemfile') }, + { language: 'php', detect: (cwd) => fileExists(cwd, 'composer.json') }, + { language: 'python', detect: detectPython }, + { language: 'javascript', detect: (cwd) => fileExists(cwd, 'package.json') }, +]; + +/** + * Detect the primary programming language of a project. + * Runs all detectors and returns the highest-priority match. + * Sets `ambiguous: true` if multiple non-JS languages are detected. + */ +export function detectLanguage(cwd: string): LanguageDetectionResult | undefined { + const signals: LanguageSignal[] = []; + + for (const detector of LANGUAGE_DETECTORS) { + const result = detector.detect(cwd); + if (result.found) { + signals.push({ + language: detector.language, + confidence: 1.0, + manifestFile: result.manifestFile, + }); + } + } + + if (signals.length === 0) { + return undefined; + } + + const primary = signals[0].language; + + // Ambiguous if multiple non-JS languages detected + const nonJsSignals = signals.filter((s) => s.language !== 'javascript'); + const ambiguous = nonJsSignals.length > 1; + + return { primary, signals, ambiguous }; +} diff --git a/src/lib/port-detection.ts b/src/lib/port-detection.ts index eb94c7b..f8f6477 100644 --- a/src/lib/port-detection.ts +++ b/src/lib/port-detection.ts @@ -13,14 +13,17 @@ const INTEGRATION_TO_SETTINGS_KEY: Record = { 'vanilla-js': 'vanillaJs', }; +const DEFAULT_PORT = 3000; +const DEFAULT_CALLBACK_PATH = '/auth/callback'; + function getDefaultPort(integration: Integration): number { const settingsKey = INTEGRATION_TO_SETTINGS_KEY[integration]; - return settings.frameworks[settingsKey].port; + return settings.frameworks[settingsKey]?.port ?? DEFAULT_PORT; } export function getCallbackPath(integration: Integration): string { const settingsKey = INTEGRATION_TO_SETTINGS_KEY[integration]; - return settings.frameworks[settingsKey].callbackPath; + return settings.frameworks[settingsKey]?.callbackPath ?? DEFAULT_CALLBACK_PATH; } /** diff --git a/src/lib/registry.ts b/src/lib/registry.ts new file mode 100644 index 0000000..9ea4edd --- /dev/null +++ b/src/lib/registry.ts @@ -0,0 +1,147 @@ +import { readdirSync, existsSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { FrameworkConfig } from './framework-config.js'; +import type { Language } from './language-detection.js'; +import type { InstallerOptions } from '../utils/types.js'; + +/** + * Standard exports from an integration module. + * Each `src/integrations/{name}/index.ts` must export these. + */ +export interface IntegrationModule { + config: FrameworkConfig; + run: (options: InstallerOptions) => Promise; +} + +/** + * Registry that provides lookup, detection, and enumeration of integrations. + */ +export interface IntegrationRegistry { + /** All registered integrations */ + all(): FrameworkConfig[]; + + /** Get config by integration name */ + get(name: string): IntegrationModule | undefined; + + /** Get integrations for a specific language, ordered by priority */ + forLanguage(language: Language): FrameworkConfig[]; + + /** Get integration names for CLI choices */ + choices(): Array<{ name: string; value: string }>; + + /** Detection order: all integrations sorted by priority (higher = checked first) */ + detectionOrder(): FrameworkConfig[]; +} + +/** + * Build the integration registry by discovering all integration modules. + * Scans `src/integrations/` (or `dist/integrations/` at runtime) for directories + * with an index.js/index.ts file and dynamically imports them. + */ +export async function buildRegistry(): Promise { + const modules = new Map(); + + // Resolve the integrations directory relative to this file + // In dev: src/lib/registry.ts -> src/integrations/ + // In dist: dist/lib/registry.js -> dist/integrations/ + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const integrationsDir = join(__dirname, '..', 'integrations'); + + if (!existsSync(integrationsDir)) { + throw new Error(`No integrations directory found at ${integrationsDir}. Is the build corrupt?`); + } + + const entries = readdirSync(integrationsDir, { withFileTypes: true }); + const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); + + if (dirs.length === 0) { + throw new Error('No integrations found. Is the build corrupt?'); + } + + for (const dir of dirs) { + // Skip directories starting with _ (convention for internal files like _manifest.ts) + if (dir.startsWith('_')) continue; + + const indexPath = join(integrationsDir, dir, 'index.js'); + const indexTsPath = join(integrationsDir, dir, 'index.ts'); + + if (!existsSync(indexPath) && !existsSync(indexTsPath)) { + // Skip directories without an index file (not an integration) + continue; + } + + try { + const mod = (await import(join(integrationsDir, dir, 'index.js'))) as IntegrationModule; + + if (!mod.config || !mod.run) { + console.warn(`Integration ${dir} missing 'config' or 'run' export, skipping`); + continue; + } + + const name = mod.config.metadata.integration; + + if (modules.has(name)) { + throw new Error(`Duplicate integration name: '${name}' (found in both existing and '${dir}/')`); + } + + modules.set(name, mod); + } catch (err) { + if (err instanceof Error && err.message.startsWith('Duplicate integration name')) { + throw err; // Re-throw duplicate name errors + } + console.warn(`Failed to load integration from ${dir}/: ${err}`); + } + } + + // Build sorted config array (by priority, descending) + const sortedConfigs = Array.from(modules.values()) + .map((m) => m.config) + .sort((a, b) => b.metadata.priority - a.metadata.priority); + + return { + all() { + return sortedConfigs; + }, + + get(name: string) { + return modules.get(name); + }, + + forLanguage(language: Language) { + return sortedConfigs.filter((c) => c.metadata.language === language); + }, + + choices() { + return sortedConfigs.map((c) => ({ + name: c.metadata.name, + value: c.metadata.integration, + })); + }, + + detectionOrder() { + return sortedConfigs; + }, + }; +} + +// Singleton cache +let _registry: IntegrationRegistry | null = null; + +/** + * Get the integration registry (builds once, caches thereafter). + */ +export async function getRegistry(): Promise { + if (!_registry) { + _registry = await buildRegistry(); + } + return _registry; +} + +/** + * Reset the registry cache. Used in tests. + */ +export function resetRegistry(): void { + _registry = null; +} diff --git a/src/lib/run-with-core.ts b/src/lib/run-with-core.ts index 89350a8..949f01f 100644 --- a/src/lib/run-with-core.ts +++ b/src/lib/run-with-core.ts @@ -15,7 +15,7 @@ import type { AgentOutput, BranchCheckOutput, } from './installer-core.types.js'; -import { Integration } from './constants.js'; +import type { Integration } from './constants.js'; import { parseEnvFile } from '../utils/env-parser.js'; import { enableDebugLogs, initLogFile, logInfo, logError } from '../utils/debug.js'; @@ -46,31 +46,18 @@ import { generateCommitMessage as generateCommitMessageAi, generatePrDescription as generatePrDescriptionAi, } from './ai-content.js'; -import { INTEGRATION_CONFIG, INTEGRATION_ORDER } from './config.js'; import { autoConfigureWorkOSEnvironment } from './workos-management.js'; import { detectPort, getCallbackPath } from './port-detection.js'; import { writeEnvLocal } from './env-writer.js'; -import { runNextjsInstallerAgent } from '../nextjs/nextjs-installer-agent.js'; -import { runReactInstallerAgent } from '../react/react-installer-agent.js'; -import { runReactRouterInstallerAgent } from '../react-router/react-router-installer-agent.js'; -import { runTanstackStartInstallerAgent } from '../tanstack-start/tanstack-start-installer-agent.js'; -import { runVanillaJsInstallerAgent } from '../vanilla-js/vanilla-js-installer-agent.js'; +import { getRegistry } from './registry.js'; async function runIntegrationInstallerFn(integration: Integration, options: InstallerOptions): Promise { - switch (integration) { - case Integration.nextjs: - return runNextjsInstallerAgent(options); - case Integration.react: - return runReactInstallerAgent(options); - case Integration.reactRouter: - return runReactRouterInstallerAgent(options); - case Integration.tanstackStart: - return runTanstackStartInstallerAgent(options); - case Integration.vanillaJs: - return runVanillaJsInstallerAgent(options); - default: - throw new Error(`Unknown integration: ${integration}`); + const registry = await getRegistry(); + const mod = registry.get(integration); + if (!mod) { + throw new Error(`Unknown integration: ${integration}`); } + return mod.run(options); } function readExistingCredentials(installDir: string): { apiKey?: string; clientId?: string } { @@ -92,19 +79,85 @@ function readExistingCredentials(installDir: string): { apiKey?: string; clientI } async function detectIntegrationFn(options: Pick): Promise { - const integrationConfigs = Object.entries(INTEGRATION_CONFIG).sort( - ([a], [b]) => INTEGRATION_ORDER.indexOf(a as Integration) - INTEGRATION_ORDER.indexOf(b as Integration), - ); + const registry = await getRegistry(); + const configs = registry.detectionOrder(); - for (const [integration, config] of integrationConfigs) { - const detected = await config.detect(options); + for (const config of configs) { + // Use the detect function from INTEGRATION_CONFIG in config.ts for JS integrations, + // or fall back to checking if the framework package is installed + const detected = await detectSingleIntegration(config.metadata.integration, options); if (detected) { - return integration as Integration; + return config.metadata.integration; } } return undefined; } +/** + * Detect if a single integration matches the project. + * Uses package.json detection for JS integrations, manifest files for others. + */ +async function detectSingleIntegration( + integration: string, + options: Pick, +): Promise { + const { getPackageDotJson } = await import('../utils/clack-utils.js'); + const { hasPackageInstalled } = await import('../utils/package-json.js'); + const { existsSync } = await import('node:fs'); + const { join } = await import('node:path'); + + const registry = await getRegistry(); + const mod = registry.get(integration); + if (!mod) return false; + + const config = mod.config; + + // For JS integrations, check package.json + if (config.metadata.language === 'javascript') { + const packageJson = await getPackageDotJson(options); + + switch (integration) { + case 'nextjs': + return hasPackageInstalled('next', packageJson); + case 'tanstack-start': + return hasPackageInstalled('@tanstack/react-start', packageJson); + case 'react-router': + return hasPackageInstalled('react-router', packageJson); + case 'react': { + const hasReact = hasPackageInstalled('react', packageJson); + const hasNext = hasPackageInstalled('next', packageJson); + const hasReactRouter = hasPackageInstalled('react-router', packageJson); + const hasTanstack = hasPackageInstalled('@tanstack/react-start', packageJson); + const hasSvelteKit = hasPackageInstalled('@sveltejs/kit', packageJson); + return hasReact && !hasNext && !hasReactRouter && !hasTanstack && !hasSvelteKit; + } + case 'sveltekit': + return hasPackageInstalled('@sveltejs/kit', packageJson); + case 'node': { + const hasExpress = hasPackageInstalled('express', packageJson); + const hasFrontend = + hasPackageInstalled('next', packageJson) || + hasPackageInstalled('@sveltejs/kit', packageJson) || + hasPackageInstalled('react', packageJson) || + hasPackageInstalled('@tanstack/react-start', packageJson); + return hasExpress && !hasFrontend; + } + case 'vanilla-js': + return true; // Fallback + default: + // Unknown JS integration — try package name detection + return hasPackageInstalled(config.detection.packageName, packageJson); + } + } + + // For non-JS integrations, check manifest files + if (config.metadata.manifestFile) { + return existsSync(join(options.installDir, config.metadata.manifestFile)); + } + + return false; +} + export async function runWithCore(options: InstallerOptions): Promise { // Initialize debug/logging early so we capture all failures initLogFile(); @@ -188,9 +241,7 @@ export async function runWithCore(options: InstallerOptions): Promise { const callbackPath = getCallbackPath(integration); const redirectUri = installerOptions.redirectUri || `http://localhost:${port}${callbackPath}`; - const requiresApiKey = [Integration.nextjs, Integration.tanstackStart, Integration.reactRouter].includes( - integration, - ); + const requiresApiKey = ['nextjs', 'tanstack-start', 'react-router'].includes(integration); if (credentials.apiKey && requiresApiKey) { await autoConfigureWorkOSEnvironment(credentials.apiKey, integration, port, { homepageUrl: installerOptions.homepageUrl, @@ -198,8 +249,7 @@ export async function runWithCore(options: InstallerOptions): Promise { }); } - const redirectUriKey = - integration === Integration.nextjs ? 'NEXT_PUBLIC_WORKOS_REDIRECT_URI' : 'WORKOS_REDIRECT_URI'; + const redirectUriKey = integration === 'nextjs' ? 'NEXT_PUBLIC_WORKOS_REDIRECT_URI' : 'WORKOS_REDIRECT_URI'; writeEnvLocal(installerOptions.installDir, { ...(credentials.apiKey ? { WORKOS_API_KEY: credentials.apiKey } : {}), diff --git a/src/nextjs/nextjs-installer-agent.ts b/src/nextjs/nextjs-installer-agent.ts index 0313374..5cc561c 100644 --- a/src/nextjs/nextjs-installer-agent.ts +++ b/src/nextjs/nextjs-installer-agent.ts @@ -1,112 +1,5 @@ -/* Simplified Next.js wizard using Claude Agent SDK with WorkOS MCP */ -import type { InstallerOptions } from '../utils/types.js'; -import { enableDebugLogs } from '../utils/debug.js'; -import { runAgentInstaller } from '../lib/agent-runner.js'; -import { Integration } from '../lib/constants.js'; -import { getPackageVersion } from '../utils/package-json.js'; -import { getPackageDotJson } from '../utils/clack-utils.js'; -import clack from '../utils/clack.js'; -import chalk from 'chalk'; -import * as semver from 'semver'; -import { getNextJsRouter, getNextJsVersionBucket, getNextJsRouterName, NextJsRouter } from './utils.js'; - /** - * Next.js framework configuration for the universal agent runner. + * @deprecated Import from 'src/integrations/nextjs/index.js' instead. + * This file is kept for backwards compatibility. */ -const MINIMUM_NEXTJS_VERSION = '15.3.0'; - -const NEXTJS_AGENT_CONFIG = { - metadata: { - name: 'Next.js', - integration: Integration.nextjs, - docsUrl: 'https://workos.com/docs/user-management/authkit/nextjs', - unsupportedVersionDocsUrl: 'https://workos.com/docs/user-management/authkit/nextjs', - skillName: 'workos-authkit-nextjs', - gatherContext: async (options: InstallerOptions) => { - const router = await getNextJsRouter(options); - return { router }; - }, - }, - - detection: { - packageName: 'next', - packageDisplayName: 'Next.js', - getVersion: (packageJson: any) => getPackageVersion('next', packageJson), - getVersionBucket: getNextJsVersionBucket, - }, - - environment: { - uploadToHosting: true, - requiresApiKey: true, // Server-side framework - getEnvVars: (apiKey: string, clientId: string) => ({ - WORKOS_API_KEY: apiKey, - WORKOS_CLIENT_ID: clientId, - }), - }, - - analytics: { - getTags: (context: any) => { - const router = context.router as NextJsRouter; - return { - router: router === NextJsRouter.APP_ROUTER ? 'app' : 'pages', - }; - }, - }, - - prompts: { - getAdditionalContextLines: (context: any) => { - const router = context.router as NextJsRouter; - const routerType = router === NextJsRouter.APP_ROUTER ? 'app' : 'pages'; - return [`Router: ${routerType}`]; - }, - }, - - ui: { - successMessage: 'WorkOS AuthKit integration complete', - getOutroChanges: (context: any) => { - const router = context.router as NextJsRouter; - const routerName = getNextJsRouterName(router); - return [ - `Analyzed your Next.js project structure (${routerName})`, - `Created and configured WorkOS AuthKit`, - `Integrated authentication into your application`, - ]; - }, - getOutroNextSteps: () => { - return [ - 'Start your development server to test authentication', - 'Visit the WorkOS Dashboard to manage users and settings', - ]; - }, - }, -}; - -/** - * Next.js wizard powered by the universal agent runner. - * @returns Summary of what was done, or empty string if version check fails - */ -export async function runNextjsInstallerAgent(options: InstallerOptions): Promise { - if (options.debug) { - enableDebugLogs(); - } - - // Check Next.js version - agent wizard requires >= 15.3.0 - const packageJson = await getPackageDotJson(options); - const nextVersion = getPackageVersion('next', packageJson); - - if (nextVersion) { - const coercedVersion = semver.coerce(nextVersion); - if (coercedVersion && semver.lt(coercedVersion, MINIMUM_NEXTJS_VERSION)) { - const docsUrl = NEXTJS_AGENT_CONFIG.metadata.unsupportedVersionDocsUrl ?? NEXTJS_AGENT_CONFIG.metadata.docsUrl; - - clack.log.warn( - `Sorry: the installer can't help you with Next.js ${nextVersion}. Upgrade to Next.js ${MINIMUM_NEXTJS_VERSION} or later, or check out the manual setup guide.`, - ); - clack.log.info(`Setup Next.js manually: ${chalk.cyan(docsUrl)}`); - clack.outro('WorkOS AuthKit installer will see you next time!'); - return ''; - } - } - - return runAgentInstaller(NEXTJS_AGENT_CONFIG, options); -} +export { run as runNextjsInstallerAgent, config as NEXTJS_AGENT_CONFIG } from '../integrations/nextjs/index.js'; diff --git a/src/nextjs/utils.ts b/src/nextjs/utils.ts index d84d730..c68ecb2 100644 --- a/src/nextjs/utils.ts +++ b/src/nextjs/utils.ts @@ -1,66 +1,9 @@ -import fg from 'fast-glob'; -import { abortIfCancelled } from '../utils/clack-utils.js'; -import clack from '../utils/clack.js'; -import { getVersionBucket } from '../utils/semver.js'; -import type { InstallerOptions } from '../utils/types.js'; -import { IGNORE_PATTERNS, Integration } from '../lib/constants.js'; - -export function getNextJsVersionBucket(version: string | undefined): string { - return getVersionBucket(version, 11); -} - -export enum NextJsRouter { - APP_ROUTER = 'app-router', - PAGES_ROUTER = 'pages-router', -} - -export async function getNextJsRouter({ installDir }: Pick): Promise { - const pagesMatches = await fg('**/pages/_app.@(ts|tsx|js|jsx)', { - dot: true, - cwd: installDir, - ignore: IGNORE_PATTERNS, - }); - - const hasPagesDir = pagesMatches.length > 0; - - const appMatches = await fg('**/app/**/layout.@(ts|tsx|js|jsx)', { - dot: true, - cwd: installDir, - ignore: IGNORE_PATTERNS, - }); - - const hasAppDir = appMatches.length > 0; - - if (hasPagesDir && !hasAppDir) { - clack.log.info(`Detected ${getNextJsRouterName(NextJsRouter.PAGES_ROUTER)} 📃`); - return NextJsRouter.PAGES_ROUTER; - } - - if (hasAppDir && !hasPagesDir) { - clack.log.info(`Detected ${getNextJsRouterName(NextJsRouter.APP_ROUTER)} 📱`); - return NextJsRouter.APP_ROUTER; - } - - const result: NextJsRouter = await abortIfCancelled( - clack.select({ - message: 'What router are you using?', - options: [ - { - label: getNextJsRouterName(NextJsRouter.APP_ROUTER), - value: NextJsRouter.APP_ROUTER, - }, - { - label: getNextJsRouterName(NextJsRouter.PAGES_ROUTER), - value: NextJsRouter.PAGES_ROUTER, - }, - ], - }), - Integration.nextjs, - ); - - return result; -} - -export const getNextJsRouterName = (router: NextJsRouter) => { - return router === NextJsRouter.APP_ROUTER ? 'app router' : 'pages router'; -}; +/** + * @deprecated Import from 'src/integrations/nextjs/utils.js' instead. + */ +export { + getNextJsVersionBucket, + NextJsRouter, + getNextJsRouter, + getNextJsRouterName, +} from '../integrations/nextjs/utils.js'; diff --git a/src/react-router/react-router-installer-agent.ts b/src/react-router/react-router-installer-agent.ts index 29d0b9c..c507b6b 100644 --- a/src/react-router/react-router-installer-agent.ts +++ b/src/react-router/react-router-installer-agent.ts @@ -1,123 +1,7 @@ -/* React Router wizard using Claude Agent SDK with WorkOS MCP */ -import type { InstallerOptions } from '../utils/types.js'; -import type { FrameworkConfig } from '../lib/framework-config.js'; -import { enableDebugLogs } from '../utils/debug.js'; -import { runAgentInstaller } from '../lib/agent-runner.js'; -import { Integration } from '../lib/constants.js'; -import { getPackageVersion } from '../utils/package-json.js'; -import { getPackageDotJson } from '../utils/clack-utils.js'; -import clack from '../utils/clack.js'; -import chalk from 'chalk'; -import * as semver from 'semver'; -import { getReactRouterMode, getReactRouterModeName, getReactRouterVersionBucket, ReactRouterMode } from './utils.js'; - /** - * React Router framework configuration for the universal agent runner. + * @deprecated Import from 'src/integrations/react-router/index.js' instead. */ -const MINIMUM_REACT_ROUTER_VERSION = '6.0.0'; - -const REACT_ROUTER_AGENT_CONFIG: FrameworkConfig = { - metadata: { - name: 'React Router', - integration: Integration.reactRouter, - docsUrl: 'https://workos.com/docs/user-management/authkit/react-router', - unsupportedVersionDocsUrl: 'https://workos.com/docs/user-management/authkit/react-router', - skillName: 'workos-authkit-react-router', - gatherContext: async (options: InstallerOptions) => { - const routerMode = await getReactRouterMode(options); - return { routerMode }; - }, - }, - - detection: { - packageName: 'react-router', - packageDisplayName: 'React Router', - getVersion: (packageJson: any) => getPackageVersion('react-router', packageJson), - getVersionBucket: getReactRouterVersionBucket, - }, - - environment: { - uploadToHosting: false, - requiresApiKey: true, // Can do SSR - getEnvVars: (apiKey: string, clientId: string) => ({ - WORKOS_API_KEY: apiKey, - WORKOS_CLIENT_ID: clientId, - }), - }, - - analytics: { - getTags: (context: any) => { - const routerMode = context.routerMode as ReactRouterMode; - return { - routerMode: routerMode || 'unknown', - }; - }, - }, - - prompts: { - getAdditionalContextLines: (context: any) => { - const routerMode = context.routerMode as ReactRouterMode; - const modeName = routerMode ? getReactRouterModeName(routerMode) : 'unknown'; - - // Map router mode to framework ID for MCP docs resource - const frameworkIdMap: Record = { - [ReactRouterMode.V6]: 'react-react-router-6', - [ReactRouterMode.V7_FRAMEWORK]: 'react-react-router-7-framework', - [ReactRouterMode.V7_DATA]: 'react-react-router-7-data', - [ReactRouterMode.V7_DECLARATIVE]: 'react-react-router-7-declarative', - }; - - const frameworkId = routerMode ? frameworkIdMap[routerMode] : ReactRouterMode.V7_FRAMEWORK; - - return [`Router mode: ${modeName}`, `Framework docs ID: ${frameworkId}`]; - }, - }, - - ui: { - successMessage: 'WorkOS AuthKit integration complete', - getOutroChanges: (context: any) => { - const routerMode = context.routerMode as ReactRouterMode; - const modeName = routerMode ? getReactRouterModeName(routerMode) : 'React Router'; - return [ - `Analyzed your React Router project structure (${modeName})`, - `Created and configured WorkOS AuthKit`, - `Integrated authentication into your application`, - ]; - }, - getOutroNextSteps: () => [ - 'Start your development server to test authentication', - 'Visit the WorkOS Dashboard to manage users and settings', - ], - }, -}; - -/** - * React Router wizard powered by the universal agent runner. - * @returns Summary of what was done, or empty string if version check fails - */ -export async function runReactRouterInstallerAgent(options: InstallerOptions): Promise { - if (options.debug) { - enableDebugLogs(); - } - - // Check React Router version - agent wizard requires >= 6.0.0 - const packageJson = await getPackageDotJson(options); - const reactRouterVersion = getPackageVersion('react-router', packageJson); - - if (reactRouterVersion) { - const coercedVersion = semver.coerce(reactRouterVersion); - if (coercedVersion && semver.lt(coercedVersion, MINIMUM_REACT_ROUTER_VERSION)) { - const docsUrl = - REACT_ROUTER_AGENT_CONFIG.metadata.unsupportedVersionDocsUrl ?? REACT_ROUTER_AGENT_CONFIG.metadata.docsUrl; - - clack.log.warn( - `Sorry: the installer can't help you with React Router ${reactRouterVersion}. Upgrade to React Router ${MINIMUM_REACT_ROUTER_VERSION} or later, or check out the manual setup guide.`, - ); - clack.log.info(`Setup React Router manually: ${chalk.cyan(docsUrl)}`); - clack.outro('WorkOS AuthKit installer will see you next time!'); - return ''; - } - } - - return runAgentInstaller(REACT_ROUTER_AGENT_CONFIG, options); -} +export { + run as runReactRouterInstallerAgent, + config as REACT_ROUTER_AGENT_CONFIG, +} from '../integrations/react-router/index.js'; diff --git a/src/react-router/utils.ts b/src/react-router/utils.ts index 7847fdc..b93ae18 100644 --- a/src/react-router/utils.ts +++ b/src/react-router/utils.ts @@ -1,242 +1,9 @@ -import { major } from 'semver'; -import fg from 'fast-glob'; -import { abortIfCancelled, getPackageDotJson } from '../utils/clack-utils.js'; -import clack from '../utils/clack.js'; -import { getVersionBucket } from '../utils/semver.js'; -import type { InstallerOptions } from '../utils/types.js'; -import { IGNORE_PATTERNS, Integration } from '../lib/constants.js'; -import { getPackageVersion } from '../utils/package-json.js'; -import chalk from 'chalk'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import * as semver from 'semver'; - -export enum ReactRouterMode { - V6 = 'v6', // React Router v6 - V7_FRAMEWORK = 'v7-framework', // React Router v7 with react-router.config.ts - V7_DATA = 'v7-data', // React Router v7 with createBrowserRouter - V7_DECLARATIVE = 'v7-declarative', // React Router v7 with BrowserRouter -} - /** - * Get React Router version bucket for analytics + * @deprecated Import from 'src/integrations/react-router/utils.js' instead. */ -export function getReactRouterVersionBucket(version: string | undefined): string { - return getVersionBucket(version, 6); -} - -/** - * Check if react-router.config.ts exists (indicates framework mode - React Router v7) - */ -async function hasReactRouterConfig({ installDir }: Pick): Promise { - const configMatches = await fg('**/react-router.config.@(ts|js|tsx|jsx)', { - dot: true, - cwd: installDir, - ignore: IGNORE_PATTERNS, - }); - - return configMatches.length > 0; -} - -/** - * Search for createBrowserRouter usage in source files - */ -async function hasCreateBrowserRouter({ installDir }: Pick): Promise { - const sourceFiles = await fg('**/*.@(ts|tsx|js|jsx)', { - dot: true, - cwd: installDir, - ignore: IGNORE_PATTERNS, - }); - - for (const file of sourceFiles) { - try { - const filePath = path.join(installDir, file); - const content = fs.readFileSync(filePath, 'utf-8'); - - // Check for createBrowserRouter import or usage - if (content.includes('createBrowserRouter')) { - return true; - } - } catch { - // Skip files that can't be read - continue; - } - } - - return false; -} - -/** - * Search for declarative BrowserRouter usage - */ -async function hasDeclarativeRouter({ installDir }: Pick): Promise { - const sourceFiles = await fg('**/*.@(ts|tsx|js|jsx)', { - dot: true, - cwd: installDir, - ignore: IGNORE_PATTERNS, - }); - - for (const file of sourceFiles) { - try { - const filePath = path.join(installDir, file); - const content = fs.readFileSync(filePath, 'utf-8'); - - // Check for BrowserRouter usage (JSX or import) - if ( - content.includes(' { - const { installDir } = options; - - // First, get the React Router version - const packageJson = await getPackageDotJson(options); - const reactRouterVersion = - getPackageVersion('react-router-dom', packageJson) || getPackageVersion('react-router', packageJson); - - if (!reactRouterVersion) { - // If we can't detect version, ask the user - clack.log.info(`Learn more about React Router modes: ${chalk.cyan('https://reactrouter.com/start/modes')}`); - const result: ReactRouterMode = await abortIfCancelled( - clack.select({ - message: 'What React Router version and mode are you using?', - options: [ - { - label: 'React Router v6', - value: ReactRouterMode.V6, - }, - { - label: 'React Router v7 - Framework mode', - value: ReactRouterMode.V7_FRAMEWORK, - }, - { - label: 'React Router v7 - Data mode', - value: ReactRouterMode.V7_DATA, - }, - { - label: 'React Router v7 - Declarative mode', - value: ReactRouterMode.V7_DECLARATIVE, - }, - ], - }), - Integration.reactRouter, - ); - return result; - } - - const coercedVersion = semver.coerce(reactRouterVersion); - const majorVersion = coercedVersion ? major(coercedVersion) : null; - - // If v6, return V6 - if (majorVersion === 6) { - clack.log.info('Detected React Router v6'); - return ReactRouterMode.V6; - } - - // If v7, detect the mode - if (majorVersion === 7) { - // First check for framework mode (react-router.config.ts) - const hasConfig = await hasReactRouterConfig({ installDir }); - if (hasConfig) { - clack.log.info('Detected React Router v7 - Framework mode'); - return ReactRouterMode.V7_FRAMEWORK; - } - - // Check for data mode (createBrowserRouter) - const hasDataMode = await hasCreateBrowserRouter({ installDir }); - if (hasDataMode) { - clack.log.info('Detected React Router v7 - Data mode'); - return ReactRouterMode.V7_DATA; - } - - // Check for declarative mode (BrowserRouter) - const hasDeclarative = await hasDeclarativeRouter({ installDir }); - if (hasDeclarative) { - clack.log.info('Detected React Router v7 - Declarative mode'); - return ReactRouterMode.V7_DECLARATIVE; - } - - // If v7 but can't detect mode, ask the user - clack.log.info(`Learn more about React Router modes: ${chalk.cyan('https://reactrouter.com/start/modes')}`); - const result: ReactRouterMode = await abortIfCancelled( - clack.select({ - message: 'What React Router v7 mode are you using?', - options: [ - { - label: 'Framework mode', - value: ReactRouterMode.V7_FRAMEWORK, - }, - { - label: 'Data mode', - value: ReactRouterMode.V7_DATA, - }, - { - label: 'Declarative mode', - value: ReactRouterMode.V7_DECLARATIVE, - }, - ], - }), - Integration.reactRouter, - ); - return result; - } - - // If version is not 6 or 7, default to asking - clack.log.info(`Learn more about React Router modes: ${chalk.cyan('https://reactrouter.com/start/modes')}`); - const result: ReactRouterMode = await abortIfCancelled( - clack.select({ - message: 'What React Router version and mode are you using?', - options: [ - { - label: 'React Router v6', - value: ReactRouterMode.V6, - }, - { - label: 'React Router v7 - Framework mode', - value: ReactRouterMode.V7_FRAMEWORK, - }, - { - label: 'React Router v7 - Data mode', - value: ReactRouterMode.V7_DATA, - }, - { - label: 'React Router v7 - Declarative mode', - value: ReactRouterMode.V7_DECLARATIVE, - }, - ], - }), - Integration.reactRouter, - ); - return result; -} - -/** - * Get human-readable name for React Router mode - */ -export function getReactRouterModeName(mode: ReactRouterMode): string { - switch (mode) { - case ReactRouterMode.V6: - return 'v6'; - case ReactRouterMode.V7_FRAMEWORK: - return 'v7 Framework mode'; - case ReactRouterMode.V7_DATA: - return 'v7 Data mode'; - case ReactRouterMode.V7_DECLARATIVE: - return 'v7 Declarative mode'; - } -} +export { + ReactRouterMode, + getReactRouterVersionBucket, + getReactRouterMode, + getReactRouterModeName, +} from '../integrations/react-router/utils.js'; diff --git a/src/react/react-installer-agent.ts b/src/react/react-installer-agent.ts index 5086282..5299f29 100644 --- a/src/react/react-installer-agent.ts +++ b/src/react/react-installer-agent.ts @@ -1,57 +1,4 @@ -/* React SPA wizard using Claude Agent SDK */ -import type { InstallerOptions } from '../utils/types.js'; -import type { FrameworkConfig } from '../lib/framework-config.js'; -import { enableDebugLogs } from '../utils/debug.js'; -import { runAgentInstaller } from '../lib/agent-runner.js'; -import { Integration } from '../lib/constants.js'; - -const REACT_AGENT_CONFIG: FrameworkConfig = { - metadata: { - name: 'React', - integration: Integration.react, - docsUrl: 'https://workos.com/docs/user-management/authkit/react', - unsupportedVersionDocsUrl: 'https://workos.com/docs/user-management/authkit/react', - skillName: 'workos-authkit-react', - }, - - detection: { - packageName: 'react', - packageDisplayName: 'React', - getVersion: (packageJson: any) => packageJson.dependencies?.react || packageJson.devDependencies?.react, - }, - - environment: { - uploadToHosting: false, - requiresApiKey: false, // Client-only SPA - getEnvVars: (_apiKey: string, clientId: string) => ({ - WORKOS_CLIENT_ID: clientId, // Only client ID needed - }), - }, - - analytics: { - getTags: () => ({}), - }, - - prompts: {}, - - ui: { - successMessage: 'WorkOS AuthKit integration complete', - getOutroChanges: () => [ - 'Analyzed your React project structure', - 'Created and configured WorkOS AuthKit', - 'Integrated authentication into your application', - ], - getOutroNextSteps: () => [ - 'Start your development server to test authentication', - 'Visit the WorkOS Dashboard to manage users and settings', - ], - }, -}; - -export async function runReactInstallerAgent(options: InstallerOptions): Promise { - if (options.debug) { - enableDebugLogs(); - } - - return runAgentInstaller(REACT_AGENT_CONFIG, options); -} +/** + * @deprecated Import from 'src/integrations/react/index.js' instead. + */ +export { run as runReactInstallerAgent, config as REACT_AGENT_CONFIG } from '../integrations/react/index.js'; diff --git a/src/tanstack-start/tanstack-start-installer-agent.ts b/src/tanstack-start/tanstack-start-installer-agent.ts index db2e95e..9785db6 100644 --- a/src/tanstack-start/tanstack-start-installer-agent.ts +++ b/src/tanstack-start/tanstack-start-installer-agent.ts @@ -1,59 +1,7 @@ -/* TanStack Start wizard using Claude Agent SDK */ -import type { InstallerOptions } from '../utils/types.js'; -import type { FrameworkConfig } from '../lib/framework-config.js'; -import { enableDebugLogs } from '../utils/debug.js'; -import { runAgentInstaller } from '../lib/agent-runner.js'; -import { Integration } from '../lib/constants.js'; -import { getPackageVersion } from '../utils/package-json.js'; - -const TANSTACK_START_AGENT_CONFIG: FrameworkConfig = { - metadata: { - name: 'TanStack Start', - integration: Integration.tanstackStart, - docsUrl: 'https://workos.com/docs/user-management/authkit/tanstack-start', - unsupportedVersionDocsUrl: 'https://workos.com/docs/user-management/authkit/tanstack-start', - skillName: 'workos-authkit-tanstack-start', - }, - - detection: { - packageName: '@tanstack/react-start', - packageDisplayName: 'TanStack Start', - getVersion: (packageJson: any) => getPackageVersion('@tanstack/react-start', packageJson), - }, - - environment: { - uploadToHosting: false, - requiresApiKey: true, // Server-side framework - getEnvVars: (apiKey: string, clientId: string) => ({ - WORKOS_API_KEY: apiKey, - WORKOS_CLIENT_ID: clientId, - }), - }, - - analytics: { - getTags: () => ({}), - }, - - prompts: {}, - - ui: { - successMessage: 'WorkOS AuthKit integration complete', - getOutroChanges: () => [ - 'Analyzed your TanStack Start project structure', - 'Created and configured WorkOS AuthKit', - 'Integrated authentication into your application', - ], - getOutroNextSteps: () => [ - 'Start your development server to test authentication', - 'Visit the WorkOS Dashboard to manage users and settings', - ], - }, -}; - -export async function runTanstackStartInstallerAgent(options: InstallerOptions): Promise { - if (options.debug) { - enableDebugLogs(); - } - - return runAgentInstaller(TANSTACK_START_AGENT_CONFIG, options); -} +/** + * @deprecated Import from 'src/integrations/tanstack-start/index.js' instead. + */ +export { + run as runTanstackStartInstallerAgent, + config as TANSTACK_START_AGENT_CONFIG, +} from '../integrations/tanstack-start/index.js'; diff --git a/src/vanilla-js/vanilla-js-installer-agent.ts b/src/vanilla-js/vanilla-js-installer-agent.ts index e300edd..457dce1 100644 --- a/src/vanilla-js/vanilla-js-installer-agent.ts +++ b/src/vanilla-js/vanilla-js-installer-agent.ts @@ -1,57 +1,7 @@ -/* Vanilla JS wizard using Claude Agent SDK */ -import type { InstallerOptions } from '../utils/types.js'; -import type { FrameworkConfig } from '../lib/framework-config.js'; -import { enableDebugLogs } from '../utils/debug.js'; -import { runAgentInstaller } from '../lib/agent-runner.js'; -import { Integration } from '../lib/constants.js'; - -const VANILLA_JS_AGENT_CONFIG: FrameworkConfig = { - metadata: { - name: 'Vanilla JavaScript', - integration: Integration.vanillaJs, - docsUrl: 'https://workos.com/docs/user-management/authkit/javascript', - unsupportedVersionDocsUrl: 'https://workos.com/docs/user-management/authkit/javascript', - skillName: 'workos-authkit-vanilla-js', - }, - - detection: { - packageName: 'workos', - packageDisplayName: 'Vanilla JavaScript', - getVersion: () => undefined, - }, - - environment: { - uploadToHosting: false, - requiresApiKey: false, // Client-only - getEnvVars: (apiKey: string, clientId: string) => ({ - WORKOS_CLIENT_ID: clientId, // Only client ID needed - }), - }, - - analytics: { - getTags: () => ({}), - }, - - prompts: {}, - - ui: { - successMessage: 'WorkOS AuthKit integration complete', - getOutroChanges: () => [ - 'Created WorkOS AuthKit integration', - 'Added authentication to your JavaScript application', - 'Set up login/logout functionality', - ], - getOutroNextSteps: () => [ - 'Start your development server to test authentication', - 'Visit the WorkOS Dashboard to manage users and settings', - ], - }, -}; - -export async function runVanillaJsInstallerAgent(options: InstallerOptions): Promise { - if (options.debug) { - enableDebugLogs(); - } - - return runAgentInstaller(VANILLA_JS_AGENT_CONFIG, options); -} +/** + * @deprecated Import from 'src/integrations/vanilla-js/index.js' instead. + */ +export { + run as runVanillaJsInstallerAgent, + config as VANILLA_JS_AGENT_CONFIG, +} from '../integrations/vanilla-js/index.js'; diff --git a/tests/evals/agent-executor.ts b/tests/evals/agent-executor.ts index ad072b6..3c4b0cd 100644 --- a/tests/evals/agent-executor.ts +++ b/tests/evals/agent-executor.ts @@ -1,8 +1,10 @@ import path from 'node:path'; +import { writeFileSync, existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { Integration } from '../../src/lib/constants.js'; import { loadCredentials } from './env-loader.js'; import { writeEnvLocal } from '../../src/lib/env-writer.js'; +import { parseEnvFile } from '../../src/utils/env-parser.js'; import { getConfig } from '../../src/lib/settings.js'; import { LatencyTracker } from './latency-tracker.js'; import type { ToolCall, LatencyMetrics } from './types.js'; @@ -21,14 +23,45 @@ export interface AgentExecutorOptions { } // Skill name mapping for each framework -const SKILL_NAMES: Record = { - [Integration.nextjs]: 'workos-authkit-nextjs', - [Integration.react]: 'workos-authkit-react', - [Integration.reactRouter]: 'workos-authkit-react-router', - [Integration.tanstackStart]: 'workos-authkit-tanstack-start', - [Integration.vanillaJs]: 'workos-authkit-vanilla-js', +const SKILL_NAMES: Record = { + nextjs: 'workos-authkit-nextjs', + react: 'workos-authkit-react', + 'react-router': 'workos-authkit-react-router', + 'tanstack-start': 'workos-authkit-tanstack-start', + 'vanilla-js': 'workos-authkit-vanilla-js', + // New SDKs + sveltekit: 'workos-authkit-sveltekit', + node: 'workos-node', + python: 'workos-python', + ruby: 'workos-ruby', + go: 'workos-go', + php: 'workos-php', + 'php-laravel': 'workos-php-laravel', + kotlin: 'workos-kotlin', + dotnet: 'workos-dotnet', + elixir: 'workos-elixir', }; +/** Frameworks that use package.json / .env.local */ +const JS_FRAMEWORKS = ['nextjs', 'react', 'react-router', 'tanstack-start', 'vanilla-js', 'sveltekit', 'node']; + +/** + * Write a .env file (for non-JS frameworks). + * Merges with existing .env if present. + */ +function writeEnvFile(workDir: string, envVars: Record): void { + const envPath = join(workDir, '.env'); + let existing: Record = {}; + if (existsSync(envPath)) { + existing = parseEnvFile(readFileSync(envPath, 'utf-8')); + } + const merged = { ...existing, ...envVars }; + const content = Object.entries(merged) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + writeFileSync(envPath, content + '\n'); +} + export class AgentExecutor { private options: AgentExecutorOptions; private credentials: ReturnType; @@ -57,11 +90,17 @@ export class AgentExecutor { // Start latency tracking this.latencyTracker.start(); - // Write .env.local with credentials (agent configures redirect URI per framework) - writeEnvLocal(this.workDir, { + // Write credentials to appropriate env file based on framework + const envVars = { WORKOS_API_KEY: this.credentials.workosApiKey, WORKOS_CLIENT_ID: this.credentials.workosClientId, - }); + }; + + if (JS_FRAMEWORKS.includes(this.framework)) { + writeEnvLocal(this.workDir, envVars); + } else { + writeEnvFile(this.workDir, envVars); + } // Build prompt const skillName = SKILL_NAMES[integration]; @@ -191,14 +230,8 @@ Begin by invoking the ${skillName} skill.`; } } - private getIntegration(): Integration { - const map: Record = { - nextjs: Integration.nextjs, - react: Integration.react, - 'react-router': Integration.reactRouter, - 'tanstack-start': Integration.tanstackStart, - 'vanilla-js': Integration.vanillaJs, - }; - return map[this.framework]; + private getIntegration(): string { + // Integration is now a string type — framework name IS the integration name + return this.framework; } } diff --git a/tests/evals/cli.ts b/tests/evals/cli.ts index bfda37d..f6c4509 100644 --- a/tests/evals/cli.ts +++ b/tests/evals/cli.ts @@ -20,7 +20,24 @@ export interface CliOptions { pruneKeep?: number; } -const FRAMEWORKS = ['nextjs', 'react', 'react-router', 'tanstack-start', 'vanilla-js']; +const FRAMEWORKS = [ + 'nextjs', + 'react', + 'react-router', + 'tanstack-start', + 'vanilla-js', + // New SDKs + 'sveltekit', + 'node', + 'python', + 'ruby', + 'go', + 'php', + 'php-laravel', + 'kotlin', + 'dotnet', + 'elixir', +]; const STATES = [ 'example', 'example-auth0', diff --git a/tests/evals/fixture-manager.ts b/tests/evals/fixture-manager.ts index eae3fdc..87add3f 100644 --- a/tests/evals/fixture-manager.ts +++ b/tests/evals/fixture-manager.ts @@ -1,4 +1,5 @@ import { cp, rm, mkdtemp } from 'node:fs/promises'; +import { existsSync, readdirSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { execFileNoThrow } from '../../src/utils/exec-file.js'; @@ -29,15 +30,9 @@ export class FixtureManager { await cp(fixtureSource, this.tempDir, { recursive: true }); - // Install dependencies using safe exec + // Install dependencies — detect language and use appropriate package manager console.log(' Installing dependencies...'); - const result = await execFileNoThrow('pnpm', ['install'], { - cwd: this.tempDir, - }); - - if (result.status !== 0) { - throw new Error(`pnpm install failed: ${result.stderr}`); - } + await this.installDependencies(this.tempDir); // Initialize git repo for diff capture (quality grading) await execFileNoThrow('git', ['init'], { cwd: this.tempDir }); @@ -49,7 +44,11 @@ export class FixtureManager { async cleanup(): Promise { if (this.tempDir) { - await rm(this.tempDir, { recursive: true, force: true }); + try { + await rm(this.tempDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 500 }); + } catch { + // Best-effort cleanup — don't let temp dir issues fail scenarios + } this.tempDir = null; } } @@ -57,4 +56,71 @@ export class FixtureManager { getTempDir(): string | null { return this.tempDir; } + + /** + * Detect project language and install dependencies using the appropriate package manager. + */ + private async installDependencies(workDir: string): Promise { + // JS projects + if (existsSync(join(workDir, 'package.json'))) { + const result = await execFileNoThrow('pnpm', ['install'], { cwd: workDir }); + if (result.status !== 0) throw new Error(`pnpm install failed: ${result.stderr}`); + return; + } + + // Python + if (existsSync(join(workDir, 'requirements.txt'))) { + const result = await execFileNoThrow('pip', ['install', '-r', 'requirements.txt'], { cwd: workDir }); + if (result.status !== 0) throw new Error(`pip install failed: ${result.stderr}`); + return; + } + if (existsSync(join(workDir, 'pyproject.toml'))) { + const result = await execFileNoThrow('pip', ['install', '-e', '.'], { cwd: workDir }); + if (result.status !== 0) throw new Error(`pip install failed: ${result.stderr}`); + return; + } + + // Ruby + if (existsSync(join(workDir, 'Gemfile'))) { + const result = await execFileNoThrow('bundle', ['install'], { cwd: workDir }); + if (result.status !== 0) throw new Error(`bundle install failed: ${result.stderr}`); + return; + } + + // Go + if (existsSync(join(workDir, 'go.mod'))) { + const result = await execFileNoThrow('go', ['mod', 'download'], { cwd: workDir }); + if (result.status !== 0) throw new Error(`go mod download failed: ${result.stderr}`); + return; + } + + // PHP + if (existsSync(join(workDir, 'composer.json'))) { + const result = await execFileNoThrow('composer', ['install', '--no-interaction'], { cwd: workDir }); + if (result.status !== 0) throw new Error(`composer install failed: ${result.stderr}`); + return; + } + + // Elixir + if (existsSync(join(workDir, 'mix.exs'))) { + const result = await execFileNoThrow('mix', ['deps.get'], { cwd: workDir }); + if (result.status !== 0) throw new Error(`mix deps.get failed: ${result.stderr}`); + return; + } + + // .NET + const csprojFiles = readdirSync(workDir).filter((f) => f.endsWith('.csproj')); + if (csprojFiles.length > 0) { + const result = await execFileNoThrow('dotnet', ['restore'], { cwd: workDir }); + if (result.status !== 0) throw new Error(`dotnet restore failed: ${result.stderr}`); + return; + } + + // Kotlin/Gradle — deps resolved at build time, skip + if (existsSync(join(workDir, 'build.gradle.kts')) || existsSync(join(workDir, 'build.gradle'))) { + return; + } + + console.warn(' No recognized dependency manifest found, skipping install'); + } } diff --git a/tests/evals/graders/build-grader.ts b/tests/evals/graders/build-grader.ts index 9834ac1..eb5b205 100644 --- a/tests/evals/graders/build-grader.ts +++ b/tests/evals/graders/build-grader.ts @@ -24,6 +24,27 @@ export class BuildGrader { }; } + /** + * Run an arbitrary command as a grading check. + * Used for non-JS build/validation commands (e.g., go build, python -m py_compile, ruby -c). + */ + async checkCommand(cmd: string, args: string[], name: string, timeout = 120000): Promise { + const result = await execFileNoThrow(cmd, args, { + cwd: this.workDir, + timeout, + }); + + if (result.status === 0) { + return { name, passed: true }; + } + + return { + name, + passed: false, + message: `${cmd} ${args.join(' ')} failed: ${result.stderr.slice(0, 500)}`, + }; + } + async checkTypecheck(): Promise { const result = await execFileNoThrow('pnpm', ['tsc', '--noEmit'], { cwd: this.workDir, diff --git a/tests/evals/graders/dotnet.grader.ts b/tests/evals/graders/dotnet.grader.ts new file mode 100644 index 0000000..449a649 --- /dev/null +++ b/tests/evals/graders/dotnet.grader.ts @@ -0,0 +1,49 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +/** + * .NET Grader + * + * SDK: WorkOS (NuGet) + * + * Key patterns: + * - WorkOS in *.csproj + * - Program.cs contains auth routes + * - dotnet build passes + */ +export class DotnetGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const checks: GradeCheck[] = []; + + // Check WorkOS in *.csproj + checks.push( + await this.fileGrader.checkFileWithPattern('**/*.csproj', ['WorkOS'], 'WorkOS package reference in .csproj'), + ); + + // Check Program.cs contains auth routes + checks.push( + await this.fileGrader.checkFileWithPattern( + '**/Program.cs', + [/auth|authorization|callback/i], + 'Program.cs contains auth routes', + ), + ); + + // Check dotnet build passes + checks.push(await this.buildGrader.checkCommand('dotnet', ['build'], 'dotnet build')); + + return { + passed: checks.every((c) => c.passed), + checks, + }; + } +} diff --git a/tests/evals/graders/elixir.grader.ts b/tests/evals/graders/elixir.grader.ts new file mode 100644 index 0000000..63d589d --- /dev/null +++ b/tests/evals/graders/elixir.grader.ts @@ -0,0 +1,58 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +/** + * Elixir Grader + * + * SDK: workos (Hex) + * + * Key patterns: + * - workos in mix.exs + * - Auth controller exists + * - mix compile passes + */ +export class ElixirGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const requiredChecks: GradeCheck[] = []; + const bonusChecks: GradeCheck[] = []; + + // Required: workos in mix.exs + requiredChecks.push(...(await this.fileGrader.checkFileContains('mix.exs', ['workos']))); + + // Required: auth controller exists + requiredChecks.push( + await this.fileGrader.checkFileWithPattern( + 'lib/**/*controller*.ex', + [/auth|authorization|callback/i], + 'Auth controller exists', + ), + ); + + // Required: mix compile passes + requiredChecks.push(await this.buildGrader.checkCommand('mix', ['compile'], 'mix compile')); + + // Bonus: existing app routes preserved (proves agent read existing code) + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + 'lib/**/*.ex', + [/api\/health/], + 'Existing app routes preserved', + ), + ); + + const allChecks = [...requiredChecks, ...bonusChecks]; + return { + passed: requiredChecks.every((c) => c.passed), + checks: allChecks, + }; + } +} diff --git a/tests/evals/graders/go.grader.ts b/tests/evals/graders/go.grader.ts new file mode 100644 index 0000000..1aa16ff --- /dev/null +++ b/tests/evals/graders/go.grader.ts @@ -0,0 +1,52 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +/** + * Go Grader + * + * SDK: workos (Go module) + * + * Key patterns: + * - workos in go.mod + * - go build ./... passes + * - go vet ./... passes + */ +export class GoGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const requiredChecks: GradeCheck[] = []; + const bonusChecks: GradeCheck[] = []; + + // Required: workos in go.mod + requiredChecks.push(...(await this.fileGrader.checkFileContains('go.mod', ['workos']))); + + // Required: go build ./... passes + requiredChecks.push(await this.buildGrader.checkCommand('go', ['build', './...'], 'go build ./...')); + + // Required: go vet ./... passes + requiredChecks.push(await this.buildGrader.checkCommand('go', ['vet', './...'], 'go vet ./...')); + + // Bonus: existing app routes preserved (proves agent read existing code) + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.go', + [/api\/health/], + 'Existing app routes preserved', + ), + ); + + const allChecks = [...requiredChecks, ...bonusChecks]; + return { + passed: requiredChecks.every((c) => c.passed), + checks: allChecks, + }; + } +} diff --git a/tests/evals/graders/kotlin.grader.ts b/tests/evals/graders/kotlin.grader.ts new file mode 100644 index 0000000..c57df56 --- /dev/null +++ b/tests/evals/graders/kotlin.grader.ts @@ -0,0 +1,48 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +/** + * Kotlin Grader + * + * SDK: workos (Gradle) + * + * Key patterns: + * - workos in build.gradle.kts + * - ./gradlew build passes (180s timeout for JVM cold start) + */ +export class KotlinGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const requiredChecks: GradeCheck[] = []; + const bonusChecks: GradeCheck[] = []; + + // Required: workos in build.gradle.kts + requiredChecks.push(...(await this.fileGrader.checkFileContains('build.gradle.kts', ['workos']))); + + // Required: ./gradlew build passes (180s timeout for JVM cold start) + requiredChecks.push(await this.buildGrader.checkCommand('./gradlew', ['build'], './gradlew build', 180000)); + + // Bonus: existing app routes preserved (proves agent read existing code) + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.kt', + [/api\/health/], + 'Existing app routes preserved', + ), + ); + + const allChecks = [...requiredChecks, ...bonusChecks]; + return { + passed: requiredChecks.every((c) => c.passed), + checks: allChecks, + }; + } +} diff --git a/tests/evals/graders/node.grader.ts b/tests/evals/graders/node.grader.ts new file mode 100644 index 0000000..3347042 --- /dev/null +++ b/tests/evals/graders/node.grader.ts @@ -0,0 +1,81 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +/** + * Node.js Grader + * + * SDK: @workos-inc/node + * + * Required checks (must pass): + * - SDK installed in package.json + * - Auth endpoints exist (login redirect + callback code exchange) + * - Syntax valid + * + * Bonus checks (don't block pass): + * - Sealed session handling (step 3 of quickstart — agent may not get this far) + */ +export class NodeGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const requiredChecks: GradeCheck[] = []; + const bonusChecks: GradeCheck[] = []; + + // Required: SDK in package.json + requiredChecks.push(...(await this.fileGrader.checkFileContains('package.json', ['@workos-inc/node']))); + + // Required: sign-in endpoint (getAuthorizationUrl or authorizationUrl) + requiredChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.{js,ts}', + [/getAuthorizationUrl|authorization_url|authorizationUrl/i], + 'Sign-in endpoint with authorization URL', + ), + ); + + // Required: callback endpoint (authenticateWithCode) + requiredChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.{js,ts}', + [/authenticateWithCode/], + 'Callback endpoint with code exchange', + ), + ); + + // Required: syntax check + requiredChecks.push( + await this.buildGrader.checkCommand('node', ['--check', 'server.js'], 'node --check server.js'), + ); + + // Bonus: existing app routes preserved (proves agent read existing code) + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.{js,ts}', + [/api\/health/], + 'Existing app routes preserved', + ), + ); + + // Bonus: sealed session handling (step 3 of quickstart) + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.{js,ts}', + [/loadSealedSession|sealSession|sealed_session/], + 'Sealed session handling (bonus)', + ), + ); + + const allChecks = [...requiredChecks, ...bonusChecks]; + return { + passed: requiredChecks.every((c) => c.passed), + checks: allChecks, + }; + } +} diff --git a/tests/evals/graders/php-laravel.grader.ts b/tests/evals/graders/php-laravel.grader.ts new file mode 100644 index 0000000..7047c7f --- /dev/null +++ b/tests/evals/graders/php-laravel.grader.ts @@ -0,0 +1,61 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +/** + * PHP Laravel Grader + * + * SDK: workos (Composer, Laravel integration) + * + * Required checks (must pass): + * - SDK in composer.json + * - Auth controller or route with workos integration + * - Routes contain auth paths + * + * The agent may put auth code in controllers, routes/web.php directly, + * or a service provider — check broadly. + */ +export class PhpLaravelGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const requiredChecks: GradeCheck[] = []; + const bonusChecks: GradeCheck[] = []; + + // Required: workos in composer.json + requiredChecks.push(...(await this.fileGrader.checkFileContains('composer.json', ['workos']))); + + // Required: auth integration exists somewhere in PHP files + requiredChecks.push(await this.fileGrader.checkFileWithPattern('**/*.php', [/workos/i], 'WorkOS integration in PHP files')); + + // Required: routes contain auth paths + requiredChecks.push( + await this.fileGrader.checkFileWithPattern( + 'routes/**/*.php', + [/auth|login|callback/], + 'Routes contain auth paths', + ), + ); + + // Bonus: existing app routes preserved (proves agent read existing code) + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + '{routes/**/*.php,**/*.php}', + [/api\/health/], + 'Existing app routes preserved', + ), + ); + + const allChecks = [...requiredChecks, ...bonusChecks]; + return { + passed: requiredChecks.every((c) => c.passed), + checks: allChecks, + }; + } +} diff --git a/tests/evals/graders/php.grader.ts b/tests/evals/graders/php.grader.ts new file mode 100644 index 0000000..e5ec518 --- /dev/null +++ b/tests/evals/graders/php.grader.ts @@ -0,0 +1,54 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +/** + * PHP Grader + * + * SDK: workos (Composer) + * + * Key patterns: + * - workos in composer.json + * - Auth endpoint files exist + */ +export class PhpGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const requiredChecks: GradeCheck[] = []; + const bonusChecks: GradeCheck[] = []; + + // Required: workos in composer.json + requiredChecks.push(...(await this.fileGrader.checkFileContains('composer.json', ['workos']))); + + // Required: auth endpoint files exist + requiredChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.php', + ['workos'], + 'Auth endpoint files contain WorkOS integration', + ), + ); + + // Bonus: existing app routes preserved (proves agent read existing code) + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.php', + [/api\/health/], + 'Existing app routes preserved', + ), + ); + + const allChecks = [...requiredChecks, ...bonusChecks]; + return { + passed: requiredChecks.every((c) => c.passed), + checks: allChecks, + }; + } +} diff --git a/tests/evals/graders/python.grader.ts b/tests/evals/graders/python.grader.ts new file mode 100644 index 0000000..e6420b5 --- /dev/null +++ b/tests/evals/graders/python.grader.ts @@ -0,0 +1,86 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +/** + * Python Grader + * + * SDK: workos (PyPI) + * + * Required checks (must pass): + * - SDK installed in requirements.txt or pyproject.toml + * - Auth endpoints exist (login redirect + callback code exchange) + * + * Bonus checks (don't block pass): + * - Sealed session handling (step 3 of quickstart) + * - Syntax validation (requires Python runtime) + */ +export class PythonGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const requiredChecks: GradeCheck[] = []; + const bonusChecks: GradeCheck[] = []; + + // Required: SDK in deps (requirements.txt or pyproject.toml) + const reqTxt = await this.fileGrader.checkFileWithPattern( + '{requirements*.txt,pyproject.toml}', + ['workos'], + 'WorkOS SDK in dependencies', + ); + requiredChecks.push(reqTxt); + + // Required: sign-in endpoint + requiredChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.py', + [/get_authorization_url|authorization_url/], + 'Sign-in endpoint with authorization URL', + ), + ); + + // Required: callback endpoint + requiredChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.py', + [/authenticate_with_code/], + 'Callback endpoint with code exchange', + ), + ); + + // Bonus: existing app routes preserved (proves agent read existing code) + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.py', + [/api\/health/], + 'Existing app routes preserved', + ), + ); + + // Bonus: sealed session handling + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.py', + [/load_sealed_session|seal_session|sealed_session/], + 'Sealed session handling (bonus)', + ), + ); + + // Bonus: syntax check (requires Python) + bonusChecks.push( + await this.buildGrader.checkCommand('python3', ['-m', 'py_compile', 'server.py'], 'Python syntax check (bonus)'), + ); + + const allChecks = [...requiredChecks, ...bonusChecks]; + return { + passed: requiredChecks.every((c) => c.passed), + checks: allChecks, + }; + } +} diff --git a/tests/evals/graders/ruby.grader.ts b/tests/evals/graders/ruby.grader.ts new file mode 100644 index 0000000..865a29d --- /dev/null +++ b/tests/evals/graders/ruby.grader.ts @@ -0,0 +1,79 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +/** + * Ruby Grader + * + * SDK: workos (RubyGems) + * + * Required checks (must pass): + * - SDK installed in Gemfile + * - Auth endpoints exist (login redirect + callback code exchange) + * + * Bonus checks (don't block pass): + * - Sealed session handling (step 3 of quickstart) + * - Syntax validation (requires Ruby runtime) + */ +export class RubyGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const requiredChecks: GradeCheck[] = []; + const bonusChecks: GradeCheck[] = []; + + // Required: SDK in Gemfile + requiredChecks.push(...(await this.fileGrader.checkFileContains('Gemfile', ['workos']))); + + // Required: sign-in endpoint + requiredChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.rb', + [/authorization_url/], + 'Sign-in endpoint with authorization URL', + ), + ); + + // Required: callback endpoint + requiredChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.rb', + [/authenticate_with_code/], + 'Callback endpoint with code exchange', + ), + ); + + // Bonus: existing app routes preserved (proves agent read existing code) + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.rb', + [/api\/health/], + 'Existing app routes preserved', + ), + ); + + // Bonus: sealed session handling + bonusChecks.push( + await this.fileGrader.checkFileWithPattern( + '**/*.rb', + [/load_sealed_session|seal_session|sealed_session/], + 'Sealed session handling (bonus)', + ), + ); + + // Bonus: syntax check (requires Ruby) + bonusChecks.push(await this.buildGrader.checkCommand('ruby', ['-c', 'server.rb'], 'Ruby syntax check (bonus)')); + + const allChecks = [...requiredChecks, ...bonusChecks]; + return { + passed: requiredChecks.every((c) => c.passed), + checks: allChecks, + }; + } +} diff --git a/tests/evals/graders/sveltekit.grader.ts b/tests/evals/graders/sveltekit.grader.ts new file mode 100644 index 0000000..e341ad0 --- /dev/null +++ b/tests/evals/graders/sveltekit.grader.ts @@ -0,0 +1,63 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +/** + * SvelteKit Grader + * + * SDK: @workos/authkit-sveltekit (NOT @workos-inc) + * + * Required checks (must pass): + * - AuthKit SDK in package.json + * - hooks.server.ts exists with workos/authkit integration + * - Callback route exists + * - Build passes + */ +export class SvelteKitGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const checks: GradeCheck[] = []; + + // Check authkit-sveltekit in package.json (could be @workos/ or @workos-inc/) + checks.push( + await this.fileGrader.checkFileWithPattern( + 'package.json', + [/authkit-sveltekit/], + 'AuthKit SvelteKit SDK in package.json', + ), + ); + + // Check hooks.server.ts exists with workos/authkit reference + checks.push( + await this.fileGrader.checkFileWithPattern( + 'src/hooks.server.ts', + [/workos|authkit/i], + 'hooks.server.ts exists with AuthKit integration', + ), + ); + + // Check callback route exists (could be at various paths) + checks.push( + await this.fileGrader.checkFileWithPattern( + 'src/routes/**/+server.ts', + [/workos|authkit|code|callback/i], + 'Callback route exists', + ), + ); + + // Check build succeeds + checks.push(await this.buildGrader.checkBuild()); + + return { + passed: checks.every((c) => c.passed), + checks, + }; + } +} diff --git a/tests/evals/quality-key-files.ts b/tests/evals/quality-key-files.ts index 3bb8b0f..d81439a 100644 --- a/tests/evals/quality-key-files.ts +++ b/tests/evals/quality-key-files.ts @@ -64,4 +64,21 @@ export const QUALITY_KEY_FILES: Record = { // HTML entry 'index.html', ], + + // New SDKs + sveltekit: [ + 'src/hooks.server.ts', + 'src/routes/callback/+server.ts', + 'src/routes/+layout.server.ts', + 'src/routes/+page.svelte', + ], + node: ['server.js', 'server.ts', 'src/index.ts', 'index.html'], + python: ['server.py', 'app.py', 'index.html'], + ruby: ['server.rb', 'app.rb', 'index.html'], + go: ['main.go', 'handlers/**/*.go', 'auth/**/*.go'], + php: ['public/index.php', 'login.php', 'callback.php'], + 'php-laravel': ['app/Http/Controllers/*Auth*.php', 'routes/web.php', 'config/workos.php'], + kotlin: ['src/main/kotlin/**/*Controller*.kt', 'src/main/resources/application.properties'], + dotnet: ['Program.cs', '*.csproj'], + elixir: ['lib/*_web/controllers/*auth*.ex', 'lib/*_web/router.ex', 'config/runtime.exs'], }; diff --git a/tests/evals/quality-rubrics.ts b/tests/evals/quality-rubrics.ts index 728b2a8..e48d387 100644 --- a/tests/evals/quality-rubrics.ts +++ b/tests/evals/quality-rubrics.ts @@ -23,13 +23,13 @@ export const QUALITY_RUBRICS = { }, errorHandling: { name: 'Error Handling', - description: 'Proper error handling and user-friendly error messages', + description: 'Appropriate error handling for the integration context', scale: { - 1: 'Missing: no error handling, crashes on edge cases', - 2: 'Basic: catches some errors but poor messages or recovery', - 3: 'Acceptable: handles main errors, generic messages', - 4: 'Good: comprehensive error handling, helpful messages', - 5: 'Excellent: robust handling with actionable user-friendly messages', + 1: 'Dangerous: silently swallows errors, loses user data, or crashes without recovery', + 2: 'Poor: catches errors but ignores them, or shows raw stack traces to users', + 3: 'Acceptable: delegates to SDK/framework defaults, no custom handling needed for basic integration', + 4: 'Good: adds targeted error handling where it matters (callback failures, missing params)', + 5: 'Excellent: comprehensive handling with user-friendly messages and graceful degradation', }, }, idiomatic: { diff --git a/tests/evals/runner.ts b/tests/evals/runner.ts index f81f6d3..414dfbb 100644 --- a/tests/evals/runner.ts +++ b/tests/evals/runner.ts @@ -3,6 +3,17 @@ import { ReactGrader } from './graders/react.grader.js'; import { ReactRouterGrader } from './graders/react-router.grader.js'; import { TanstackGrader } from './graders/tanstack.grader.js'; import { VanillaGrader } from './graders/vanilla.grader.js'; +// New SDK graders +import { SvelteKitGrader } from './graders/sveltekit.grader.js'; +import { NodeGrader } from './graders/node.grader.js'; +import { PythonGrader } from './graders/python.grader.js'; +import { RubyGrader } from './graders/ruby.grader.js'; +import { GoGrader } from './graders/go.grader.js'; +import { PhpGrader } from './graders/php.grader.js'; +import { PhpLaravelGrader } from './graders/php-laravel.grader.js'; +import { KotlinGrader } from './graders/kotlin.grader.js'; +import { DotnetGrader } from './graders/dotnet.grader.js'; +import { ElixirGrader } from './graders/elixir.grader.js'; import { saveResults } from './history.js'; import { ParallelRunner } from './parallel-runner.js'; import { renderDashboard } from './dashboard/index.js'; @@ -53,6 +64,30 @@ const SCENARIOS: Scenario[] = [ { framework: 'vanilla-js', state: 'example-auth0', grader: VanillaGrader }, { framework: 'vanilla-js', state: 'partial-install', grader: VanillaGrader }, { framework: 'vanilla-js', state: 'conflicting-auth', grader: VanillaGrader }, + + // SvelteKit (1 state) + { framework: 'sveltekit', state: 'example', grader: SvelteKitGrader }, + + // Backend SDKs (2 states each — happy path + auth0 migration) + { framework: 'node', state: 'example', grader: NodeGrader }, + { framework: 'node', state: 'example-auth0', grader: NodeGrader }, + { framework: 'python', state: 'example', grader: PythonGrader }, + { framework: 'python', state: 'example-auth0', grader: PythonGrader }, + { framework: 'ruby', state: 'example', grader: RubyGrader }, + { framework: 'ruby', state: 'example-auth0', grader: RubyGrader }, + { framework: 'go', state: 'example', grader: GoGrader }, + { framework: 'go', state: 'example-auth0', grader: GoGrader }, + { framework: 'php', state: 'example', grader: PhpGrader }, + { framework: 'php', state: 'example-auth0', grader: PhpGrader }, + { framework: 'php-laravel', state: 'example', grader: PhpLaravelGrader }, + { framework: 'php-laravel', state: 'example-auth0', grader: PhpLaravelGrader }, + { framework: 'kotlin', state: 'example', grader: KotlinGrader }, + { framework: 'kotlin', state: 'example-auth0', grader: KotlinGrader }, + { framework: 'elixir', state: 'example', grader: ElixirGrader }, + { framework: 'elixir', state: 'example-auth0', grader: ElixirGrader }, + + // .NET (broken — no runtime) + { framework: 'dotnet', state: 'example', grader: DotnetGrader }, ]; export interface ExtendedEvalOptions extends EvalOptions { diff --git a/tests/evals/versioning.spec.ts b/tests/evals/versioning.spec.ts index 615f50d..d30a71d 100644 --- a/tests/evals/versioning.spec.ts +++ b/tests/evals/versioning.spec.ts @@ -91,9 +91,9 @@ describe('versioning', () => { const metadata = await captureVersionMetadata(); - // Should have called git hash-object for each framework - expect(execFileNoThrow).toHaveBeenCalledTimes(5); - expect(Object.keys(metadata.skillVersions)).toHaveLength(5); + // Should have called git hash-object for each framework (15 total: 5 original + 10 new) + expect(execFileNoThrow).toHaveBeenCalledTimes(15); + expect(Object.keys(metadata.skillVersions)).toHaveLength(15); }); it('handles mixed success/failure gracefully', async () => { diff --git a/tests/evals/versioning.ts b/tests/evals/versioning.ts index 6418a4f..1f3c16a 100644 --- a/tests/evals/versioning.ts +++ b/tests/evals/versioning.ts @@ -8,11 +8,23 @@ import { join } from 'node:path'; * Used to compute git hashes for version tracking. */ const SKILL_FILES: Record = { - nextjs: 'src/nextjs/nextjs-installer-agent.ts', - react: 'src/react/react-installer-agent.ts', - 'react-router': 'src/react-router/react-router-installer-agent.ts', - 'tanstack-start': 'src/tanstack-start/tanstack-start-installer-agent.ts', - 'vanilla-js': 'src/vanilla-js/vanilla-js-installer-agent.ts', + // Existing JS SDKs (updated paths from Phase 1 migration) + nextjs: 'src/integrations/nextjs/index.ts', + react: 'src/integrations/react/index.ts', + 'react-router': 'src/integrations/react-router/index.ts', + 'tanstack-start': 'src/integrations/tanstack-start/index.ts', + 'vanilla-js': 'src/integrations/vanilla-js/index.ts', + // New SDKs + sveltekit: 'src/integrations/sveltekit/index.ts', + node: 'src/integrations/node/index.ts', + python: 'src/integrations/python/index.ts', + ruby: 'src/integrations/ruby/index.ts', + go: 'src/integrations/go/index.ts', + php: 'src/integrations/php/index.ts', + 'php-laravel': 'src/integrations/php-laravel/index.ts', + kotlin: 'src/integrations/kotlin/index.ts', + dotnet: 'src/integrations/dotnet/index.ts', + elixir: 'src/integrations/elixir/index.ts', }; export interface VersionMetadata { diff --git a/tests/fixtures/dotnet/example/Example.csproj b/tests/fixtures/dotnet/example/Example.csproj new file mode 100644 index 0000000..f577589 --- /dev/null +++ b/tests/fixtures/dotnet/example/Example.csproj @@ -0,0 +1,5 @@ + + + net8.0 + + diff --git a/tests/fixtures/dotnet/example/Program.cs b/tests/fixtures/dotnet/example/Program.cs new file mode 100644 index 0000000..e01f740 --- /dev/null +++ b/tests/fixtures/dotnet/example/Program.cs @@ -0,0 +1,7 @@ +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +app.MapGet("/", () => Results.Content( + System.IO.File.ReadAllText("index.html"), "text/html")); + +app.Run("http://localhost:3000"); diff --git a/tests/fixtures/dotnet/example/appsettings.json b/tests/fixtures/dotnet/example/appsettings.json new file mode 100644 index 0000000..960be50 --- /dev/null +++ b/tests/fixtures/dotnet/example/appsettings.json @@ -0,0 +1,7 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + } +} diff --git a/tests/fixtures/dotnet/example/index.html b/tests/fixtures/dotnet/example/index.html new file mode 100644 index 0000000..51d0261 --- /dev/null +++ b/tests/fixtures/dotnet/example/index.html @@ -0,0 +1,11 @@ + + + + AuthKit example + + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/elixir/example-auth0/config/config.exs b/tests/fixtures/elixir/example-auth0/config/config.exs new file mode 100644 index 0000000..e22d0da --- /dev/null +++ b/tests/fixtures/elixir/example-auth0/config/config.exs @@ -0,0 +1,15 @@ +import Config + +config :example, ExampleWeb.Endpoint, + url: [host: "localhost"], + http: [port: 3000], + secret_key_base: "placeholder_secret_key_base_for_fixture" + +config :ueberauth, Ueberauth, + providers: [ + auth0: {Ueberauth.Strategy.Auth0, [ + domain: System.get_env("AUTH0_DOMAIN"), + client_id: System.get_env("AUTH0_CLIENT_ID"), + client_secret: System.get_env("AUTH0_CLIENT_SECRET") + ]} + ] diff --git a/tests/fixtures/elixir/example-auth0/lib/example/application.ex b/tests/fixtures/elixir/example-auth0/lib/example/application.ex new file mode 100644 index 0000000..8a25635 --- /dev/null +++ b/tests/fixtures/elixir/example-auth0/lib/example/application.ex @@ -0,0 +1,12 @@ +defmodule Example.Application do + use Application + + def start(_type, _args) do + children = [ + ExampleWeb.Endpoint + ] + + opts = [strategy: :one_for_one, name: Example.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/tests/fixtures/elixir/example-auth0/lib/example_web/controllers/auth_controller.ex b/tests/fixtures/elixir/example-auth0/lib/example_web/controllers/auth_controller.ex new file mode 100644 index 0000000..a552616 --- /dev/null +++ b/tests/fixtures/elixir/example-auth0/lib/example_web/controllers/auth_controller.ex @@ -0,0 +1,32 @@ +defmodule ExampleWeb.AuthController do + use ExampleWeb, :controller + plug Ueberauth + + def request(conn, _params) do + # Ueberauth handles the redirect to Auth0 automatically + conn + end + + def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do + user = %{ + name: auth.info.name, + email: auth.info.email + } + + conn + |> put_session(:user, user) + |> redirect(to: "/") + end + + def callback(%{assigns: %{ueberauth_failure: _failure}} = conn, _params) do + conn + |> put_status(401) + |> text("Authentication failed") + end + + def sign_out(conn, _params) do + conn + |> clear_session() + |> redirect(to: "/") + end +end diff --git a/tests/fixtures/elixir/example-auth0/lib/example_web/controllers/page_controller.ex b/tests/fixtures/elixir/example-auth0/lib/example_web/controllers/page_controller.ex new file mode 100644 index 0000000..12d61fb --- /dev/null +++ b/tests/fixtures/elixir/example-auth0/lib/example_web/controllers/page_controller.ex @@ -0,0 +1,21 @@ +defmodule ExampleWeb.PageController do + use Phoenix.Controller, formats: [:html] + + def health(conn, _params) do + json(conn, %{status: "ok", version: "1.0.0"}) + end + + def index(conn, _params) do + html(conn, """ + + + AuthKit example + +

AuthKit example

+

Sign in

+

Sign out

+ + + """) + end +end diff --git a/tests/fixtures/elixir/example-auth0/lib/example_web/endpoint.ex b/tests/fixtures/elixir/example-auth0/lib/example_web/endpoint.ex new file mode 100644 index 0000000..eccfdeb --- /dev/null +++ b/tests/fixtures/elixir/example-auth0/lib/example_web/endpoint.ex @@ -0,0 +1,5 @@ +defmodule ExampleWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :example + + plug ExampleWeb.Router +end diff --git a/tests/fixtures/elixir/example-auth0/lib/example_web/router.ex b/tests/fixtures/elixir/example-auth0/lib/example_web/router.ex new file mode 100644 index 0000000..0685d37 --- /dev/null +++ b/tests/fixtures/elixir/example-auth0/lib/example_web/router.ex @@ -0,0 +1,22 @@ +defmodule ExampleWeb.Router do + use Phoenix.Router + + pipeline :browser do + plug :accepts, ["html"] + end + + scope "/", ExampleWeb do + pipe_through :browser + + get "/", PageController, :index + get "/api/health", PageController, :health + end + + scope "/auth", ExampleWeb do + pipe_through :browser + + get "/:provider", AuthController, :request + get "/:provider/callback", AuthController, :callback + post "/sign-out", AuthController, :sign_out + end +end diff --git a/tests/fixtures/elixir/example-auth0/mix.exs b/tests/fixtures/elixir/example-auth0/mix.exs new file mode 100644 index 0000000..0f9b1fa --- /dev/null +++ b/tests/fixtures/elixir/example-auth0/mix.exs @@ -0,0 +1,30 @@ +defmodule Example.MixProject do + use Mix.Project + + def project do + [ + app: :example, + version: "0.1.0", + elixir: "~> 1.15", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + mod: {Example.Application, []}, + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:phoenix, "~> 1.7"}, + {:phoenix_html, "~> 4.0"}, + {:plug_cowboy, "~> 2.7"}, + {:ueberauth, "~> 0.10"}, + {:ueberauth_auth0, "~> 2.1"} + ] + end +end diff --git a/tests/fixtures/elixir/example/config/config.exs b/tests/fixtures/elixir/example/config/config.exs new file mode 100644 index 0000000..c033afb --- /dev/null +++ b/tests/fixtures/elixir/example/config/config.exs @@ -0,0 +1,6 @@ +import Config + +config :example, ExampleWeb.Endpoint, + url: [host: "localhost"], + http: [port: 3000], + secret_key_base: "placeholder_secret_key_base_for_fixture" diff --git a/tests/fixtures/elixir/example/lib/example/application.ex b/tests/fixtures/elixir/example/lib/example/application.ex new file mode 100644 index 0000000..8a25635 --- /dev/null +++ b/tests/fixtures/elixir/example/lib/example/application.ex @@ -0,0 +1,12 @@ +defmodule Example.Application do + use Application + + def start(_type, _args) do + children = [ + ExampleWeb.Endpoint + ] + + opts = [strategy: :one_for_one, name: Example.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/tests/fixtures/elixir/example/lib/example_web/controllers/page_controller.ex b/tests/fixtures/elixir/example/lib/example_web/controllers/page_controller.ex new file mode 100644 index 0000000..48ea80a --- /dev/null +++ b/tests/fixtures/elixir/example/lib/example_web/controllers/page_controller.ex @@ -0,0 +1,17 @@ +defmodule ExampleWeb.PageController do + use Phoenix.Controller, formats: [:html] + + def index(conn, _params) do + html(conn, """ + + + AuthKit example + +

AuthKit example

+

Sign in

+

Sign out

+ + + """) + end +end diff --git a/tests/fixtures/elixir/example/lib/example_web/endpoint.ex b/tests/fixtures/elixir/example/lib/example_web/endpoint.ex new file mode 100644 index 0000000..eccfdeb --- /dev/null +++ b/tests/fixtures/elixir/example/lib/example_web/endpoint.ex @@ -0,0 +1,5 @@ +defmodule ExampleWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :example + + plug ExampleWeb.Router +end diff --git a/tests/fixtures/elixir/example/lib/example_web/router.ex b/tests/fixtures/elixir/example/lib/example_web/router.ex new file mode 100644 index 0000000..ded6964 --- /dev/null +++ b/tests/fixtures/elixir/example/lib/example_web/router.ex @@ -0,0 +1,13 @@ +defmodule ExampleWeb.Router do + use Phoenix.Router + + pipeline :browser do + plug :accepts, ["html"] + end + + scope "/", ExampleWeb do + pipe_through :browser + + get "/", PageController, :index + end +end diff --git a/tests/fixtures/elixir/example/mix.exs b/tests/fixtures/elixir/example/mix.exs new file mode 100644 index 0000000..f514bba --- /dev/null +++ b/tests/fixtures/elixir/example/mix.exs @@ -0,0 +1,28 @@ +defmodule Example.MixProject do + use Mix.Project + + def project do + [ + app: :example, + version: "0.1.0", + elixir: "~> 1.15", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + mod: {Example.Application, []}, + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:phoenix, "~> 1.7"}, + {:phoenix_html, "~> 4.0"}, + {:plug_cowboy, "~> 2.7"} + ] + end +end diff --git a/tests/fixtures/go/example-auth0/authkit-example b/tests/fixtures/go/example-auth0/authkit-example new file mode 100755 index 0000000..197a95a Binary files /dev/null and b/tests/fixtures/go/example-auth0/authkit-example differ diff --git a/tests/fixtures/go/example-auth0/go.mod b/tests/fixtures/go/example-auth0/go.mod new file mode 100644 index 0000000..804f169 --- /dev/null +++ b/tests/fixtures/go/example-auth0/go.mod @@ -0,0 +1,40 @@ +module example.com/authkit-example + +go 1.21 + +require ( + github.com/coreos/go-oidc/v3 v3.9.0 + github.com/gin-gonic/gin v1.9.1 + github.com/joho/godotenv v1.5.1 + golang.org/x/oauth2 v0.16.0 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/tests/fixtures/go/example-auth0/go.sum b/tests/fixtures/go/example-auth0/go.sum new file mode 100644 index 0000000..5be986a --- /dev/null +++ b/tests/fixtures/go/example-auth0/go.sum @@ -0,0 +1,129 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= +github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/tests/fixtures/go/example-auth0/index.html b/tests/fixtures/go/example-auth0/index.html new file mode 100644 index 0000000..51d0261 --- /dev/null +++ b/tests/fixtures/go/example-auth0/index.html @@ -0,0 +1,11 @@ + + + + AuthKit example + + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/go/example-auth0/main.go b/tests/fixtures/go/example-auth0/main.go new file mode 100644 index 0000000..2eb2aa7 --- /dev/null +++ b/tests/fixtures/go/example-auth0/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "encoding/json" + "net/http" + "os" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" + "golang.org/x/oauth2" +) + +var ( + oauth2Config oauth2.Config + oidcProvider *oidc.Provider +) + +func main() { + godotenv.Load() + + ctx := context.Background() + + provider, err := oidc.NewProvider(ctx, "https://"+os.Getenv("AUTH0_DOMAIN")+"/") + if err != nil { + panic(err) + } + oidcProvider = provider + + oauth2Config = oauth2.Config{ + ClientID: os.Getenv("AUTH0_CLIENT_ID"), + ClientSecret: os.Getenv("AUTH0_CLIENT_SECRET"), + RedirectURL: "http://localhost:3000/callback", + Endpoint: provider.Endpoint(), + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + } + + r := gin.Default() + + r.GET("/", func(c *gin.Context) { + c.File("./index.html") + }) + + r.GET("/api/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok", "version": "1.0.0"}) + }) + + r.GET("/login", func(c *gin.Context) { + c.Redirect(http.StatusTemporaryRedirect, oauth2Config.AuthCodeURL("state")) + }) + + r.GET("/callback", func(c *gin.Context) { + token, err := oauth2Config.Exchange(c.Request.Context(), c.Query("code")) + if err != nil { + c.String(http.StatusInternalServerError, "Token exchange failed: "+err.Error()) + return + } + + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + c.String(http.StatusInternalServerError, "No id_token in response") + return + } + + verifier := oidcProvider.Verifier(&oidc.Config{ClientID: oauth2Config.ClientID}) + idToken, err := verifier.Verify(c.Request.Context(), rawIDToken) + if err != nil { + c.String(http.StatusInternalServerError, "Token verification failed: "+err.Error()) + return + } + + var claims map[string]interface{} + idToken.Claims(&claims) + + userJSON, _ := json.Marshal(claims) + c.SetCookie("user", string(userJSON), 3600, "/", "", false, true) + c.Redirect(http.StatusTemporaryRedirect, "/") + }) + + r.GET("/logout", func(c *gin.Context) { + c.SetCookie("user", "", -1, "/", "", false, true) + c.Redirect(http.StatusTemporaryRedirect, "/") + }) + + r.Run(":3000") +} diff --git a/tests/fixtures/go/example/go.mod b/tests/fixtures/go/example/go.mod new file mode 100644 index 0000000..58a360f --- /dev/null +++ b/tests/fixtures/go/example/go.mod @@ -0,0 +1,5 @@ +module example.com/authkit-example + +go 1.21 + +require github.com/gin-gonic/gin v1.9.1 diff --git a/tests/fixtures/go/example/index.html b/tests/fixtures/go/example/index.html new file mode 100644 index 0000000..51d0261 --- /dev/null +++ b/tests/fixtures/go/example/index.html @@ -0,0 +1,11 @@ + + + + AuthKit example + + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/go/example/main.go b/tests/fixtures/go/example/main.go new file mode 100644 index 0000000..ce831b1 --- /dev/null +++ b/tests/fixtures/go/example/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + + r.GET("/", func(c *gin.Context) { + c.File("./index.html") + }) + + r.Run(":3000") +} diff --git a/tests/fixtures/kotlin/example-auth0/build.gradle.kts b/tests/fixtures/kotlin/example-auth0/build.gradle.kts new file mode 100644 index 0000000..a7395b2 --- /dev/null +++ b/tests/fixtures/kotlin/example-auth0/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("org.springframework.boot") version "3.2.0" + id("io.spring.dependency-management") version "1.1.4" + kotlin("jvm") version "1.9.21" + kotlin("plugin.spring") version "1.9.21" +} + +group = "com.example" +version = "0.0.1" + +java { + sourceCompatibility = JavaVersion.VERSION_17 +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("com.auth0:mvc-auth-commons:1.11.0") +} diff --git a/tests/fixtures/kotlin/example-auth0/gradle/wrapper/gradle-wrapper.jar b/tests/fixtures/kotlin/example-auth0/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..61285a6 Binary files /dev/null and b/tests/fixtures/kotlin/example-auth0/gradle/wrapper/gradle-wrapper.jar differ diff --git a/tests/fixtures/kotlin/example-auth0/gradle/wrapper/gradle-wrapper.properties b/tests/fixtures/kotlin/example-auth0/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9355b41 --- /dev/null +++ b/tests/fixtures/kotlin/example-auth0/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/tests/fixtures/kotlin/example-auth0/gradlew b/tests/fixtures/kotlin/example-auth0/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/tests/fixtures/kotlin/example-auth0/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/tests/fixtures/kotlin/example-auth0/gradlew.bat b/tests/fixtures/kotlin/example-auth0/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/tests/fixtures/kotlin/example-auth0/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/tests/fixtures/kotlin/example-auth0/settings.gradle.kts b/tests/fixtures/kotlin/example-auth0/settings.gradle.kts new file mode 100644 index 0000000..749a944 --- /dev/null +++ b/tests/fixtures/kotlin/example-auth0/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "authkit-example" diff --git a/tests/fixtures/kotlin/example-auth0/src/main/kotlin/com/example/Application.kt b/tests/fixtures/kotlin/example-auth0/src/main/kotlin/com/example/Application.kt new file mode 100644 index 0000000..2b564e8 --- /dev/null +++ b/tests/fixtures/kotlin/example-auth0/src/main/kotlin/com/example/Application.kt @@ -0,0 +1,11 @@ +package com.example + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class Application + +fun main(args: Array) { + runApplication(*args) +} diff --git a/tests/fixtures/kotlin/example-auth0/src/main/kotlin/com/example/AuthController.kt b/tests/fixtures/kotlin/example-auth0/src/main/kotlin/com/example/AuthController.kt new file mode 100644 index 0000000..08860ef --- /dev/null +++ b/tests/fixtures/kotlin/example-auth0/src/main/kotlin/com/example/AuthController.kt @@ -0,0 +1,42 @@ +package com.example + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.servlet.view.RedirectView +import jakarta.servlet.http.HttpSession +import jakarta.servlet.http.HttpServletRequest + +@Controller +class AuthController( + @Value("\${auth0.domain}") private val auth0Domain: String, + @Value("\${auth0.clientId}") private val clientId: String, + @Value("\${auth0.clientSecret}") private val clientSecret: String, +) { + @GetMapping("/api/health") + @org.springframework.web.bind.annotation.ResponseBody + fun health(): Map { + return mapOf("status" to "ok", "version" to "1.0.0") + } + + @GetMapping("/login") + fun login(): RedirectView { + val authorizeUrl = "https://$auth0Domain/authorize?" + + "client_id=$clientId&redirect_uri=http://localhost:8080/callback&response_type=code&scope=openid profile email" + return RedirectView(authorizeUrl) + } + + @GetMapping("/callback") + fun callback(request: HttpServletRequest, session: HttpSession): RedirectView { + val code = request.getParameter("code") + // In a real app, exchange code for tokens via Auth0 API + session.setAttribute("user", mapOf("code" to code)) + return RedirectView("/") + } + + @GetMapping("/logout") + fun logout(session: HttpSession): RedirectView { + session.invalidate() + return RedirectView("/") + } +} diff --git a/tests/fixtures/kotlin/example-auth0/src/main/resources/application.properties b/tests/fixtures/kotlin/example-auth0/src/main/resources/application.properties new file mode 100644 index 0000000..23c5e38 --- /dev/null +++ b/tests/fixtures/kotlin/example-auth0/src/main/resources/application.properties @@ -0,0 +1,4 @@ +server.port=3000 +auth0.domain=${AUTH0_DOMAIN} +auth0.clientId=${AUTH0_CLIENT_ID} +auth0.clientSecret=${AUTH0_CLIENT_SECRET} diff --git a/tests/fixtures/kotlin/example/build.gradle.kts b/tests/fixtures/kotlin/example/build.gradle.kts new file mode 100644 index 0000000..8a2bc6c --- /dev/null +++ b/tests/fixtures/kotlin/example/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("org.springframework.boot") version "3.2.0" + id("io.spring.dependency-management") version "1.1.4" + kotlin("jvm") version "1.9.21" + kotlin("plugin.spring") version "1.9.21" +} + +group = "com.example" +version = "0.0.1" + +java { + sourceCompatibility = JavaVersion.VERSION_17 +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.jetbrains.kotlin:kotlin-reflect") +} diff --git a/tests/fixtures/kotlin/example/gradle/wrapper/gradle-wrapper.jar b/tests/fixtures/kotlin/example/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..61285a6 Binary files /dev/null and b/tests/fixtures/kotlin/example/gradle/wrapper/gradle-wrapper.jar differ diff --git a/tests/fixtures/kotlin/example/gradle/wrapper/gradle-wrapper.properties b/tests/fixtures/kotlin/example/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9355b41 --- /dev/null +++ b/tests/fixtures/kotlin/example/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/tests/fixtures/kotlin/example/gradlew b/tests/fixtures/kotlin/example/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/tests/fixtures/kotlin/example/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/tests/fixtures/kotlin/example/gradlew.bat b/tests/fixtures/kotlin/example/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/tests/fixtures/kotlin/example/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/tests/fixtures/kotlin/example/settings.gradle.kts b/tests/fixtures/kotlin/example/settings.gradle.kts new file mode 100644 index 0000000..749a944 --- /dev/null +++ b/tests/fixtures/kotlin/example/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "authkit-example" diff --git a/tests/fixtures/kotlin/example/src/main/kotlin/com/example/Application.kt b/tests/fixtures/kotlin/example/src/main/kotlin/com/example/Application.kt new file mode 100644 index 0000000..2b564e8 --- /dev/null +++ b/tests/fixtures/kotlin/example/src/main/kotlin/com/example/Application.kt @@ -0,0 +1,11 @@ +package com.example + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class Application + +fun main(args: Array) { + runApplication(*args) +} diff --git a/tests/fixtures/kotlin/example/src/main/resources/application.properties b/tests/fixtures/kotlin/example/src/main/resources/application.properties new file mode 100644 index 0000000..b0f2669 --- /dev/null +++ b/tests/fixtures/kotlin/example/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=3000 diff --git a/tests/fixtures/node/example-auth0/index.html b/tests/fixtures/node/example-auth0/index.html new file mode 100644 index 0000000..51d0261 --- /dev/null +++ b/tests/fixtures/node/example-auth0/index.html @@ -0,0 +1,11 @@ + + + + AuthKit example + + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/node/example-auth0/package.json b/tests/fixtures/node/example-auth0/package.json new file mode 100644 index 0000000..bbdad5e --- /dev/null +++ b/tests/fixtures/node/example-auth0/package.json @@ -0,0 +1,14 @@ +{ + "name": "node-existing-auth0-fixture", + "version": "0.0.1", + "private": true, + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.0", + "express-openid-connect": "^2.17.0", + "cookie-parser": "^1.4.6", + "dotenv": "^16.3.0" + } +} diff --git a/tests/fixtures/node/example-auth0/server.js b/tests/fixtures/node/example-auth0/server.js new file mode 100644 index 0000000..92d8ff1 --- /dev/null +++ b/tests/fixtures/node/example-auth0/server.js @@ -0,0 +1,45 @@ +const path = require('path'); +const express = require('express'); +const cookieParser = require('cookie-parser'); +const { auth, requiresAuth } = require('express-openid-connect'); +require('dotenv').config(); + +const app = express(); + +app.use(cookieParser()); + +app.use( + auth({ + authRequired: false, + auth0Logout: true, + secret: process.env.AUTH0_SECRET, + baseURL: process.env.AUTH0_BASE_URL || 'http://localhost:3000', + clientID: process.env.AUTH0_CLIENT_ID, + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, + }) +); + +app.get('/', (req, res) => { + if (req.oidc.isAuthenticated()) { + const user = req.oidc.user; + res.send(` +

Welcome, ${user.name}!

+

Email: ${user.email}

+

Sign out

+ `); + } else { + res.sendFile(path.join(__dirname, 'index.html')); + } +}); + +app.get('/profile', requiresAuth(), (req, res) => { + res.json(req.oidc.user); +}); + +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', version: '1.0.0' }); +}); + +app.listen(3000, () => { + console.log('Server running on http://localhost:3000'); +}); diff --git a/tests/fixtures/node/example/index.html b/tests/fixtures/node/example/index.html new file mode 100644 index 0000000..51d0261 --- /dev/null +++ b/tests/fixtures/node/example/index.html @@ -0,0 +1,11 @@ + + + + AuthKit example + + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/node/example/package.json b/tests/fixtures/node/example/package.json new file mode 100644 index 0000000..7c012b8 --- /dev/null +++ b/tests/fixtures/node/example/package.json @@ -0,0 +1,12 @@ +{ + "name": "node-express-fixture", + "private": true, + "version": "0.0.0", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.21.0", + "dotenv": "^16.4.0" + } +} diff --git a/tests/fixtures/node/example/server.js b/tests/fixtures/node/example/server.js new file mode 100644 index 0000000..0adc397 --- /dev/null +++ b/tests/fixtures/node/example/server.js @@ -0,0 +1,12 @@ +const path = require('path'); +const express = require('express'); + +const app = express(); + +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); +}); + +app.listen(3000, () => { + console.log('Server running on http://localhost:3000'); +}); diff --git a/tests/fixtures/php-laravel/example-auth0/app/Http/Controllers/AuthController.php b/tests/fixtures/php-laravel/example-auth0/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..883a02c --- /dev/null +++ b/tests/fixtures/php-laravel/example-auth0/app/Http/Controllers/AuthController.php @@ -0,0 +1,32 @@ + $user]); + } + + public function login() + { + return Auth0::login(); + } + + public function callback() + { + Auth0::callback(); + return redirect('/'); + } + + public function logout() + { + Auth0::logout(); + return redirect('/'); + } +} diff --git a/tests/fixtures/php-laravel/example-auth0/artisan b/tests/fixtures/php-laravel/example-auth0/artisan new file mode 100644 index 0000000..4504e57 --- /dev/null +++ b/tests/fixtures/php-laravel/example-auth0/artisan @@ -0,0 +1,10 @@ +#!/usr/bin/env php +make(Illuminate\Contracts\Console\Kernel::class); +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); +exit($status); diff --git a/tests/fixtures/php-laravel/example-auth0/bootstrap/app.php b/tests/fixtures/php-laravel/example-auth0/bootstrap/app.php new file mode 100644 index 0000000..2ae1cd0 --- /dev/null +++ b/tests/fixtures/php-laravel/example-auth0/bootstrap/app.php @@ -0,0 +1,4 @@ + env('AUTH0_DOMAIN'), + 'clientId' => env('AUTH0_CLIENT_ID'), + 'clientSecret' => env('AUTH0_CLIENT_SECRET'), + 'cookieSecret' => env('AUTH0_COOKIE_SECRET'), + + 'routes' => [ + 'login' => '/login', + 'callback' => '/callback', + 'logout' => '/logout', + ], +]; diff --git a/tests/fixtures/php-laravel/example-auth0/resources/views/welcome.blade.php b/tests/fixtures/php-laravel/example-auth0/resources/views/welcome.blade.php new file mode 100644 index 0000000..5c06a10 --- /dev/null +++ b/tests/fixtures/php-laravel/example-auth0/resources/views/welcome.blade.php @@ -0,0 +1,9 @@ + + + AuthKit example + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/php-laravel/example-auth0/routes/web.php b/tests/fixtures/php-laravel/example-auth0/routes/web.php new file mode 100644 index 0000000..2202439 --- /dev/null +++ b/tests/fixtures/php-laravel/example-auth0/routes/web.php @@ -0,0 +1,12 @@ +json(['status' => 'ok', 'version' => '1.0.0']); +}); +Route::get('/login', [AuthController::class, 'login'])->name('login'); +Route::get('/callback', [AuthController::class, 'callback']); +Route::get('/logout', [AuthController::class, 'logout'])->name('logout'); diff --git a/tests/fixtures/php-laravel/example/artisan b/tests/fixtures/php-laravel/example/artisan new file mode 100644 index 0000000..4504e57 --- /dev/null +++ b/tests/fixtures/php-laravel/example/artisan @@ -0,0 +1,10 @@ +#!/usr/bin/env php +make(Illuminate\Contracts\Console\Kernel::class); +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); +exit($status); diff --git a/tests/fixtures/php-laravel/example/bootstrap/app.php b/tests/fixtures/php-laravel/example/bootstrap/app.php new file mode 100644 index 0000000..2ae1cd0 --- /dev/null +++ b/tests/fixtures/php-laravel/example/bootstrap/app.php @@ -0,0 +1,4 @@ + + + AuthKit example + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/php-laravel/example/routes/web.php b/tests/fixtures/php-laravel/example/routes/web.php new file mode 100644 index 0000000..86a06c5 --- /dev/null +++ b/tests/fixtures/php-laravel/example/routes/web.php @@ -0,0 +1,7 @@ +=8.1", + "auth0/auth0-php": "^8.0", + "vlucas/phpdotenv": "^5.5" + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + } +} diff --git a/tests/fixtures/php/example-auth0/public/index.php b/tests/fixtures/php/example-auth0/public/index.php new file mode 100644 index 0000000..01404c7 --- /dev/null +++ b/tests/fixtures/php/example-auth0/public/index.php @@ -0,0 +1,66 @@ +load(); + +$auth0 = new Auth0([ + 'domain' => $_ENV['AUTH0_DOMAIN'], + 'clientId' => $_ENV['AUTH0_CLIENT_ID'], + 'clientSecret' => $_ENV['AUTH0_CLIENT_SECRET'], + 'cookieSecret' => $_ENV['AUTH0_COOKIE_SECRET'], +]); + +$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + +switch ($path) { + case '/api/health': + header('Content-Type: application/json'); + echo json_encode(['status' => 'ok', 'version' => '1.0.0']); + exit; + + case '/login': + $auth0->clear(); + header('Location: ' . $auth0->login( + $_ENV['AUTH0_CALLBACK_URL'] + )); + exit; + + case '/callback': + $auth0->exchange( + $_ENV['AUTH0_CALLBACK_URL'] + ); + $_SESSION['user'] = $auth0->getUser(); + header('Location: /'); + exit; + + case '/logout': + $auth0->logout(); + session_destroy(); + header('Location: ' . $auth0->getLogoutLink( + $_ENV['AUTH0_CALLBACK_URL'] + )); + exit; +} + +$user = $_SESSION['user'] ?? null; +?> + + + AuthKit example + +

AuthKit example

+ +

Welcome,

+

Sign out

+ +

Sign in

+ + + diff --git a/tests/fixtures/php/example/composer.json b/tests/fixtures/php/example/composer.json new file mode 100644 index 0000000..c8a3b8c --- /dev/null +++ b/tests/fixtures/php/example/composer.json @@ -0,0 +1,11 @@ +{ + "name": "example/authkit-php", + "require": { + "php": ">=8.1" + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + } +} diff --git a/tests/fixtures/php/example/public/index.php b/tests/fixtures/php/example/public/index.php new file mode 100644 index 0000000..6b3c262 --- /dev/null +++ b/tests/fixtures/php/example/public/index.php @@ -0,0 +1,9 @@ + + + AuthKit example + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/python/example-auth0/index.html b/tests/fixtures/python/example-auth0/index.html new file mode 100644 index 0000000..51d0261 --- /dev/null +++ b/tests/fixtures/python/example-auth0/index.html @@ -0,0 +1,11 @@ + + + + AuthKit example + + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/python/example-auth0/requirements.txt b/tests/fixtures/python/example-auth0/requirements.txt new file mode 100644 index 0000000..09b32f9 --- /dev/null +++ b/tests/fixtures/python/example-auth0/requirements.txt @@ -0,0 +1,4 @@ +flask +python-dotenv +authlib +requests diff --git a/tests/fixtures/python/example-auth0/server.py b/tests/fixtures/python/example-auth0/server.py new file mode 100644 index 0000000..6a5d322 --- /dev/null +++ b/tests/fixtures/python/example-auth0/server.py @@ -0,0 +1,63 @@ +import os +from flask import Flask, redirect, session, url_for, send_file +from dotenv import load_dotenv +from authlib.integrations.flask_client import OAuth + +load_dotenv() + +app = Flask(__name__) +app.secret_key = os.environ.get("FLASK_SECRET_KEY", "super-secret-key") + +oauth = OAuth(app) +auth0 = oauth.register( + "auth0", + client_id=os.environ.get("AUTH0_CLIENT_ID"), + client_secret=os.environ.get("AUTH0_CLIENT_SECRET"), + api_base_url=f'https://{os.environ.get("AUTH0_DOMAIN")}', + access_token_url=f'https://{os.environ.get("AUTH0_DOMAIN")}/oauth/token', + authorize_url=f'https://{os.environ.get("AUTH0_DOMAIN")}/authorize', + client_kwargs={"scope": "openid profile email"}, +) + + +@app.route("/") +def index(): + user = session.get("user") + if user: + return f"

Welcome, {user['name']}

Sign out

" + return send_file("index.html") + + +@app.route("/login") +def login(): + return auth0.authorize_redirect(redirect_uri=url_for("callback", _external=True)) + + +@app.route("/callback") +def callback(): + token = auth0.authorize_access_token() + user_info = token.get("userinfo") + if user_info is None: + user_info = auth0.get("userinfo").json() + session["user"] = user_info + return redirect("/") + + +@app.route("/api/health") +def health(): + return {"status": "ok", "version": "1.0.0"} + + +@app.route("/logout") +def logout(): + session.clear() + auth0_domain = os.environ.get("AUTH0_DOMAIN") + client_id = os.environ.get("AUTH0_CLIENT_ID") + return_to = url_for("index", _external=True) + return redirect( + f"https://{auth0_domain}/v2/logout?client_id={client_id}&returnTo={return_to}" + ) + + +if __name__ == "__main__": + app.run(debug=True, port=3000) diff --git a/tests/fixtures/python/example/index.html b/tests/fixtures/python/example/index.html new file mode 100644 index 0000000..51d0261 --- /dev/null +++ b/tests/fixtures/python/example/index.html @@ -0,0 +1,11 @@ + + + + AuthKit example + + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/python/example/requirements.txt b/tests/fixtures/python/example/requirements.txt new file mode 100644 index 0000000..f34604e --- /dev/null +++ b/tests/fixtures/python/example/requirements.txt @@ -0,0 +1,2 @@ +flask +python-dotenv diff --git a/tests/fixtures/python/example/server.py b/tests/fixtures/python/example/server.py new file mode 100644 index 0000000..038e9cb --- /dev/null +++ b/tests/fixtures/python/example/server.py @@ -0,0 +1,12 @@ +from flask import Flask, send_file + +app = Flask(__name__) + + +@app.route("/") +def index(): + return send_file("index.html") + + +if __name__ == "__main__": + app.run(debug=True, port=3000) diff --git a/tests/fixtures/ruby/example-auth0/Gemfile b/tests/fixtures/ruby/example-auth0/Gemfile new file mode 100644 index 0000000..44db9e7 --- /dev/null +++ b/tests/fixtures/ruby/example-auth0/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +gem "sinatra" +gem "omniauth" +gem "omniauth-auth0" +gem "rack_csrf", require: "rack/csrf" diff --git a/tests/fixtures/ruby/example-auth0/index.html b/tests/fixtures/ruby/example-auth0/index.html new file mode 100644 index 0000000..51d0261 --- /dev/null +++ b/tests/fixtures/ruby/example-auth0/index.html @@ -0,0 +1,11 @@ + + + + AuthKit example + + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/ruby/example-auth0/server.rb b/tests/fixtures/ruby/example-auth0/server.rb new file mode 100644 index 0000000..16cc4e5 --- /dev/null +++ b/tests/fixtures/ruby/example-auth0/server.rb @@ -0,0 +1,45 @@ +require "sinatra" +require "omniauth" +require "omniauth-auth0" + +enable :sessions +set :session_secret, ENV.fetch("SESSION_SECRET", "super-secret-key") +set :port, 3000 + +use OmniAuth::Builder do + provider :auth0, + ENV["AUTH0_CLIENT_ID"], + ENV["AUTH0_CLIENT_SECRET"], + ENV["AUTH0_DOMAIN"] +end + +get "/" do + if session[:user] + "" \ + "

Welcome, #{session[:user]["info"]["name"]}

" \ + "

Sign out

" \ + "" + else + send_file "index.html" + end +end + +get "/api/health" do + content_type :json + '{"status":"ok","version":"1.0.0"}' +end + +get "/auth/auth0/callback" do + auth = request.env["omniauth.auth"] + session[:user] = auth + redirect "/" +end + +get "/auth/failure" do + "Authentication failed: #{params[:message]}" +end + +get "/logout" do + session.clear + redirect "/" +end diff --git a/tests/fixtures/ruby/example/Gemfile b/tests/fixtures/ruby/example/Gemfile new file mode 100644 index 0000000..185d241 --- /dev/null +++ b/tests/fixtures/ruby/example/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "sinatra" +gem "dotenv" diff --git a/tests/fixtures/ruby/example/index.html b/tests/fixtures/ruby/example/index.html new file mode 100644 index 0000000..51d0261 --- /dev/null +++ b/tests/fixtures/ruby/example/index.html @@ -0,0 +1,11 @@ + + + + AuthKit example + + +

AuthKit example

+

Sign in

+

Sign out

+ + diff --git a/tests/fixtures/ruby/example/server.rb b/tests/fixtures/ruby/example/server.rb new file mode 100644 index 0000000..45056bf --- /dev/null +++ b/tests/fixtures/ruby/example/server.rb @@ -0,0 +1,7 @@ +require "sinatra" + +set :port, 3000 + +get "/" do + send_file "index.html" +end diff --git a/tests/fixtures/sveltekit/example/.gitignore b/tests/fixtures/sveltekit/example/.gitignore new file mode 100644 index 0000000..1a341bd --- /dev/null +++ b/tests/fixtures/sveltekit/example/.gitignore @@ -0,0 +1,2 @@ +.svelte-kit +node_modules diff --git a/tests/fixtures/sveltekit/example/package.json b/tests/fixtures/sveltekit/example/package.json new file mode 100644 index 0000000..b41a046 --- /dev/null +++ b/tests/fixtures/sveltekit/example/package.json @@ -0,0 +1,21 @@ +{ + "name": "sveltekit-existing-fixture", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@sveltejs/kit": "^2.0.0", + "svelte": "^5.0.0" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "typescript": "~5.6.2", + "vite": "^5.0.0" + } +} diff --git a/tests/fixtures/sveltekit/example/src/app.html b/tests/fixtures/sveltekit/example/src/app.html new file mode 100644 index 0000000..adf8bd8 --- /dev/null +++ b/tests/fixtures/sveltekit/example/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/tests/fixtures/sveltekit/example/src/routes/+layout.svelte b/tests/fixtures/sveltekit/example/src/routes/+layout.svelte new file mode 100644 index 0000000..a394ce7 --- /dev/null +++ b/tests/fixtures/sveltekit/example/src/routes/+layout.svelte @@ -0,0 +1,5 @@ + + + diff --git a/tests/fixtures/sveltekit/example/src/routes/+page.svelte b/tests/fixtures/sveltekit/example/src/routes/+page.svelte new file mode 100644 index 0000000..7a2f8d2 --- /dev/null +++ b/tests/fixtures/sveltekit/example/src/routes/+page.svelte @@ -0,0 +1,36 @@ + + +
+

Welcome to SvelteKit

+

This is a minimal SvelteKit application.

+ + +
+ + diff --git a/tests/fixtures/sveltekit/example/svelte.config.js b/tests/fixtures/sveltekit/example/svelte.config.js new file mode 100644 index 0000000..2f86665 --- /dev/null +++ b/tests/fixtures/sveltekit/example/svelte.config.js @@ -0,0 +1,10 @@ +import adapter from '@sveltejs/adapter-auto'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter(), + }, +}; + +export default config; diff --git a/tests/fixtures/sveltekit/example/tsconfig.json b/tests/fixtures/sveltekit/example/tsconfig.json new file mode 100644 index 0000000..4344710 --- /dev/null +++ b/tests/fixtures/sveltekit/example/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/tests/fixtures/sveltekit/example/vite.config.ts b/tests/fixtures/sveltekit/example/vite.config.ts new file mode 100644 index 0000000..2e920e4 --- /dev/null +++ b/tests/fixtures/sveltekit/example/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], +});