diff --git a/samples/LocalWorkers/Trax.Samples.GameServer.Api/GoogleJwtResolver.cs b/samples/LocalWorkers/Trax.Samples.GameServer.Api/GoogleJwtResolver.cs new file mode 100644 index 0000000..4ab4d0a --- /dev/null +++ b/samples/LocalWorkers/Trax.Samples.GameServer.Api/GoogleJwtResolver.cs @@ -0,0 +1,40 @@ +using Trax.Api.Auth; +using Trax.Api.Auth.Jwt; +using Trax.Samples.GameServer.Auth; + +namespace Trax.Samples.GameServer.Api; + +/// +/// Maps a Google-issued id-token onto a Trax principal. The JWT bearer +/// handler has already verified signature, issuer, audience, and lifetime +/// against Google's JWKS before this runs. +/// +/// +/// Demo only: every authenticated Google user is granted the Player role so +/// sample trains work out of the box. A real deployment would look up the +/// principal in the game's user table and assign roles based on account +/// state, revoke unknown subjects, etc. +/// +internal sealed class GoogleJwtResolver : ITraxPrincipalResolver +{ + public ValueTask ResolveAsync(JwtTokenInput input, CancellationToken ct) + { + var sub = input.Principal.FindFirst("sub")?.Value; + if (string.IsNullOrWhiteSpace(sub)) + return new ValueTask((TraxPrincipal?)null); + + var name = + input.Principal.FindFirst("name")?.Value + ?? input.Principal.FindFirst("email")?.Value + ?? sub; + + return new ValueTask( + new TraxPrincipal( + Id: sub, + DisplayName: name, + Roles: [nameof(GameRole.Player)], + PrincipalType: JwtDefaults.PrincipalType + ) + ); + } +} diff --git a/samples/LocalWorkers/Trax.Samples.GameServer.Api/Program.cs b/samples/LocalWorkers/Trax.Samples.GameServer.Api/Program.cs index 599fde4..81a1355 100644 --- a/samples/LocalWorkers/Trax.Samples.GameServer.Api/Program.cs +++ b/samples/LocalWorkers/Trax.Samples.GameServer.Api/Program.cs @@ -6,9 +6,21 @@ // dispatch { trainName(mode: QUEUE) } mutations. This process does NOT run a scheduler — start the // Scheduler project alongside this one. // -// Authentication: fake API key via X-Api-Key header (for demonstration only) -// Admin key: admin-key-do-not-use-in-production (roles: Admin, Player) -// Player key: player-key-do-not-use-in-production (role: Player) +// Authentication (two schemes coexist, pick either per request): +// +// 1. API key via X-Api-Key header — service-to-service / scripting +// Admin key: admin-key-do-not-use-in-production (roles: Admin, Player) +// Player key: player-key-do-not-use-in-production (role: Player) +// +// 2. JWT bearer via Authorization: Bearer — the Next.js +// companion app (samples/LocalWorkers/trax-samples-gameserver-web) +// signs users in with Google via NextAuth, then forwards the id-token +// to this API. Trax validates against Google's JWKS. Enable by setting +// Google:ClientId in appsettings.json to your OAuth 2.0 client id from +// Google Cloud Console. +// +// Both schemes feed the same TraxPrincipal, so [TraxAuthorize] works against +// either credential type. // // Prerequisites: // 1. Start Postgres: cd Trax.Samples && docker compose up -d @@ -25,25 +37,25 @@ // curl -H "X-Api-Key: player-key-do-not-use-in-production" \ // -X POST http://localhost:5200/trax/graphql \ // -H "Content-Type: application/json" \ -// -d '{"query":"{ trains { serviceTypeName inputTypeName requiredPolicies requiredRoles inputSchema { name typeName } } }"}' +// -d '{"query":"{ operations { trains { serviceTypeName inputTypeName requiredPolicies requiredRoles inputSchema { name typeName } } } }"}' // // # Query a train directly (typed query from [TraxQuery]) // curl -H "X-Api-Key: player-key-do-not-use-in-production" \ // -X POST http://localhost:5200/trax/graphql \ // -H "Content-Type: application/json" \ -// -d '{"query":"{ discover { lookupPlayer(input: {playerId: \"player-42\"}) { playerId rank wins losses rating } } }"}' +// -d '{"query":"{ discover { players { lookupPlayer(input: {playerId: \"player-42\"}) { playerId rank wins losses rating } } } }"}' // // # Query model data directly with filtering and pagination ([TraxQueryModel]) // curl -H "X-Api-Key: player-key-do-not-use-in-production" \ // -X POST http://localhost:5200/trax/graphql \ // -H "Content-Type: application/json" \ -// -d '{"query":"{ discover { playerRecords(first: 10, where: { rating: { gte: 1500 } }) { nodes { playerId displayName rating } pageInfo { hasNextPage endCursor } } } }"}' +// -d '{"query":"{ discover { players { playerRecords(first: 10, where: { rating: { gte: 1500 } }) { nodes { playerId displayName rating } pageInfo { hasNextPage endCursor } } } } }"}' // // # Queue a heavy train for the scheduler (typed mutation from [TraxMutation]) // curl -H "X-Api-Key: player-key-do-not-use-in-production" \ // -X POST http://localhost:5200/trax/graphql \ // -H "Content-Type: application/json" \ -// -d '{"query":"mutation { dispatch { processMatchResult(input: {region: \"na\", matchId: \"match-999\", winnerId: \"player-1\", loserId: \"player-2\", winnerScore: 100, loserScore: 30}, mode: QUEUE, priority: 10) { externalId workQueueId } } }"}' +// -d '{"query":"mutation { dispatch { matches { processMatchResult(input: {region: \"na\", matchId: \"match-999\", winnerId: \"player-1\", loserId: \"player-2\", winnerScore: 100, loserScore: 30}, mode: QUEUE, priority: 10) { externalId workQueueId } } } }"}' // // # Subscribe to real-time train lifecycle events (use Banana Cake Pop IDE): // # subscription { onTrainStarted { metadataId trainName trainState timestamp } } @@ -52,10 +64,14 @@ // // # Health check (no auth required) // curl http://localhost:5200/trax/health +// +// # Google JWT path: see ../trax-samples-gameserver-web for a Next.js app +// # that signs in with Google via NextAuth and forwards the id-token here. // ───────────────────────────────────────────────────────────────────────────── using Microsoft.EntityFrameworkCore; using Trax.Api.Auth.ApiKey; +using Trax.Api.Auth.Jwt; using Trax.Api.Extensions; using Trax.Api.GraphQL.Extensions; using Trax.Effect.Data.Extensions; @@ -65,6 +81,7 @@ using Trax.Effect.Provider.Parameter.Extensions; using Trax.Mediator.Extensions; using Trax.Samples.GameServer; +using Trax.Samples.GameServer.Api; using Trax.Samples.GameServer.Auth; using Trax.Samples.GameServer.Data; using Trax.Samples.GameServer.Data.Models; @@ -94,12 +111,29 @@ ); }); -// ── Authentication, fake API key for demonstration (NO WARRANTY, see SECURITY-DISCLAIMER.md) ── +// ── Authentication (NO WARRANTY, see SECURITY-DISCLAIMER.md) ────────── +// Two schemes coexist. Both contribute to the combined TraxAuthPolicy, so a +// route gated by that policy accepts either credential type. + +// 1. Fake API keys for scripting / service-to-service. builder.Services.AddTraxApiKeyAuth(keys => keys.Add(SampleKeys.AdminKey, id: "admin", nameof(GameRole.Admin), nameof(GameRole.Player)) .Add(SampleKeys.PlayerKey, id: "player", nameof(GameRole.Player)) ); +// 2. JWT bearer: accept Google-issued id-tokens. The Next.js frontend obtains +// them via NextAuth and sends them as Authorization: Bearer . +// Signature validation happens against Google's published JWKS; only tokens +// minted for our specific OAuth client id are accepted (aud claim check). +var googleClientId = builder.Configuration["Google:ClientId"]; +if (!string.IsNullOrWhiteSpace(googleClientId)) +{ + builder.Services.AddTraxJwtAuth( + "https://accounts.google.com", + googleClientId + ); +} + // ── Authorization policies ────────────────────────────────────────────── builder.Services.AddAuthorization(options => { diff --git a/samples/LocalWorkers/Trax.Samples.GameServer.Api/Trax.Samples.GameServer.Api.csproj b/samples/LocalWorkers/Trax.Samples.GameServer.Api/Trax.Samples.GameServer.Api.csproj index eec17a9..64fc670 100644 --- a/samples/LocalWorkers/Trax.Samples.GameServer.Api/Trax.Samples.GameServer.Api.csproj +++ b/samples/LocalWorkers/Trax.Samples.GameServer.Api/Trax.Samples.GameServer.Api.csproj @@ -5,6 +5,7 @@ enable enable true + a4458be6-4c07-4927-ad99-1a366d87cb3b @@ -16,6 +17,7 @@ + diff --git a/samples/LocalWorkers/Trax.Samples.GameServer.Api/appsettings.json b/samples/LocalWorkers/Trax.Samples.GameServer.Api/appsettings.json index 0990ab0..3dd5cdc 100644 --- a/samples/LocalWorkers/Trax.Samples.GameServer.Api/appsettings.json +++ b/samples/LocalWorkers/Trax.Samples.GameServer.Api/appsettings.json @@ -14,5 +14,9 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "Google": { + "_comment": "To accept Google-issued JWTs from the Next.js frontend, set ClientId to the OAuth 2.0 client id from Google Cloud Console (the same one NextAuth uses). Leave blank to disable the JWT scheme; the API will still accept API keys for scripting.", + "ClientId": "" } } diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/.env.local.example b/samples/LocalWorkers/trax-samples-gameserver-web/.env.local.example new file mode 100644 index 0000000..8fb5835 --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/.env.local.example @@ -0,0 +1,14 @@ +# Copy to .env.local and fill in real values. + +# NextAuth secret used to encrypt the session JWT. Generate with: +# node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))" +AUTH_SECRET= + +# OAuth 2.0 client credentials from Google Cloud Console. +# Authorized redirect URI on the client must include: +# http://localhost:3000/api/auth/callback/google +AUTH_GOOGLE_ID= +AUTH_GOOGLE_SECRET= + +# Where the Trax GameServer API is listening. +NEXT_PUBLIC_TRAX_API=http://localhost:5200 diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/.gitignore b/samples/LocalWorkers/trax-samples-gameserver-web/.gitignore new file mode 100644 index 0000000..2fdbc2c --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/.gitignore @@ -0,0 +1,8 @@ +node_modules +.next +out +.env.local +.env.*.local +*.log +.DS_Store +next-env.d.ts diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/README.md b/samples/LocalWorkers/trax-samples-gameserver-web/README.md new file mode 100644 index 0000000..f9823e0 --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/README.md @@ -0,0 +1,94 @@ +# Trax GameServer · Web + +A minimal Next.js companion for `Trax.Samples.GameServer.Api`. Signs users in +with Google via NextAuth, then forwards the Google-issued id-token to the +Trax GraphQL API as an `Authorization: Bearer` credential. The API validates +the token against Google's JWKS via `AddTraxJwtAuth("https://accounts.google.com", googleClientId)`. + +> **Demo only.** The resolver in `GoogleJwtResolver.cs` grants the `Player` +> role to every authenticated Google user so the sample's trains work out of +> the box. Do not copy that pattern into anything real. + +## Prerequisites + +- Node.js 20 or newer, npm 10 or newer +- A running `Trax.Samples.GameServer.Api` on `http://localhost:5200` +- A Google Cloud project with an OAuth 2.0 client + +## 1. Create a Google OAuth client + +1. Open . +2. Create a new OAuth 2.0 Client ID of type **Web application**. +3. Add authorized redirect URI: + ``` + http://localhost:3000/api/auth/callback/google + ``` +4. Save the client id and client secret. + +## 2. Configure the web app + +```bash +cp .env.local.example .env.local +``` + +Fill in: + +- `AUTH_SECRET` — generate with `node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"` +- `AUTH_GOOGLE_ID` and `AUTH_GOOGLE_SECRET` — from step 1 +- `NEXT_PUBLIC_TRAX_API` — leave as `http://localhost:5200` unless the API runs elsewhere + +## 3. Configure the Trax API + +Open `../Trax.Samples.GameServer.Api/appsettings.json` and set: + +```json +"Google": { + "ClientId": "" +} +``` + +The API uses this value as the expected `aud` claim when validating tokens. +If it is blank, the API silently skips JWT registration and only accepts +API-key credentials. + +## 4. Run + +```bash +# Terminal 1: API (from the repo root) +dotnet run --project Trax.Samples/samples/LocalWorkers/Trax.Samples.GameServer.Scheduler +dotnet run --project Trax.Samples/samples/LocalWorkers/Trax.Samples.GameServer.Api + +# Terminal 2: web +cd Trax.Samples/samples/LocalWorkers/trax-samples-gameserver-web +npm install +npm run dev +``` + +Open , sign in, click **Discover trains**. + +## How the pieces fit + +``` +┌────────────┐ 1. sign in ┌──────────┐ +│ Browser │ ────────────────────>│ Google │ +│ (Next.js) │ <── id-token ────────│ OIDC │ +└────────────┘ └──────────┘ + │ + │ 2. fetch /trax/graphql + │ Authorization: Bearer + ▼ +┌────────────────────────┐ +│ Trax GameServer · API │ +│ │ +│ AddTraxJwtAuth( │ +│ "accounts.google.com"│──── 3. fetch JWKS ────> Google +│ clientId │ (cached) +│ ) │ +│ │ +│ GoogleJwtResolver maps │ +│ sub → TraxPrincipal.Id │ +└────────────────────────┘ +``` + +NextAuth owns the interactive browser flow. The Trax API is pure JWT +validation: no cookies, no redirects, no session state. diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/next.config.js b/samples/LocalWorkers/trax-samples-gameserver-web/next.config.js new file mode 100644 index 0000000..3dd7ef1 --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +module.exports = { + reactStrictMode: true, +}; diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/package-lock.json b/samples/LocalWorkers/trax-samples-gameserver-web/package-lock.json new file mode 100644 index 0000000..fbecfab --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/package-lock.json @@ -0,0 +1,1126 @@ +{ + "name": "trax-samples-gameserver-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "trax-samples-gameserver-web", + "version": "0.0.0", + "dependencies": { + "next": "^15.0.0", + "next-auth": "^5.0.0-beta.25", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.5.0" + } + }, + "node_modules/@auth/core": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz", + "integrity": "sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^7.0.7" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.15.tgz", + "integrity": "sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.15.tgz", + "integrity": "sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.15.tgz", + "integrity": "sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.15.tgz", + "integrity": "sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.15.tgz", + "integrity": "sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.15.tgz", + "integrity": "sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.15.tgz", + "integrity": "sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.15.tgz", + "integrity": "sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.15.tgz", + "integrity": "sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz", + "integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.15", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.15", + "@next/swc-darwin-x64": "15.5.15", + "@next/swc-linux-arm64-gnu": "15.5.15", + "@next/swc-linux-arm64-musl": "15.5.15", + "@next/swc-linux-x64-gnu": "15.5.15", + "@next/swc-linux-x64-musl": "15.5.15", + "@next/swc-win32-arm64-msvc": "15.5.15", + "@next/swc-win32-x64-msvc": "15.5.15", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "5.0.0-beta.31", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.31.tgz", + "integrity": "sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.2" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/oauth4webapi": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.5.tgz", + "integrity": "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/package.json b/samples/LocalWorkers/trax-samples-gameserver-web/package.json new file mode 100644 index 0000000..23a8852 --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/package.json @@ -0,0 +1,23 @@ +{ + "name": "trax-samples-gameserver-web", + "version": "0.0.0", + "private": true, + "description": "Next.js companion app for Trax.Samples.GameServer.Api. Signs users in with Google via NextAuth, forwards the Google-issued id-token to the Trax API as a bearer credential.", + "scripts": { + "dev": "next dev --port 3000", + "build": "next build", + "start": "next start --port 3000" + }, + "dependencies": { + "next": "^15.0.0", + "next-auth": "^5.0.0-beta.25", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.5.0" + } +} diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/src/app/api/auth/[...nextauth]/route.ts b/samples/LocalWorkers/trax-samples-gameserver-web/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..86c9f3d --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/auth"; + +export const { GET, POST } = handlers; diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/src/app/globals.css b/samples/LocalWorkers/trax-samples-gameserver-web/src/app/globals.css new file mode 100644 index 0000000..6a82dcd --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/src/app/globals.css @@ -0,0 +1,134 @@ +:root { + --bg: #0b0f14; + --panel: #131a23; + --panel-border: #1f2a36; + --fg: #e6edf3; + --muted: #8ea0b2; + --accent: #4f8cc9; + --accent-hover: #5ea4e7; + --danger: #e06c75; + --success: #7ec16e; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--fg); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + min-height: 100vh; +} + +main { + max-width: 720px; + margin: 0 auto; + padding: 48px 24px; +} + +h1 { + font-size: 28px; + margin: 0 0 8px; +} + +.subtitle { + color: var(--muted); + margin: 0 0 32px; +} + +.panel { + background: var(--panel); + border: 1px solid var(--panel-border); + border-radius: 8px; + padding: 24px; + margin-bottom: 16px; +} + +.panel h2 { + margin: 0 0 12px; + font-size: 16px; + font-weight: 600; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.row { + display: flex; + gap: 12px; + align-items: center; + flex-wrap: wrap; +} + +button { + background: var(--accent); + color: white; + border: 0; + padding: 10px 16px; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.12s ease; +} + +button:hover:not(:disabled) { + background: var(--accent-hover); +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +button.secondary { + background: transparent; + color: var(--fg); + border: 1px solid var(--panel-border); +} + +button.secondary:hover:not(:disabled) { + background: var(--panel-border); +} + +pre { + background: #0a0d12; + border: 1px solid var(--panel-border); + border-radius: 6px; + padding: 16px; + margin: 12px 0 0; + overflow-x: auto; + font-size: 12px; + line-height: 1.6; + color: #c9d1d9; +} + +.err { + color: var(--danger); +} + +.ok { + color: var(--success); +} + +.kv { + display: grid; + grid-template-columns: 120px 1fr; + gap: 4px 12px; + font-size: 13px; +} + +.kv dt { + color: var(--muted); + margin: 0; +} + +.kv dd { + margin: 0; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + word-break: break-all; +} diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/src/app/layout.tsx b/samples/LocalWorkers/trax-samples-gameserver-web/src/app/layout.tsx new file mode 100644 index 0000000..b8a0d7e --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/src/app/layout.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; +import { Providers } from "./providers"; +import "./globals.css"; + +export const metadata = { + title: "Trax GameServer · Web", + description: "Next.js companion app for the Trax GameServer sample API.", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/src/app/page.tsx b/samples/LocalWorkers/trax-samples-gameserver-web/src/app/page.tsx new file mode 100644 index 0000000..8af0b6c --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/src/app/page.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { signIn, signOut, useSession } from "next-auth/react"; +import { useState } from "react"; + +const TRAX_API = process.env.NEXT_PUBLIC_TRAX_API ?? "http://localhost:5200"; + +const DISCOVERY_QUERY = `{ + operations { + trains { + serviceTypeName + inputTypeName + requiredPolicies + requiredRoles + } + } +}`; + +const PLAYER_QUERY = `{ + discover { + players { + lookupPlayer(input: { playerId: "player-1" }) { + playerId rank wins losses rating + } + } + } +}`; + +type GqlResult = { data?: unknown; errors?: unknown[] } | { _err: string }; + +export default function Home() { + const { data: session, status } = useSession(); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + async function runQuery(query: string) { + if (!session?.idToken) return; + setLoading(true); + setResult(null); + try { + const res = await fetch(`${TRAX_API}/trax/graphql`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${session.idToken}`, + }, + body: JSON.stringify({ query }), + }); + const body = await res.json(); + setResult(body); + } catch (err) { + setResult({ _err: err instanceof Error ? err.message : String(err) }); + } finally { + setLoading(false); + } + } + + if (status === "loading") { + return ( +
+

