{"id":49899771,"url":"https://github.com/blakearoberts/visage","last_synced_at":"2026-06-12T02:01:20.431Z","repository":{"id":357033531,"uuid":"1234333758","full_name":"blakearoberts/visage","owner":"blakearoberts","description":"Visage is a Vite plugin for local development with HMR and OIDC session cookie lifecycle semantics.","archived":false,"fork":false,"pushed_at":"2026-06-03T02:28:35.000Z","size":422,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-03T04:18:03.275Z","etag":null,"topics":["authentication","hmr","local-development","oauth2","oidc","pkce","session-cookie","vite","vite-plugin"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/blakearoberts.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-10T03:37:14.000Z","updated_at":"2026-06-03T02:26:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/blakearoberts/visage","commit_stats":null,"previous_names":["blakearoberts/visage"],"tags_count":41,"template":false,"template_full_name":null,"purl":"pkg:github/blakearoberts/visage","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blakearoberts%2Fvisage","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blakearoberts%2Fvisage/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blakearoberts%2Fvisage/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blakearoberts%2Fvisage/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/blakearoberts","download_url":"https://codeload.github.com/blakearoberts/visage/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blakearoberts%2Fvisage/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34225351,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-12T02:00:06.859Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","hmr","local-development","oauth2","oidc","pkce","session-cookie","vite","vite-plugin"],"created_at":"2026-05-16T03:23:18.953Z","updated_at":"2026-06-12T02:01:20.414Z","avatar_url":"https://github.com/blakearoberts.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Visage\n\nVisage (`/vit·ɛdʒ/`) is a Vite plugin for local development with HMR and OIDC\nsession cookie lifecycle semantics.\n\n## Getting Started\n\nInstall Visage from npm:\n\n```console\nnpm install @blakearoberts/visage@next\n```\n\nAdd the plugin to `vite.config.ts`:\n\n```ts\nimport { defineConfig } from 'vite';\nimport visage from '@blakearoberts/visage';\n\nexport default defineConfig({\n  plugins: [visage()],\n});\n```\n\nStart Vite normally:\n\n```console\nvite\n```\n\nBy default, you can reach the app at `https://localhost:9001`. You will be\nredirected to Dex to sign in. The default username and password is\n`user@example.com` and `pass`.\n\n## Why Visage\n\nVisage is a local development harness for web apps that run behind an\nauth-protected edge, where browser sessions are represented by secure cookies\nbacked by OIDC tokens.\n\nVisage narrows the gap between local development, automated tests, and\nproduction by bringing production-like session lifecycle semantics to local Vite\ndevelopment without giving up HMR. That makes it practical to iterate on SSR\nidentity injection, session timeout recovery, lock screens, and authenticated\nAPI calls.\n\nVisage can also use a hosted IdP, so local frontend code can call hosted backend\nAPIs with real credentials. That avoids frontend-only auth mocks or backend-only\nlocal bypasses: code can be written for production and still work locally.\n\n## Configuration\n\nVisage is configured through `visage(options?)` in `vite.config.ts`.\n\nThe top-level `host` and `port` configure the local Visage origin that the\nbrowser visits:\n\n```ts\nvisage({ host: 'localhost', port: 9001 });\n```\n\n### Services\n\nServices are Docker Compose services managed by the Vite dev-server lifecycle.\nAdditional services automatically get a matching managed upstream with the same\nname, host, and default `/{name}/` location.\n\n```ts\nvisage({\n  services: { whoami: { image: 'traefik/whoami' } },\n});\n```\n\n### Upstreams\n\nUpstreams are proxy targets that Visage routes to. A top-level upstream with no\nmatching service entry is treated as an external upstream.\n\n```ts\nvisage({\n  upstreams: {\n    api: { host: 'api.local.test', locations: { '/api/': {} } },\n  },\n});\n```\n\nAuthenticated upstream locations do not forward bearer tokens by default. Set\n`auth.forward` to `true` to forward the default bearer token for the upstream\nkind: external upstreams receive the OAuth access token, while local service\nupstreams receive the OIDC ID token.\n\nHosted backend APIs that validate bearer auth should generally receive the\naccess token, provided the token is issued for that API's issuer, audience, and\nscopes. Use `'access'` or `'id'` when you need to force a specific token kind.\n\n```ts\nvisage({\n  upstreams: {\n    api: {\n      locations: {\n        '/api/': { auth: { forward: true } },\n      },\n    },\n  },\n});\n```\n\nOAuth2 Proxy identity values can also be mapped explicitly through headers such\nas `$auth_user`, `$auth_email`, `$auth_groups`, and `$auth_preferred_username`.\n\nAuthenticated locations also get Fetch Metadata CSRF checks by default. The\nbuilt-in Vite root location uses `csrf: 'app'`, which allows same-origin\nrequests and top-level `GET` document navigations. Other authenticated upstream\nlocations use `csrf: 'api'`, which blocks same-site and cross-site browser\nrequests when modern Fetch Metadata headers are present. Set `csrf: 'app'` for\nan upstream that serves browser pages, or `csrf: false` when the upstream\nintentionally handles cross-site browser requests itself.\n\n### External IdPs\n\nExternal OIDC providers use issuer discovery by default:\n\n```ts\nvisage({\n  idp: { issuer: 'https://idp.example.test/oauth2/default' },\n});\n```\n\nConfigure `authorization`, `token`, or `jwks` only when the provider endpoints\nmust be rendered explicitly instead of discovered from the issuer. Configure\n`end_session_endpoint` when the provider supports OIDC end-session redirects.\n\nSee [`VisageOptions`](src/types.ts) for the full option surface.\n\n## System Block Diagram\n\n```mermaid\nflowchart LR\n  Browser\n  NGINX\n  Oauth2-Proxy\n  Vite\n  IDP[\"IdP (Dex)\"]\n  ServicesUpstreams[\"Services / Upstreams\"]\n\n  Browser --\u003e NGINX\n  NGINX --\u003e Oauth2-Proxy\n  Oauth2-Proxy --\u003e IDP\n  NGINX --\u003e Vite\n  NGINX --\u003e ServicesUpstreams\n```\n\n## Required Tools\n\n- [Docker](https://docs.docker.com/get-started/get-docker/) with Compose v2\n  support through `docker compose`.\n- [`mkcert`](https://github.com/FiloSottile/mkcert#installation) installed on\n  `PATH`, or configured with `VISAGE_MKCERT=/path/to/mkcert`.\n\n## Managed Docker Images\n\nVisage pulls these as needed based on configuration:\n\n| Service                                                      | Image                                                                                       | Pin                                   |\n| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------- |\n| [NGINX](https://nginx.org/)                                  | [`nginx`](https://hub.docker.com/_/nginx)                                                   | [manifest](docker-compose.images.yml) |\n| [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy/) | [`quay.io/oauth2-proxy/oauth2-proxy`](https://quay.io/repository/oauth2-proxy/oauth2-proxy) | [manifest](docker-compose.images.yml) |\n| [Dex](https://dexidp.io/)                                    | [`ghcr.io/dexidp/dex`](https://github.com/dexidp/dex/pkgs/container/dex)                    | [manifest](docker-compose.images.yml) |\n\n## Security Notes\n\nVisage is local-development tooling. It starts local auth infrastructure,\nterminates local HTTPS, and forwards authenticated identity or token material to\nconfigured upstreams.\n\nPlease report suspected vulnerabilities through GitHub private vulnerability\nreporting as described in [Security Policy](SECURITY.md).\n\nDo not treat the managed Dex and OAuth2 Proxy defaults as production auth\ninfrastructure.\n\nVisage's CSRF policy is an edge request-isolation guard for cookie-backed\nlocations. It is not a replacement for application-owned CSRF tokens where an\napplication accepts form posts or other browser-submitted mutations. CSP,\n`frame-ancestors`, and other click-jacking controls remain application policy.\n\n## Troubleshooting\n\n- If startup fails immediately, confirm Docker is running and `docker compose`\n  works.\n- If NGINX cannot start, check whether the configured `port` is already in use.\n- If the hostname cannot be resolved, Visage may need permission to update\n  `/etc/hosts`.\n- If the browser rejects the certificate, allow the local certificate authority\n  prompt from `mkcert`; CI test runners should be configured to ignore local\n  HTTPS errors.\n\n## TO-DO\n\n- [ ] Harden the default security posture by addressing the\n      [security hardening backlog](docs/security-hardening.md).\n- [ ] Support [runtime config reloads](docs/config-reload.md).\n- [ ] Support [Dex connectors](https://dexidp.io/docs/connectors/).\n- [ ] Support Dex on a distinct subdomain, such as `auth.localhost`.\n- [ ] Support [HTTP mode without local TLS](docs/tls-http-mode.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblakearoberts%2Fvisage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fblakearoberts%2Fvisage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblakearoberts%2Fvisage/lists"}