@blakearoberts/visage 0.0.1-rc.18 → 0.0.1-rc.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,7 @@ Start Vite normally:
27
27
  vite
28
28
  ```
29
29
 
30
- By default, you can reach the app at [https://localhost:9001/](https://localhost:9001/). You will be redirected to Dex to sign in. The default username and password is `user@example.com` and `pass`.
30
+ By default, you can reach the app at `https://localhost:9001`. You will be redirected to Dex to sign in. The default username and password is `user@example.com` and `pass`.
31
31
 
32
32
  ## Why Visage
33
33
 
@@ -72,17 +72,21 @@ visage({
72
72
  });
73
73
  ```
74
74
 
75
- Authenticated upstream locations forward the OIDC ID token as the upstream
76
- `Authorization` bearer value by default. Set `auth.forward` to `'access'` for
77
- legacy upstreams that explicitly expect the OAuth access token as
78
- `Authorization: Bearer ...`.
75
+ Authenticated upstream locations do not forward bearer tokens by default. Set
76
+ `auth.forward` to `true` to forward the default bearer token for the upstream
77
+ kind: external upstreams receive the OAuth access token, while local service
78
+ upstreams receive the OIDC ID token.
79
+
80
+ Hosted backend APIs that validate bearer auth should generally receive the
81
+ access token, provided the token is issued for that API's issuer, audience, and
82
+ scopes. Use `'access'` or `'id'` when you need to force a specific token kind.
79
83
 
80
84
  ```ts