Loading session...

+
+ ); + } + + if (!session) { + return ( +
+

Trax GameServer · Web

+

+ Sign in with Google to obtain an id-token, then call the Trax GraphQL API with it. +

+
+

Not signed in

+ +
+
+ ); + } + + return ( +
+

Trax GameServer · Web

+

+ Signed in. The Google id-token below is forwarded as{" "} + Authorization: Bearer to the Trax API. +

+ +
+

Session

+
+
Name
+
{session.user?.name ?? "(none)"}
+
Email
+
{session.user?.email ?? "(none)"}
+
id-token
+
{session.idToken ? `${session.idToken.slice(0, 40)}...` : "(missing)"}
+
+
+ +
+
+ +
+

GraphQL calls

+
+ + +
+ {result && ( +
{JSON.stringify(result, null, 2)}
+ )} +
+
+ ); +} diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/src/app/providers.tsx b/samples/LocalWorkers/trax-samples-gameserver-web/src/app/providers.tsx new file mode 100644 index 0000000..550b010 --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/src/app/providers.tsx @@ -0,0 +1,8 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; +import type { ReactNode } from "react"; + +export function Providers({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/src/auth.ts b/samples/LocalWorkers/trax-samples-gameserver-web/src/auth.ts new file mode 100644 index 0000000..163301e --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/src/auth.ts @@ -0,0 +1,41 @@ +import NextAuth from "next-auth"; +import Google from "next-auth/providers/google"; + +/** + * NextAuth v5 configuration. Signs users in via Google, then captures the + * Google-issued id-token so the frontend can forward it to the Trax API + * as an Authorization: Bearer credential. The Trax API validates the token + * against Google's JWKS (see Program.cs: AddTraxJwtAuth with the Google + * authority). + */ +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [Google], + callbacks: { + // Persist the Google id-token on the NextAuth session JWT so we can + // read it from the client without a round trip. + jwt({ token, account }) { + if (account?.id_token) { + token.idToken = account.id_token; + } + return token; + }, + session({ session, token }) { + if (token.idToken) { + session.idToken = token.idToken as string; + } + return session; + }, + }, +}); + +declare module "next-auth" { + interface Session { + idToken?: string; + } +} + +declare module "@auth/core/jwt" { + interface JWT { + idToken?: string; + } +} diff --git a/samples/LocalWorkers/trax-samples-gameserver-web/tsconfig.json b/samples/LocalWorkers/trax-samples-gameserver-web/tsconfig.json new file mode 100644 index 0000000..334fafd --- /dev/null +++ b/samples/LocalWorkers/trax-samples-gameserver-web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./src/*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}