81
85
  visage({
82
86
  upstreams: {
83
87
  api: {
84
88
  locations: {
85
- '/api/': { auth: { forward: 'access' } },
89
+ '/api/': { auth: { forward: true } },
86
90
  },
87
91
  },
88
92
  },
@@ -93,6 +97,20 @@ OAuth2 Proxy identity values can also be mapped explicitly through headers such
93
97
  as `$auth_user`, `$auth_email`, `$auth_groups`, and
94
98
  `$auth_preferred_username`.
95
99
 
100
+ ### External IdPs
101
+
102
+ External OIDC providers use issuer discovery by default:
103
+
104
+ ```ts
105
+ visage({
106
+ idp: { issuer: 'https://idp.example.test/oauth2/default' },
107
+ });
108
+ ```
109
+
110
+ Configure `authorization`, `token`, or `jwks` only when the provider endpoints
111
+ must be rendered explicitly instead of discovered from the issuer. Configure
112
+ `end_session_endpoint` when the provider supports OIDC end-session redirects.
113
+
96
114
  See [`VisageOptions`](src/types.ts) for the full option surface.
97
115
 
98
116
  ## Expected Local URLs
@@ -139,11 +157,11 @@ Visage downloads [`mkcert`](https://github.com/FiloSottile/mkcert) from `dl.fili
139
157
 
140
158
  Visage pulls these as needed based on configuration:
141
159
 
142
- | Service | Image | Source |
143
- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------- |
144
- | [NGINX](https://nginx.org/) | [`nginx:1.30.0-alpine`](https://hub.docker.com/_/nginx) | Docker Hub |
145
- | [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy/) | [`quay.io/oauth2-proxy/oauth2-proxy:v7.15.2`](https://quay.io/repository/oauth2-proxy/oauth2-proxy) | Quay |
146
- | [Dex](https://dexidp.io/) | [`ghcr.io/dexidp/dex:v2.45.1`](https://github.com/dexidp/dex/pkgs/container/dex) | GitHub Container Registry |
160
+ | Service | Image | Pin |
161
+ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------- |
162
+ | [NGINX](https://nginx.org/) | [`nginx`](https://hub.docker.com/_/nginx) | [manifest](docker-compose.images.yml) |
163
+ | [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) |
164
+ | [Dex](https://dexidp.io/) | [`ghcr.io/dexidp/dex`](https://github.com/dexidp/dex/pkgs/container/dex) | [manifest](docker-compose.images.yml) |
147
165
 
148
166
  ## Security Notes
149
167
 
@@ -160,7 +178,7 @@ Do not treat the managed Dex and OAuth2 Proxy defaults as production auth infras
160
178
 
161
179
  ## TO-DO
162
180
 
163
- - [ ] Support OIDC auto-discovery with external IdP configuration.
181
+ - [ ] Support CSRF (click-jacking) mitigations/projections.
164
182
  - [ ] Support configuring [Dex connectors](https://dexidp.io/docs/connectors/).
165
183
  - [ ] Support configuring Dex on a distinct subdomain, such as `auth.localhost`.
166
184
  - [ ] Support optional [HTTP mode without local TLS](docs/tls-http-mode.md).
package/dist/certs.d.ts CHANGED
@@ -1,7 +1,3 @@
1
- type Options = {
2
- certs: string;
3
- hostname: string;
4
- };
5
- export declare function ensureCerts({ certs, hostname }: Options): Promise<void>;
6
- export {};
1
+ import type { VisageConfig } from './config';
2
+ export declare function ensureCerts(config: VisageConfig): Promise<void>;
7
3
  //# sourceMappingURL=certs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"certs.d.ts","sourceRoot":"","sources":["../src/certs.ts"],"names":[],"mappings":"AAcA,KAAK,OAAO,GAAG;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAIF,wBAAsB,WAAW,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA0C7E"}
1
+ {"version":3,"file":"certs.d.ts","sourceRoot":"","sources":["../src/certs.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAI7C,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCrE"}
package/dist/compose.d.ts CHANGED
@@ -1,4 +1,5 @@
1
+ import type { VisageConfig } from './config';
1
2
  type StopCompose = () => void;
2
- export declare function startCompose(file: string): StopCompose;
3
+ export declare function startCompose(config: VisageConfig): StopCompose;
3
4
  export {};
4
5
  //# sourceMappingURL=compose.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAIA,KAAK,WAAW,GAAG,MAAM,IAAI,CAAC;AAI9B,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAwCtD"}
1
+ {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,KAAK,WAAW,GAAG,MAAM,IAAI,CAAC;AAI9B,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW,CAmC9D"}
package/dist/config.d.ts CHANGED
@@ -27,21 +27,33 @@ type ResolvedVisageOptions = {
27
27
  readonly services: Readonly<Record<string, VisageService>>;
28
28
  readonly upstreams: Record<string, VisageUpstream>;
29
29
  };
30
- type ResolvedBaseIdpConfig = {
31
- readonly upstream: string;
30
+ type OIDCEndpointConfig = {
32
31
  readonly issuer: string;
32
+ readonly authorization?: string;
33
+ readonly token?: string;
34
+ readonly jwks?: string;
35
+ readonly end_session_endpoint?: string;
36
+ };
37
+ type ManualOIDCEndpointConfig = OIDCEndpointConfig & {
33
38
  readonly authorization: string;
34
39
  readonly token: string;
35
40
  readonly jwks: string;
36
41
  };
37
- type ResolvedDexIdpConfig = ResolvedBaseIdpConfig & {
42
+ type ResolvedDexIdpConfig = {
38
43
  readonly dex: {
39
44
  readonly expiry?: VisageDexExpiry;
40
45
  readonly users: readonly VisageDexUser[];
41
46
  };
47
+ readonly oidc: ManualOIDCEndpointConfig;
48
+ readonly upstream: {
49
+ readonly dex: ResolvedUpstream;
50
+ };
42
51
  };
43
- type ResolvedExternalIdpConfig = ResolvedBaseIdpConfig & {
44
- readonly dex?: never;
52
+ type ResolvedExternalIdpConfig = {
53
+ readonly oidc: OIDCEndpointConfig;
54
+ readonly upstream: {
55
+ readonly idp: ResolvedUpstream;
56
+ };
45
57
  };
46
58
  type ResolvedIdpConfig = ResolvedDexIdpConfig | ResolvedExternalIdpConfig;
47
59
  type ResolvedService = Omit<VisageService, 'upstream'> & {
@@ -53,8 +65,13 @@ type ResolvedUpstream = {
53
65
  readonly port: number;
54
66
  readonly locations: Readonly<Record<string, VisageProxyPolicy>>;
55
67
  };
68
+ type ResolvedAuthPolicy = {
69
+ readonly enabled: boolean;
70
+ readonly forward: false | 'id' | 'access';
71
+ readonly redirect: boolean;
72
+ };
56
73
  type ResolvedProxyPolicy = {
57
- readonly auth: Required<VisageProxyPolicy['auth']>;
74
+ readonly auth: ResolvedAuthPolicy;
58
75
  readonly headers: Readonly<Record<string, string>>;
59
76
  readonly directives: Readonly<Record<string, readonly string[]>>;
60
77
  };
@@ -78,6 +95,10 @@ export type VisageConfig = {
78
95
  readonly clientSecret: Volume;
79
96
  readonly cookieSecret: Volume;
80
97
  };
98
+ readonly network: {
99
+ readonly name: string;
100
+ readonly trustedProxyIps: readonly string[];
101
+ };
81
102
  readonly services: Readonly<Record<string, ResolvedService>>;
82
103
  readonly upstreams: Readonly<Record<string, ResolvedConfigUpstream>>;
83
104
  };
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/hosts.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export declare function ensureHostEntry(hostname: string): void;
1
+ import type { VisageConfig } from './config';
2
+ export declare function ensureHostEntry({ host }: VisageConfig): void;
2
3
  //# sourceMappingURL=hosts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hosts.d.ts","sourceRoot":"","sources":["../src/hosts.ts"],"names":[],"mappings":"AAKA,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAyDtD"}
1
+ {"version":3,"file":"hosts.d.ts","sourceRoot":"","sources":["../src/hosts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAI7C,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,EAAE,YAAY,GAAG,IAAI,CAyD5D"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,4 @@
1
- import type { Plugin } from 'vite';
2
- import type { VisageOptions, VisageServer } from './types';
3
1
  export type { VisageCookiePolicy, VisageDexExpiry, VisageDexOptions, VisageDexUser, VisageExternalIdpOptions, VisageOAuth2Client, VisageOptions, VisageProxyPolicy, VisageServer, VisageService, VisageUpstream, } from './types';
4
- export declare function createVisageServer(options: VisageOptions): VisageServer;
5
- export default function visage(options?: VisageOptions): Plugin;
2
+ export { default, visage } from './plugin';
3
+ export { createVisageServer } from './server';
6
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAOnC,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAkB,MAAM,SAAS,CAAC;AAE3E,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,wBAAwB,EACxB,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,cAAc,GACf,MAAM,SAAS,CAAC;AAEjB,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAgCvE;AAED,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,MAAM,CA4ElE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,wBAAwB,EACxB,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,cAAc,GACf,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -1,111 +1,14 @@
1
- import { mkdirSync, chmodSync, openSync, rmSync, existsSync, createWriteStream, readFileSync, appendFileSync, writeFileSync } from 'node:fs';
2
- import { join, dirname } from 'node:path';
1
+ import { join } from 'node:path';
2
+ import { readFileSync, mkdirSync, chmodSync, openSync, rmSync, existsSync, createWriteStream, appendFileSync, writeFileSync } from 'node:fs';
3
+ import { parse, stringify } from 'yaml';
3
4
  import { spawnSync, spawn } from 'node:child_process';
4
5
  import { homedir } from 'node:os';
5
6
  import { Readable } from 'node:stream';
6
7
  import { pipeline } from 'node:stream/promises';
7
- import { stringify } from 'yaml';
8
8
  import { hashSync } from 'bcryptjs';
9
9
  import { Eta } from 'eta';
10
10
  import { randomBytes } from 'node:crypto';
11
11
 
12
- const CACHE_HOME = process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
13
- async function ensureCerts({ certs, hostname }) {
14
- const CAROOT = join(CACHE_HOME, 'visage/ca');
15
- mkdirSync(CAROOT, { recursive: true, mode: 0o700 });
16
- chmodSync(CAROOT, 0o700);
17
- const mkcert = await ensureMkCert();
18
- const logs = join(dirname(certs), 'logs');
19
- mkdirSync(logs, { recursive: true });
20
- const log = join(logs, 'mkcert.log');
21
- const output = openSync(log, 'w');
22
- const env = { CAROOT, TRUST_STORES: 'system', ...process.env };
23
- const tty = process.stdin.isTTY;
24
- const stdio = [
25
- tty ? 'inherit' : 'ignore',
26
- output,
27
- output,
28
- ];
29
- if (process.env.CI !== 'true') {
30
- // mkcert -install is idempotent; CA files alone do not prove trust-store state.
31
- const result = spawnSync(mkcert, ['-install'], { env, stdio });
32
- if (result.error)
33
- throw result.error;
34
- if (result.status !== 0) {
35
- throw new Error('Failed to install CA');
36
- }
37
- }
38
- const cert = join(certs, 'tls.crt');
39
- const key = join(certs, 'tls.key');
40
- mkdirSync(certs, { recursive: true });
41
- rmSync(cert, { force: true });
42
- rmSync(key, { force: true });
43
- const names = [...new Set([hostname, 'localhost', '127.0.0.1', '::1'])];
44
- const args = ['-cert-file', cert, '-key-file', key, ...names];
45
- const result = spawnSync(mkcert, args, { env, stdio });
46
- if (result.error)
47
- throw result.error;
48
- if (result.status !== 0) {
49
- throw new Error('Failed to generate TLS certificates');
50
- }
51
- }
52
- async function ensureMkCert() {
53
- const bin = join(CACHE_HOME, 'visage/bin');
54
- const file = join(bin, `mkcert-${process.platform}-${process.arch}`);
55
- if (existsSync(file))
56
- return file;
57
- mkdirSync(bin, { recursive: true });
58
- const base = 'https://dl.filippo.io/mkcert/latest';
59
- const arch = process.arch === 'x64' ? 'amd64' : process.arch;
60
- const params = `?for=${process.platform}/${arch}`;
61
- const url = new URL(params, base);
62
- const response = await fetch(url);
63
- if (!response.ok || !response.body) {
64
- throw new Error('Failed to download mkcert');
65
- }
66
- await pipeline(Readable.fromWeb(response.body), createWriteStream(file));
67
- chmodSync(file, 0o755);
68
- return file;
69
- }
70
-
71
- let stopRef;
72
- function startCompose(file) {
73
- stopRef?.();
74
- stopRef = undefined;
75
- const logs = join(dirname(file), 'logs');
76
- mkdirSync(logs, { recursive: true });
77
- const output = openSync(join(logs, 'compose.log'), 'w');
78
- const compose = [
79
- 'compose',
80
- '--ansi=never',
81
- `--file=${file}`,
82
- `--project-name=${process.env.COMPOSE_PROJECT_NAME ?? 'visage'}`,
83
- ];
84
- const env = { ...process.env, COMPOSE_MENU: 'false' };
85
- const opts = {
86
- cwd: dirname(file),
87
- stdio: ['ignore', output, output],
88
- env,
89
- };
90
- const up = [
91
- ...compose,
92
- 'up',
93
- '--abort-on-container-failure',
94
- '--remove-orphans',
95
- ];
96
- const child = spawn('docker', up, opts);
97
- const stop = () => {
98
- if (stopRef !== stop)
99
- return;
100
- stopRef = undefined;
101
- child.kill();
102
- const down = [...compose, 'down', '--remove-orphans'];
103
- spawnSync('docker', down, opts);
104
- };
105
- stopRef = stop;
106
- return stop;
107
- }
108
-
109
12
  const BaseFiles = {
110
13
  certs: ['./certs', '/etc/nginx/certs'],
111
14
  compose: './compose.yaml',
@@ -115,29 +18,24 @@ const BaseFiles = {
115
18
  clientSecret: ['./oauth2-client-secret', '/etc/oauth2-proxy/client-secret'],
116
19
  cookieSecret: ['./oauth2-cookie-secret', '/etc/oauth2-proxy/cookie-secret'],
117
20
  };
21
+ const DockerImages = parse(readFileSync(new URL('../docker-compose.images.yml', import.meta.url), 'utf8')).services;
118
22
  const BaseServiceDex = {
119
- image: 'ghcr.io/dexidp/dex:v2.45.1',
23
+ image: DockerImages.dex.image,
120
24
  command: ['dex', 'serve', '/etc/dex/dex.yml'],
121
25
  restart: 'always',
122
26
  };
123
27
  const BaseServiceNginx = {
124
- image: 'nginx:1.30.0-alpine',
28
+ image: DockerImages.nginx.image,
125
29
  depends_on: ['oauth2_proxy'],
126
30
  extra_hosts: ['host.docker.internal:host-gateway'],
127
31
  restart: 'always',
128
32
  };
129
33
  const BaseServiceOAuth2Proxy = {
130
- image: 'quay.io/oauth2-proxy/oauth2-proxy:v7.15.2',
34
+ image: DockerImages.oauth2_proxy.image,
131
35
  command: ['--config', '/etc/oauth2-proxy/config.yml'],
132
36
  extra_hosts: ['host.docker.internal:host-gateway'],
133
37
  restart: 'always',
134
38
  };
135
- const BaseUpstreamDex = {
136
- host: 'dex',
137
- scheme: 'http',
138
- port: 5556,
139
- locations: { '/dex/': { auth: { enabled: false } } },
140
- };
141
39
  const BaseUpstreamOauth2Proxy = {
142
40
  host: 'oauth2_proxy',
143
41
  scheme: 'http',
@@ -159,10 +57,7 @@ const DefaultCookiePolicy = {
159
57
  cookie_secret_file: BaseFiles.cookieSecret[1],
160
58
  };
161
59
  const DefaultDexUsers = [
162
- {
163
- email: 'user@example.com',
164
- password: 'pass',
165
- },
60
+ { email: 'user@example.com', password: 'pass' },
166
61
  ];
167
62
  const DefaultOAuth2Client = {
168
63
  id: 'visage',
@@ -170,7 +65,6 @@ const DefaultOAuth2Client = {
170
65
  scopes: ['openid', 'email', 'profile', 'offline_access'],
171
66
  emailDomains: ['example.com']};
172
67
  const DefaultProxyPolicy = {
173
- auth: { enabled: true, forward: 'id', redirect: false },
174
68
  headers: {
175
69
  Cookie: '""', // Don't forward session cookie.
176
70
  Host: '$host',
@@ -183,7 +77,7 @@ const DefaultProxyPolicy = {
183
77
  },
184
78
  };
185
79
  function resolveOptions(options) {
186
- const { host = 'localhost', port = 9001, cookie = {}, oauth2 = {} } = options;
80
+ const { host = 'localhost', port = 9001, cookie = {}, idp = {}, oauth2 = {}, } = options;
187
81
  const cookieName = cookie.name ?? 'sess';
188
82
  const publicClient = oauth2.clientSecret === null;
189
83
  const services = resolveServicesOptions(options.services);
@@ -207,7 +101,19 @@ function resolveOptions(options) {
207
101
  : { cookie_domains: cookie.domains }),
208
102
  ...(cookie.path === undefined ? {} : { cookie_path: cookie.path }),
209
103
  },
210
- idp: resolveIdpOption(options.idp),
104
+ idp: 'issuer' in idp
105
+ ? idp
106
+ : {
107
+ dex: {
108
+ ...(idp.expiry ? { expiry: idp.expiry } : {}),
109
+ users: (idp.users ?? DefaultDexUsers).map((user) => ({
110
+ email: user.email,
111
+ password: user.password,
112
+ username: user.username ?? user.email.split('@', 1)[0],
113
+ userID: user.userID ?? user.email,
114
+ })),
115
+ },
116
+ },
211
117
  oauth2: {
212
118
  id: oauth2.clientId ?? DefaultOAuth2Client.id,
213
119
  ...(publicClient
@@ -274,78 +180,28 @@ function resolveUpstreamsOptions(services, upstreams = {}) {
274
180
  ])),
275
181
  };
276
182
  }
277
- function resolveIdpOption(idp) {
278
- if (idp && 'issuer' in idp) {
279
- return {
280
- issuer: idp.issuer,
281
- authorization: idp.authorization ?? '/auth',
282
- token: idp.token ?? '/token',
283
- jwks: idp.jwks ?? '/keys',
284
- };
285
- }
286
- return {
287
- dex: {
288
- ...(idp?.expiry ? { expiry: idp.expiry } : {}),
289
- users: (idp?.users ?? DefaultDexUsers).map((user) => ({
290
- email: user.email,
291
- password: user.password,
292
- username: user.username ?? user.email.split('@', 1)[0],
293
- userID: user.userID ?? user.email,
294
- })),
295
- },
296
- };
297
- }
298
- function resolveDirectives(directives = {}) {
299
- return Object.fromEntries(Object.entries(directives).map(([name, value]) => [
300
- name,
301
- Array.isArray(value) ? value : [value],
302
- ]));
303
- }
304
- function resolveIdpConfig({ host, port, idp, }) {
305
- if ('dex' in idp) {
306
- const issuer = `https://${host}:${port}/dex`;
307
- const upstream = `http://dex:5556/dex`;
308
- return {
309
- upstream: 'dex',
310
- issuer,
311
- authorization: `${issuer}/auth`,
312
- token: `${upstream}/token`,
313
- jwks: `${upstream}/keys`,
314
- dex: {
315
- expiry: idp.dex.expiry,
316
- users: (idp.dex?.users ?? DefaultDexUsers).map((user) => ({
317
- email: user.email,
318
- password: user.password,
319
- username: user.username ?? user.email.split('@', 1)[0],
320
- userID: user.userID ?? user.email,
321
- })),
322
- },
323
- };
324
- }
325
- return {
326
- upstream: 'idp',
327
- issuer: idp.issuer,
328
- authorization: idp.issuer + (idp.authorization ?? '/auth'),
329
- token: idp.issuer + (idp.token ?? '/token'),
330
- jwks: idp.issuer + (idp.jwks ?? '/keys'),
331
- };
332
- }
333
- function resolveExternalIdpUpstream(idp) {
334
- const issuer = new URL(idp.issuer);
335
- return {
336
- host: issuer.hostname,
337
- locations: {},
338
- scheme: issuer.protocol === 'https:' ? 'https' : 'http',
339
- port: Number(issuer.port) || (issuer.protocol === 'https:' ? 443 : 80),
340
- };
341
- }
342
183
  function resolveConfig(options, cache) {
343
184
  const idp = resolveIdpConfig(options);
344
185
  const upstreams = {
345
- oauth2_proxy: BaseUpstreamOauth2Proxy,
346
- ...(idp.dex === undefined
347
- ? { idp: resolveExternalIdpUpstream(idp) }
348
- : { dex: BaseUpstreamDex }),
186
+ oauth2_proxy: {
187
+ ...BaseUpstreamOauth2Proxy,
188
+ locations: {
189
+ ...BaseUpstreamOauth2Proxy.locations,
190
+ '/oauth2/sign_out': {
191
+ auth: { enabled: false },
192
+ headers: {
193
+ Cookie: '$http_cookie', // Forward session cookie.
194
+ 'X-Auth-Request-Redirect': idp.oidc.end_session_endpoint
195
+ ? JSON.stringify(idp.oidc.end_session_endpoint +
196
+ (idp.oidc.end_session_endpoint.includes('?') ? '&' : '?') +
197
+ 'id_token_hint={id_token}&post_logout_redirect_uri=' +
198
+ encodeURIComponent(`https://${options.host}:${options.port}/`))
199
+ : '/',
200
+ },
201
+ },
202
+ },
203
+ },
204
+ ...idp.upstream,
349
205
  ...options.upstreams,
350
206
  };
351
207
  return {
@@ -355,15 +211,19 @@ function resolveConfig(options, cache) {
355
211
  idp,
356
212
  oauth2: options.oauth2,
357
213
  cache,
358
- files: { ...BaseFiles },
214
+ files: BaseFiles,
215
+ network: {
216
+ name: `${process.env.COMPOSE_PROJECT_NAME ?? 'visage'}_nginx`,
217
+ trustedProxyIps: [],
218
+ },
359
219
  services: {
360
- ...(idp.dex === undefined
361
- ? { nginx: BaseServiceNginx, oauth2_proxy: BaseServiceOAuth2Proxy }
362
- : {
220
+ ...('dex' in idp
221
+ ? {
363
222
  dex: BaseServiceDex,
364
223
  nginx: { ...BaseServiceNginx, depends_on: ['dex', 'oauth2_proxy'] },
365
224
  oauth2_proxy: { ...BaseServiceOAuth2Proxy, depends_on: ['dex'] },
366
- }),
225
+ }
226
+ : { nginx: BaseServiceNginx, oauth2_proxy: BaseServiceOAuth2Proxy }),
367
227
  ...Object.fromEntries(Object.entries(options.services).map(([name, { upstream: _upstream, ...service }]) => [
368
228
  name,
369
229
  { restart: 'on-failure', ...service },
@@ -380,7 +240,7 @@ function resolveConfig(options, cache) {
380
240
  locations: Object.fromEntries(Object.entries(upstream.locations ?? {}).map(([path, policy]) => [
381
241
  path,
382
242
  {
383
- auth: { ...DefaultProxyPolicy.auth, ...policy.auth },
243
+ auth: resolveAuthPolicy(policy.auth, external && name !== 'vite'),
384
244
  headers: {
385
245
  ...(external
386
246
  ? { ...DefaultProxyPolicy.headers, Host: upstream.host }
@@ -389,7 +249,10 @@ function resolveConfig(options, cache) {
389
249
  },
390
250
  directives: {
391
251
  ...DefaultProxyPolicy.directives,
392
- ...resolveDirectives(policy.directives),
252
+ ...Object.fromEntries(Object.entries(policy.directives ?? {}).map(([name, value]) => [
253
+ name,
254
+ Array.isArray(value) ? value : [value],
255
+ ])),
393
256
  },
394
257
  },
395
258
  ])),
@@ -398,12 +261,77 @@ function resolveConfig(options, cache) {
398
261
  })),
399
262
  };
400
263
  }
264
+ function resolveIdpConfig({ host, port, idp, }) {
265
+ if ('dex' in idp) {
266
+ return {
267
+ dex: {
268
+ expiry: idp.dex.expiry,
269
+ users: (idp.dex?.users ?? DefaultDexUsers).map((user) => ({
270
+ email: user.email,
271
+ password: user.password,
272
+ username: user.username ?? user.email.split('@', 1)[0],
273
+ userID: user.userID ?? user.email,
274
+ })),
275
+ },
276
+ oidc: {
277
+ issuer: `https://${host}:${port}/dex`,
278
+ authorization: `https://${host}:${port}/dex/auth`,
279
+ token: 'http://dex:5556/dex/token',
280
+ jwks: 'http://dex:5556/dex/keys',
281
+ },
282
+ upstream: {
283
+ dex: {
284
+ host: 'dex',
285
+ scheme: 'http',
286
+ port: 5556,
287
+ locations: { '/dex/': { auth: { enabled: false } } },
288
+ },
289
+ },
290
+ };
291
+ }
292
+ const issuer = new URL(idp.issuer);
293
+ const oidc = {
294
+ issuer: idp.issuer,
295
+ ...(idp.end_session_endpoint === undefined
296
+ ? {}
297
+ : { end_session_endpoint: idp.end_session_endpoint }),
298
+ };
299
+ return {
300
+ oidc: !idp.authorization && !idp.token && !idp.jwks
301
+ ? oidc
302
+ : {
303
+ ...oidc,
304
+ authorization: idp.issuer + (idp.authorization ?? '/auth'),
305
+ token: idp.issuer + (idp.token ?? '/token'),
306
+ jwks: idp.issuer + (idp.jwks ?? '/keys'),
307
+ },
308
+ upstream: {
309
+ idp: {
310
+ scheme: issuer.protocol === 'https:' ? 'https' : 'http',
311
+ host: issuer.hostname,
312
+ port: Number(issuer.port) || (issuer.protocol === 'https:' ? 443 : 80),
313
+ locations: {},
314
+ },
315
+ },
316
+ };
317
+ }
318
+ function resolveAuthPolicy(auth = {}, external) {
319
+ return {
320
+ enabled: auth.enabled ?? true,
321
+ forward: auth.forward === true
322
+ ? external
323
+ ? 'access'
324
+ : 'id'
325
+ : (auth.forward ?? false),
326
+ redirect: auth.redirect ?? false,
327
+ };
328
+ }
401
329
  const BaseViteUpstream = {
402
330
  host: 'host.docker.internal',
403
331
  scheme: 'http',
404
332
  locations: {
405
333
  '/': {
406
- auth: { forward: undefined, redirect: true },
334
+ auth: { redirect: true },
407
335
  headers: {
408
336
  Host: '$host',
409
337
  Upgrade: '$http_upgrade',
@@ -441,12 +369,99 @@ function resolveViteUpstream(vite = { locations: {} }) {
441
369
  };
442
370
  }
443
371
 
372
+ const CACHE_HOME = process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
373
+ async function ensureCerts(config) {
374
+ const CAROOT = join(CACHE_HOME, 'visage/ca');
375
+ mkdirSync(CAROOT, { recursive: true, mode: 0o700 });
376
+ chmodSync(CAROOT, 0o700);
377
+ const mkcert = await ensureMkCert();
378
+ mkdirSync(join(config.cache, 'logs'), { recursive: true });
379
+ const out = openSync(join(config.cache, 'logs', 'mkcert.log'), 'w');
380
+ const env = { CAROOT, TRUST_STORES: 'system', ...process.env };
381
+ const tty = process.stdin.isTTY;
382
+ const stdio = [tty ? 'inherit' : 'ignore', out, out];
383
+ if (process.env.CI !== 'true') {
384
+ // mkcert -install is idempotent; CA files alone do not prove trust-store state.
385
+ const result = spawnSync(mkcert, ['-install'], { env, stdio });
386
+ if (result.error)
387
+ throw result.error;
388
+ if (result.status !== 0) {
389
+ throw new Error('Failed to install CA');
390
+ }
391
+ }
392
+ const certs = join(config.cache, config.files.certs[0]);
393
+ const cert = join(certs, 'tls.crt');
394
+ const key = join(certs, 'tls.key');
395
+ mkdirSync(certs, { recursive: true });
396
+ rmSync(cert, { force: true });
397
+ rmSync(key, { force: true });
398
+ const names = [...new Set([config.host, 'localhost', '127.0.0.1', '::1'])];
399
+ const args = ['-cert-file', cert, '-key-file', key, ...names];
400
+ const result = spawnSync(mkcert, args, { env, stdio });
401
+ if (result.error)
402
+ throw result.error;
403
+ if (result.status !== 0) {
404
+ throw new Error('Failed to generate TLS certificates');
405
+ }
406
+ }
407
+ async function ensureMkCert() {
408
+ const bin = join(CACHE_HOME, 'visage/bin');
409
+ const file = join(bin, `mkcert-${process.platform}-${process.arch}`);
410
+ if (existsSync(file))
411
+ return file;
412
+ mkdirSync(bin, { recursive: true });
413
+ const base = 'https://dl.filippo.io/mkcert/latest';
414
+ const arch = process.arch === 'x64' ? 'amd64' : process.arch;
415
+ const params = `?for=${process.platform}/${arch}`;
416
+ const url = new URL(params, base);
417
+ const response = await fetch(url);
418
+ if (!response.ok || !response.body) {
419
+ throw new Error('Failed to download mkcert');
420
+ }
421
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(file));
422
+ chmodSync(file, 0o755);
423
+ return file;
424
+ }
425
+
426
+ let stopRef;
427
+ function startCompose(config) {
428
+ stopRef?.();
429
+ stopRef = undefined;
430
+ const file = join(config.cache, config.files.compose);
431
+ const logs = join(config.cache, 'logs');
432
+ const output = openSync(join(logs, 'compose.log'), 'w');
433
+ const compose = [
434
+ 'compose',
435
+ '--ansi=never',
436
+ `--file=${file}`,
437
+ `--project-name=${process.env.COMPOSE_PROJECT_NAME ?? 'visage'}`,
438
+ ];
439
+ const env = { ...process.env, COMPOSE_MENU: 'false' };
440
+ const opts = {
441
+ cwd: config.cache,
442
+ stdio: ['ignore', output, output],
443
+ env,
444
+ };
445
+ const up = [...compose, 'up', '--remove-orphans'];
446
+ const child = spawn('docker', up, opts);
447
+ const stop = () => {
448
+ if (stopRef !== stop)
449
+ return;
450
+ stopRef = undefined;
451
+ child.kill();
452
+ const down = [...compose, 'down', '--remove-orphans'];
453
+ spawnSync('docker', down, opts);
454
+ };
455
+ stopRef = stop;
456
+ return stop;
457
+ }
458
+
444
459
  const HOSTS_FILE = '/etc/hosts';
445
- function ensureHostEntry(hostname) {
446
- if (!hostname ||
447
- hostname.trim() !== hostname ||
448
- hostname.includes('/') ||
449
- hostname.includes(':')) {
460
+ function ensureHostEntry({ host }) {
461
+ if (!host ||
462
+ host.trim() !== host ||
463
+ host.includes('/') ||
464
+ host.includes(':')) {
450
465
  throw new Error('Invalid hostname');
451
466
  }
452
467
  const contents = readFileSync(HOSTS_FILE, 'utf8');
@@ -456,7 +471,7 @@ function ensureHostEntry(hostname) {
456
471
  continue;
457
472
  }
458
473
  const [address, ...names] = uncommented.split(/\s+/);
459
- if (!names.includes(hostname)) {
474
+ if (!names.includes(host)) {
460
475
  continue;
461
476
  }
462
477
  if (address === '127.0.0.1' || address === '::1') {
@@ -466,7 +481,7 @@ function ensureHostEntry(hostname) {
466
481
  throw new Error('Hosts file contains a conflicting entry');
467
482
  }
468
483
  const prefix = contents.endsWith('\n') ? '' : '\n';
469
- const entry = `${prefix}127.0.0.1\t${hostname} # visage\n`;
484
+ const entry = `${prefix}127.0.0.1\t${host} # visage\n`;
470
485
  try {
471
486
  appendFileSync(HOSTS_FILE, entry);
472
487
  return;
@@ -486,6 +501,65 @@ function ensureHostEntry(hostname) {
486
501
  }
487
502
  }
488
503
 
504
+ function ensureNginxNetwork(config) {
505
+ const exists = spawnSync('docker', [
506
+ 'network',
507
+ 'ls',
508
+ '--filter',
509
+ `name=${config.network.name}`,
510
+ '--format',
511
+ '{{ .Name }}',
512
+ ], { encoding: 'utf-8' });
513
+ if (exists.error)
514
+ throw exists.error;
515
+ if (exists.status !== 0) {
516
+ console.error(exists.stderr);
517
+ throw new Error('Failed to list Docker network');
518
+ }
519
+ if (exists.stdout) {
520
+ return {
521
+ ...config,
522
+ network: {
523
+ ...config.network,
524
+ trustedProxyIps: inspectNetwork(config.network.name),
525
+ },
526
+ };
527
+ }
528
+ const create = spawnSync('docker', ['network', 'create', '--driver', 'bridge', config.network.name], { encoding: 'utf-8' });
529
+ if (create.error)
530
+ throw create.error;
531
+ if (create.status !== 0) {
532
+ console.error(create.stderr);
533
+ throw new Error('Failed to create Docker network');
534
+ }
535
+ return {
536
+ ...config,
537
+ network: {
538
+ ...config.network,
539
+ trustedProxyIps: inspectNetwork(config.network.name),
540
+ },
541
+ };
542
+ }
543
+ function inspectNetwork(name) {
544
+ const result = spawnSync('docker', [
545
+ 'network',
546
+ 'inspect',
547
+ '--format',
548
+ '{{range .IPAM.Config}}{{println .Subnet}}{{end}}',
549
+ name,
550
+ ], { encoding: 'utf-8' });
551
+ if (result.error)
552
+ throw result.error;
553
+ if (result.status !== 0) {
554
+ console.error(result.stderr);
555
+ throw new Error('Failed to inspect Docker network');
556
+ }
557
+ return result.stdout
558
+ .split(/\r?\n/)
559
+ .map((line) => line.trim())
560
+ .filter(Boolean);
561
+ }
562
+
489
563
  function writeComposeConfig(config) {
490
564
  const file = join(config.cache, config.files.compose);
491
565
  const render = renderComposeConfig(config);
@@ -494,8 +568,9 @@ function writeComposeConfig(config) {
494
568
  function renderComposeConfig(config) {
495
569
  const { dex, nginx, oauth2_proxy, ...services } = config.services;
496
570
  return stringify({
571
+ networks: { default: { external: true, name: config.network.name } },
497
572
  services: {
498
- ...(config.idp.dex !== undefined
573
+ ...('dex' in config.idp
499
574
  ? {
500
575
  dex: {
501
576
  ...config.services.dex,
@@ -526,38 +601,34 @@ function renderComposeConfig(config) {
526
601
  }
527
602
 
528
603
  function writeDexConfig(config) {
529
- const file = join(config.cache, config.files.dex[0]);
530
604
  const render = renderDexConfig(config);
605
+ const file = join(config.cache, config.files.dex[0]);
531
606
  writeFileSync(file, render, 'utf-8');
532
607
  }
533
608
  function renderDexConfig(config) {
534
- const { idp } = config;
535
- if (idp.dex === undefined) {
536
- throw new Error('Dex config is required to render Dex');
537
- }
538
- const origin = `https://${config.host}:${config.port}`;
539
- const redirect = `${origin}/oauth2/callback`;
540
- const upstream = config.upstreams[idp.upstream];
609
+ if (!('dex' in config.idp))
610
+ throw new Error('Dex config missing');
611
+ const { host, port, oauth2, idp: { dex: { expiry, users }, oidc, upstream, }, } = config;
541
612
  return stringify({
542
- issuer: idp.issuer,
613
+ issuer: oidc.issuer,
543
614
  storage: { type: 'memory' },
544
- web: { http: `0.0.0.0:${upstream.port}` },
615
+ web: { http: `0.0.0.0:${upstream.dex.port}` },
545
616
  oauth2: { skipApprovalScreen: true },
546
617
  staticClients: [
547
618
  {
548
- id: config.oauth2.id,
619
+ id: oauth2.id,
549
620
  name: 'Visage',
550
- ...(config.oauth2.secret === undefined
621
+ ...(oauth2.secret === undefined
551
622
  ? { public: true }
552
- : { secret: config.oauth2.secret }),
553
- redirectURIs: [redirect],
623
+ : { secret: oauth2.secret }),
624
+ redirectURIs: [`https://${host}:${port}/oauth2/callback`],
554
625
  },
555
626
  ],
556
627
  enablePasswordDB: true,
557
- ...(idp.dex.expiry === undefined ? {} : { expiry: idp.dex.expiry }),
558
- staticPasswords: idp.dex.users.map(({ password, ...user }) => ({
628
+ ...(expiry === undefined ? {} : { expiry }),
629
+ staticPasswords: users.map(({ password, ...user }) => ({
559
630
  ...user,
560
- hash: hashSync(password, 10),
631
+ hash: hashSync(password),
561
632
  })),
562
633
  });
563
634
  }
@@ -703,11 +774,15 @@ function renderOauth2ProxyConfig(config) {
703
774
  const data = {
704
775
  http_address: `0.0.0.0:${config.upstreams.oauth2_proxy.port}`,
705
776
  provider: 'oidc',
706
- oidc_issuer_url: config.idp.issuer,
707
- skip_oidc_discovery: true,
708
- login_url: config.idp.authorization,
709
- redeem_url: config.idp.token,
710
- oidc_jwks_url: config.idp.jwks,
777
+ oidc_issuer_url: config.idp.oidc.issuer,
778
+ ...('authorization' in config.idp.oidc
779
+ ? {
780
+ skip_oidc_discovery: true,
781
+ login_url: config.idp.oidc.authorization,
782
+ redeem_url: config.idp.oidc.token,
783
+ oidc_jwks_url: config.idp.oidc.jwks,
784
+ }
785
+ : {}),
711
786
  redirect_url: `https://${config.host}:${config.port}/oauth2/callback`,
712
787
  client_id: config.oauth2.id,
713
788
  ...(config.oauth2.secret === undefined
@@ -723,9 +798,16 @@ function renderOauth2ProxyConfig(config) {
723
798
  cookie_csrf_per_request: true,
724
799
  cookie_csrf_per_request_limit: 16,
725
800
  email_domains: config.oauth2.emailDomains,
726
- whitelist_domains: [config.host, `${config.host}:${config.port}`],
801
+ whitelist_domains: [
802
+ config.host,
803
+ `${config.host}:${config.port}`,
804
+ ...(!('dex' in config.idp) && config.idp.oidc.end_session_endpoint
805
+ ? [new URL(config.idp.oidc.end_session_endpoint).host]
806
+ : []),
807
+ ],
727
808
  scope: config.oauth2.scopes.join(' '),
728
809
  reverse_proxy: true,
810
+ trusted_proxy_ips: config.network.trustedProxyIps,
729
811
  set_xauthrequest: true,
730
812
  set_authorization_header: true,
731
813
  pass_access_token: true,
@@ -746,36 +828,19 @@ function renderOauth2ProxyConfig(config) {
746
828
  .join('\n')}\n`;
747
829
  }
748
830
 
749
- function render(config) {
750
- writeComposeConfig(config);
751
- if (config.idp.dex !== undefined) {
752
- writeDexConfig(config);
753
- }
754
- writeNginxConfig(config);
755
- writeOauth2ProxyConfig(config);
756
- }
757
-
758
831
  function createVisageServer(options) {
832
+ const cache = join(process.cwd(), '.visage');
759
833
  const config = resolveConfig(resolveOptions({
760
834
  ...options,
761
835
  upstreams: {
762
836
  ...options.upstreams,
763
837
  vite: resolveViteUpstream(options.upstreams?.vite),
764
838
  },
765
- }), join(process.cwd(), '.visage'));
839
+ }), cache);
766
840
  let stop;
767
841
  return {
768
842
  async listen() {
769
- if (stop)
770
- return;
771
- rmSync(join(config.cache, 'logs'), { recursive: true, force: true });
772
- await ensureCerts({
773
- certs: join(config.cache, config.files.certs[0]),
774
- hostname: config.host,
775
- });
776
- ensureHostEntry(config.host);
777
- render(config);
778
- stop = await startCompose(join(config.cache, config.files.compose));
843
+ stop ??= await startVisageServer(config);
779
844
  },
780
845
  close() {
781
846
  stop?.();
@@ -783,6 +848,22 @@ function createVisageServer(options) {
783
848
  },
784
849
  };
785
850
  }
851
+ async function startVisageServer(config) {
852
+ const logs = join(config.cache, 'logs');
853
+ rmSync(logs, { recursive: true, force: true });
854
+ mkdirSync(logs, { recursive: true });
855
+ await ensureCerts(config);
856
+ ensureHostEntry(config);
857
+ const renderConfig = ensureNginxNetwork(config);
858
+ writeComposeConfig(renderConfig);
859
+ if ('dex' in renderConfig.idp) {
860
+ writeDexConfig(renderConfig);
861
+ }
862
+ writeNginxConfig(renderConfig);
863
+ writeOauth2ProxyConfig(renderConfig);
864
+ return startCompose(renderConfig);
865
+ }
866
+
786
867
  function visage(options = {}) {
787
868
  const resolvedOptions = resolveOptions(options);
788
869
  let stop;
@@ -813,21 +894,6 @@ function visage(options = {}) {
813
894
  printUrls();
814
895
  viteDevServer.config.logger.info(visageUrl ?? 'Visage failed to start');
815
896
  };
816
- async function startVisage(vite) {
817
- const config = resolveConfig(resolveOptions({
818
- ...options,
819
- upstreams: { ...options.upstreams, vite },
820
- }), join(viteDevServer.config.cacheDir, 'visage'));
821
- visageUrl = formatVisageUrlLog(config.host, config.port);
822
- rmSync(join(config.cache, 'logs'), { recursive: true, force: true });
823
- await ensureCerts({
824
- certs: join(config.cache, config.files.certs[0]),
825
- hostname: config.host,
826
- });
827
- ensureHostEntry(config.host);
828
- render(config);
829
- return startCompose(join(config.cache, config.files.compose));
830
- }
831
897
  // monkey patch vite's listen to get vite's auto-resolved port
832
898
  const listen = viteDevServer.listen.bind(viteDevServer);
833
899
  viteDevServer.listen = async (port, isRestart) => {
@@ -836,11 +902,19 @@ function visage(options = {}) {
836
902
  if (!address || typeof address === 'string') {
837
903
  throw new Error('Failed to resolve port for Visage');
838
904
  }
839
- const vite = resolveViteUpstream({
840
- port: address.port,
841
- ...options.upstreams?.vite,
842
- });
843
- stop = await startVisage(vite);
905
+ const cache = join(viteDevServer.config.cacheDir, 'visage');
906
+ const config = resolveConfig(resolveOptions({
907
+ ...options,
908
+ upstreams: {
909
+ ...options.upstreams,
910
+ vite: resolveViteUpstream({
911
+ port: address.port,
912
+ ...options.upstreams?.vite,
913
+ }),
914
+ },
915
+ }), cache);
916
+ visageUrl = formatVisageUrlLog(config.host, config.port);
917
+ stop = await startVisageServer(config);
844
918
  viteDevServer.httpServer?.once('close', closeBundle);
845
919
  return result;
846
920
  };
@@ -856,4 +930,4 @@ function formatVisageUrlLog(host, port) {
856
930
  return ` ${AnsiGreen}➜${AnsiReset} ${AnsiBold}Visage${AnsiReset}: ${AnsiCyan}https://${host}:${AnsiBold}${port}${AnsiReset}${AnsiCyan}/${AnsiReset}`;
857
931
  }
858
932
 
859
- export { createVisageServer, visage as default };
933
+ export { createVisageServer, visage as default, visage };
@@ -0,0 +1,3 @@
1
+ import type { VisageConfig } from './config';
2
+ export declare function ensureNginxNetwork(config: VisageConfig): VisageConfig;
3
+ //# sourceMappingURL=network.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CA6CrE"}
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { VisageOptions } from './types';
3
+ export declare function visage(options?: VisageOptions): Plugin;
4
+ export default visage;
5
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAInC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,MAAM,CAoE1D;AAED,eAAe,MAAM,CAAC"}
@@ -1,3 +1,5 @@
1
- import type { VisageConfig } from '../config';
2
- export declare function render(config: VisageConfig): void;
1
+ export { writeComposeConfig } from './compose';
2
+ export { writeDexConfig } from './dex';
3
+ export { writeNginxConfig } from './nginx';
4
+ export { writeOauth2ProxyConfig } from './oauth2-proxy';
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAM9C,wBAAgB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAOjD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { type VisageConfig } from './config';
2
+ import type { VisageOptions, VisageServer } from './types';
3
+ export declare function createVisageServer(options: VisageOptions): VisageServer;
4
+ export declare function startVisageServer(config: VisageConfig): Promise<() => void>;
5
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,UAAU,CAAC;AASlB,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE3D,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAsBvE;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,MAAM,IAAI,CAAC,CAkBrB"}
package/dist/types.d.ts CHANGED
@@ -100,11 +100,14 @@ export type VisageCookiePolicy = {
100
100
  readonly path?: string;
101
101
  };
102
102
  /**
103
- * Managed Dex identity provider options.
103
+ * Managed Dex identity provider options. Dex is the default identity provider
104
+ * for Visage.
104
105
  */
105
106
  export type VisageDexOptions = {
106
107
  /**
107
108
  * Token expiration and rotation settings rendered into the Dex config.
109
+ *
110
+ * @see {@link https://dexidp.io/docs/configuration/tokens/#expiration-and-rotation-settings}
108
111
  */
109
112
  readonly expiry?: VisageDexExpiry;
110
113
  /**
@@ -190,30 +193,36 @@ export type VisageDexUser = {
190
193
  */
191
194
  export type VisageExternalIdpOptions = {
192
195
  /**
193
- * OIDC issuer URL used by OAuth2 Proxy.
196
+ * OIDC issuer URL used by OAuth2 Proxy. When no endpoint paths are
197
+ * configured, OAuth2 Proxy discovers provider endpoints from this issuer.
194
198
  */
195
199
  readonly issuer: string;
196
200
  /**
197
201
  * OIDC authorization path appended to
198
- * {@link VisageExternalIdpOptions.issuer}.
199
- *
200
- * @defaultValue '/auth'
202
+ * {@link VisageExternalIdpOptions.issuer}. Configure this, `token`, or `jwks`
203
+ * to disable OIDC discovery and render explicit provider endpoints.
201
204
  */
202
205
  readonly authorization?: string;
203
206
  /**
204
207
  * OIDC token endpoint path appended to
205
- * {@link VisageExternalIdpOptions.issuer}.
206
- *
207
- * @defaultValue '/token'
208
+ * {@link VisageExternalIdpOptions.issuer}. Configure this, `authorization`,
209
+ * or `jwks` to disable OIDC discovery and render explicit provider
210
+ * endpoints.
208
211
  */
209
212
  readonly token?: string;
210
213
  /**
211
214
  * OIDC JWKS endpoint path appended to
212
- * {@link VisageExternalIdpOptions.issuer}.
213
- *
214
- * @defaultValue '/keys'
215
+ * {@link VisageExternalIdpOptions.issuer}. Configure this, `authorization`,
216
+ * or `token` to disable OIDC discovery and render explicit provider
217
+ * endpoints.
215
218
  */
216
219
  readonly jwks?: string;
220
+ /**
221
+ * OIDC end-session endpoint URL. When configured, Visage routes OAuth2 Proxy
222
+ * sign-out redirects through this endpoint so the provider session can be
223
+ * ended as part of the sign-out flow.
224
+ */
225
+ readonly end_session_endpoint?: string;
217
226
  };
218
227
  /**
219
228
  * OAuth2 client configuration used by OAuth2 Proxy and, for managed Dex, the
@@ -333,15 +342,16 @@ export type VisageProxyPolicy = {
333
342
  */
334
343
  readonly redirect?: boolean;
335
344
  /**
336
- * Token forwarding behavior for the upstream `Authorization` header.
345
+ * Token forwarding behavior for the upstream `Authorization` header. Set
346
+ * to `false` to omit a bearer token. Set to `true` to forward the
347
+ * default bearer token for the upstream kind: an OAuth access token for
348
+ * external upstreams, or an OIDC ID token for local service upstreams.
337
349
  *
338
- * `'id'` forwards the authenticated OIDC ID token. `'access'` forwards the
339
- * OAuth access token for legacy/resource-server integrations that
340
- * explicitly require it.
350
+ * Use `'id'` or `'access'` to force a specific token kind.
341
351
  *
342
- * @defaultValue `'id'`
352
+ * @defaultValue `false`
343
353
  */
344
- readonly forward?: 'id' | 'access';
354
+ readonly forward?: boolean | 'id' | 'access';
345
355
  };
346
356
  /**
347
357
  * Request headers to set when proxying to the upstream. Values may include
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ services:
2
+ dex:
3
+ image: ghcr.io/dexidp/dex:v2.45.1
4
+ nginx:
5
+ image: nginx:1.30.0-alpine
6
+ oauth2_proxy:
7
+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blakearoberts/visage",
3
- "version": "0.0.1-rc.18",
3
+ "version": "0.0.1-rc.20",
4
4
  "description": "Vite plugin for local development with HMR and OIDC session cookie lifecycle semantics.",
5
5
  "type": "module",
6
6
  "author": "Blake Roberts",
@@ -33,7 +33,8 @@
33
33
  }
34
34
  },
35
35
  "files": [
36
- "dist"
36
+ "dist",
37
+ "docker-compose.images.yml"
37
38
  ],
38
39
  "engines": {
39
40
  "node": ">=20"
@@ -45,7 +46,8 @@
45
46
  "scripts": {
46
47
  "build": "npm run clean && rollup -c",
47
48
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
48
- "dev:example": "cd examples/simple && npm run dev",
49
+ "example": "npm run example:simple",
50
+ "example:simple": "cd examples/simple && npm run dev",
49
51
  "format": "prettier --write \"{docs,examples,src,test}/**/*.{ts,tsx,js,jsx,json,css,html,md}\" \"*.{json,md}\"",
50
52
  "format:check": "prettier --check \"{docs,examples,src,test}/**/*.{ts,tsx,js,jsx,json,css,html,md}\" \"*.{json,md}\"",
51
53
  "promote:codex": "scripts/promote-codex.sh",
@@ -63,7 +65,7 @@
63
65
  "rollup": "^4.60.4",
64
66
  "tslib": "^2.8.1",
65
67
  "typescript": "^5.9.3",
66
- "vite": "^6.3.5"
68
+ "vite": "^8.0.13"
67
69
  },
68
70
  "dependencies": {
69
71
  "bcryptjs": "^3.0.3",