@blakearoberts/visage 0.0.1-rc.19 → 0.0.1-rc.21
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 +38 -20
- package/dist/certs.d.ts +2 -6
- package/dist/certs.d.ts.map +1 -1
- package/dist/compose.d.ts +2 -1
- package/dist/compose.d.ts.map +1 -1
- package/dist/config.d.ts +27 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/hosts.d.ts +2 -1
- package/dist/hosts.d.ts.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +389 -286
- package/dist/network.d.ts +3 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/render/index.d.ts +4 -2
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/nginx.d.ts.map +1 -1
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/types.d.ts +36 -17
- package/dist/types.d.ts.map +1 -1
- package/docker-compose.images.yml +7 -0
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -1,111 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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:
|
|
214
|
+
files: BaseFiles,
|
|
215
|
+
network: {
|
|
216
|
+
name: process.env.COMPOSE_PROJECT_NAME ?? 'visage',
|
|
217
|
+
trustedProxyIps: [],
|
|
218
|
+
},
|
|
359
219
|
services: {
|
|
360
|
-
...(
|
|
361
|
-
? {
|
|
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 },
|
|
@@ -377,33 +237,106 @@ function resolveConfig(options, cache) {
|
|
|
377
237
|
{
|
|
378
238
|
...upstream,
|
|
379
239
|
external,
|
|
380
|
-
locations: Object.fromEntries(Object.entries(upstream.locations ?? {}).map(([path, policy]) =>
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
240
|
+
locations: Object.fromEntries(Object.entries(upstream.locations ?? {}).map(([path, policy]) => {
|
|
241
|
+
const auth = resolveAuthPolicy(policy.auth, external && name !== 'vite');
|
|
242
|
+
return [
|
|
243
|
+
path,
|
|
244
|
+
{
|
|
245
|
+
auth,
|
|
246
|
+
csrf: policy.csrf ?? (auth.enabled ? 'api' : false),
|
|
247
|
+
headers: {
|
|
248
|
+
...(external
|
|
249
|
+
? { ...DefaultProxyPolicy.headers, Host: upstream.host }
|
|
250
|
+
: DefaultProxyPolicy.headers),
|
|
251
|
+
...policy.headers,
|
|
252
|
+
},
|
|
253
|
+
directives: {
|
|
254
|
+
...DefaultProxyPolicy.directives,
|
|
255
|
+
...Object.fromEntries(Object.entries(policy.directives ?? {}).map(([name, value]) => [
|
|
256
|
+
name,
|
|
257
|
+
Array.isArray(value) ? value : [value],
|
|
258
|
+
])),
|
|
259
|
+
},
|
|
393
260
|
},
|
|
394
|
-
|
|
395
|
-
|
|
261
|
+
];
|
|
262
|
+
})),
|
|
396
263
|
},
|
|
397
264
|
];
|
|
398
265
|
})),
|
|
399
266
|
};
|
|
400
267
|
}
|
|
268
|
+
function resolveIdpConfig({ host, port, idp, }) {
|
|
269
|
+
if ('dex' in idp) {
|
|
270
|
+
return {
|
|
271
|
+
dex: {
|
|
272
|
+
expiry: idp.dex.expiry,
|
|
273
|
+
users: (idp.dex?.users ?? DefaultDexUsers).map((user) => ({
|
|
274
|
+
email: user.email,
|
|
275
|
+
password: user.password,
|
|
276
|
+
username: user.username ?? user.email.split('@', 1)[0],
|
|
277
|
+
userID: user.userID ?? user.email,
|
|
278
|
+
})),
|
|
279
|
+
},
|
|
280
|
+
oidc: {
|
|
281
|
+
issuer: `https://${host}:${port}/dex`,
|
|
282
|
+
authorization: `https://${host}:${port}/dex/auth`,
|
|
283
|
+
token: 'http://dex:5556/dex/token',
|
|
284
|
+
jwks: 'http://dex:5556/dex/keys',
|
|
285
|
+
},
|
|
286
|
+
upstream: {
|
|
287
|
+
dex: {
|
|
288
|
+
host: 'dex',
|
|
289
|
+
scheme: 'http',
|
|
290
|
+
port: 5556,
|
|
291
|
+
locations: { '/dex/': { auth: { enabled: false } } },
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const issuer = new URL(idp.issuer);
|
|
297
|
+
const oidc = {
|
|
298
|
+
issuer: idp.issuer,
|
|
299
|
+
...(idp.end_session_endpoint === undefined
|
|
300
|
+
? {}
|
|
301
|
+
: { end_session_endpoint: idp.end_session_endpoint }),
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
oidc: !idp.authorization && !idp.token && !idp.jwks
|
|
305
|
+
? oidc
|
|
306
|
+
: {
|
|
307
|
+
...oidc,
|
|
308
|
+
authorization: idp.issuer + (idp.authorization ?? '/auth'),
|
|
309
|
+
token: idp.issuer + (idp.token ?? '/token'),
|
|
310
|
+
jwks: idp.issuer + (idp.jwks ?? '/keys'),
|
|
311
|
+
},
|
|
312
|
+
upstream: {
|
|
313
|
+
idp: {
|
|
314
|
+
scheme: issuer.protocol === 'https:' ? 'https' : 'http',
|
|
315
|
+
host: issuer.hostname,
|
|
316
|
+
port: Number(issuer.port) || (issuer.protocol === 'https:' ? 443 : 80),
|
|
317
|
+
locations: {},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function resolveAuthPolicy(auth = {}, external) {
|
|
323
|
+
return {
|
|
324
|
+
enabled: auth.enabled ?? true,
|
|
325
|
+
forward: auth.forward === true
|
|
326
|
+
? external
|
|
327
|
+
? 'access'
|
|
328
|
+
: 'id'
|
|
329
|
+
: (auth.forward ?? false),
|
|
330
|
+
redirect: auth.redirect ?? false,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
401
333
|
const BaseViteUpstream = {
|
|
402
334
|
host: 'host.docker.internal',
|
|
403
335
|
scheme: 'http',
|
|
404
336
|
locations: {
|
|
405
337
|
'/': {
|
|
406
|
-
auth: {
|
|
338
|
+
auth: { redirect: true },
|
|
339
|
+
csrf: 'app',
|
|
407
340
|
headers: {
|
|
408
341
|
Host: '$host',
|
|
409
342
|
Upgrade: '$http_upgrade',
|
|
@@ -425,15 +358,14 @@ function resolveViteUpstream(vite = { locations: {} }) {
|
|
|
425
358
|
...Object.fromEntries(Object.entries(vite.locations ?? {}).map(([path, policy]) => {
|
|
426
359
|
if (path !== '/')
|
|
427
360
|
return [path, policy];
|
|
428
|
-
const
|
|
361
|
+
const base = BaseViteUpstream.locations['/'];
|
|
429
362
|
return [
|
|
430
363
|
path,
|
|
431
364
|
{
|
|
432
|
-
...
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
directives: { ...defaults.directives, ...policy.directives },
|
|
365
|
+
auth: { ...base.auth, ...policy.auth },
|
|
366
|
+
csrf: policy.csrf ?? base.csrf,
|
|
367
|
+
headers: { ...base.headers, ...policy.headers },
|
|
368
|
+
directives: { ...base.directives, ...policy.directives },
|
|
437
369
|
},
|
|
438
370
|
];
|
|
439
371
|
})),
|
|
@@ -441,12 +373,99 @@ function resolveViteUpstream(vite = { locations: {} }) {
|
|
|
441
373
|
};
|
|
442
374
|
}
|
|
443
375
|
|
|
376
|
+
const CACHE_HOME = process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
|
|
377
|
+
async function ensureCerts(config) {
|
|
378
|
+
const CAROOT = join(CACHE_HOME, 'visage/ca');
|
|
379
|
+
mkdirSync(CAROOT, { recursive: true, mode: 0o700 });
|
|
380
|
+
chmodSync(CAROOT, 0o700);
|
|
381
|
+
const mkcert = await ensureMkCert();
|
|
382
|
+
mkdirSync(join(config.cache, 'logs'), { recursive: true });
|
|
383
|
+
const out = openSync(join(config.cache, 'logs', 'mkcert.log'), 'w');
|
|
384
|
+
const env = { CAROOT, TRUST_STORES: 'system', ...process.env };
|
|
385
|
+
const tty = process.stdin.isTTY;
|
|
386
|
+
const stdio = [tty ? 'inherit' : 'ignore', out, out];
|
|
387
|
+
if (process.env.CI !== 'true') {
|
|
388
|
+
// mkcert -install is idempotent; CA files alone do not prove trust-store state.
|
|
389
|
+
const result = spawnSync(mkcert, ['-install'], { env, stdio });
|
|
390
|
+
if (result.error)
|
|
391
|
+
throw result.error;
|
|
392
|
+
if (result.status !== 0) {
|
|
393
|
+
throw new Error('Failed to install CA');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const certs = join(config.cache, config.files.certs[0]);
|
|
397
|
+
const cert = join(certs, 'tls.crt');
|
|
398
|
+
const key = join(certs, 'tls.key');
|
|
399
|
+
mkdirSync(certs, { recursive: true });
|
|
400
|
+
rmSync(cert, { force: true });
|
|
401
|
+
rmSync(key, { force: true });
|
|
402
|
+
const names = [...new Set([config.host, 'localhost', '127.0.0.1', '::1'])];
|
|
403
|
+
const args = ['-cert-file', cert, '-key-file', key, ...names];
|
|
404
|
+
const result = spawnSync(mkcert, args, { env, stdio });
|
|
405
|
+
if (result.error)
|
|
406
|
+
throw result.error;
|
|
407
|
+
if (result.status !== 0) {
|
|
408
|
+
throw new Error('Failed to generate TLS certificates');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function ensureMkCert() {
|
|
412
|
+
const bin = join(CACHE_HOME, 'visage/bin');
|
|
413
|
+
const file = join(bin, `mkcert-${process.platform}-${process.arch}`);
|
|
414
|
+
if (existsSync(file))
|
|
415
|
+
return file;
|
|
416
|
+
mkdirSync(bin, { recursive: true });
|
|
417
|
+
const base = 'https://dl.filippo.io/mkcert/latest';
|
|
418
|
+
const arch = process.arch === 'x64' ? 'amd64' : process.arch;
|
|
419
|
+
const params = `?for=${process.platform}/${arch}`;
|
|
420
|
+
const url = new URL(params, base);
|
|
421
|
+
const response = await fetch(url);
|
|
422
|
+
if (!response.ok || !response.body) {
|
|
423
|
+
throw new Error('Failed to download mkcert');
|
|
424
|
+
}
|
|
425
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(file));
|
|
426
|
+
chmodSync(file, 0o755);
|
|
427
|
+
return file;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let stopRef;
|
|
431
|
+
function startCompose(config) {
|
|
432
|
+
stopRef?.();
|
|
433
|
+
stopRef = undefined;
|
|
434
|
+
const file = join(config.cache, config.files.compose);
|
|
435
|
+
const logs = join(config.cache, 'logs');
|
|
436
|
+
const output = openSync(join(logs, 'compose.log'), 'w');
|
|
437
|
+
const compose = [
|
|
438
|
+
'compose',
|
|
439
|
+
'--ansi=never',
|
|
440
|
+
`--file=${file}`,
|
|
441
|
+
`--project-name=${process.env.COMPOSE_PROJECT_NAME ?? 'visage'}`,
|
|
442
|
+
];
|
|
443
|
+
const env = { ...process.env, COMPOSE_MENU: 'false' };
|
|
444
|
+
const opts = {
|
|
445
|
+
cwd: config.cache,
|
|
446
|
+
stdio: ['ignore', output, output],
|
|
447
|
+
env,
|
|
448
|
+
};
|
|
449
|
+
const up = [...compose, 'up', '--remove-orphans'];
|
|
450
|
+
const child = spawn('docker', up, opts);
|
|
451
|
+
const stop = () => {
|
|
452
|
+
if (stopRef !== stop)
|
|
453
|
+
return;
|
|
454
|
+
stopRef = undefined;
|
|
455
|
+
child.kill();
|
|
456
|
+
const down = [...compose, 'down', '--remove-orphans'];
|
|
457
|
+
spawnSync('docker', down, opts);
|
|
458
|
+
};
|
|
459
|
+
stopRef = stop;
|
|
460
|
+
return stop;
|
|
461
|
+
}
|
|
462
|
+
|
|
444
463
|
const HOSTS_FILE = '/etc/hosts';
|
|
445
|
-
function ensureHostEntry(
|
|
446
|
-
if (!
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
464
|
+
function ensureHostEntry({ host }) {
|
|
465
|
+
if (!host ||
|
|
466
|
+
host.trim() !== host ||
|
|
467
|
+
host.includes('/') ||
|
|
468
|
+
host.includes(':')) {
|
|
450
469
|
throw new Error('Invalid hostname');
|
|
451
470
|
}
|
|
452
471
|
const contents = readFileSync(HOSTS_FILE, 'utf8');
|
|
@@ -456,7 +475,7 @@ function ensureHostEntry(hostname) {
|
|
|
456
475
|
continue;
|
|
457
476
|
}
|
|
458
477
|
const [address, ...names] = uncommented.split(/\s+/);
|
|
459
|
-
if (!names.includes(
|
|
478
|
+
if (!names.includes(host)) {
|
|
460
479
|
continue;
|
|
461
480
|
}
|
|
462
481
|
if (address === '127.0.0.1' || address === '::1') {
|
|
@@ -466,7 +485,7 @@ function ensureHostEntry(hostname) {
|
|
|
466
485
|
throw new Error('Hosts file contains a conflicting entry');
|
|
467
486
|
}
|
|
468
487
|
const prefix = contents.endsWith('\n') ? '' : '\n';
|
|
469
|
-
const entry = `${prefix}127.0.0.1\t${
|
|
488
|
+
const entry = `${prefix}127.0.0.1\t${host} # visage\n`;
|
|
470
489
|
try {
|
|
471
490
|
appendFileSync(HOSTS_FILE, entry);
|
|
472
491
|
return;
|
|
@@ -486,6 +505,65 @@ function ensureHostEntry(hostname) {
|
|
|
486
505
|
}
|
|
487
506
|
}
|
|
488
507
|
|
|
508
|
+
function ensureNginxNetwork(config) {
|
|
509
|
+
const exists = spawnSync('docker', [
|
|
510
|
+
'network',
|
|
511
|
+
'ls',
|
|
512
|
+
'--filter',
|
|
513
|
+
`name=${config.network.name}`,
|
|
514
|
+
'--format',
|
|
515
|
+
'{{ .Name }}',
|
|
516
|
+
], { encoding: 'utf-8' });
|
|
517
|
+
if (exists.error)
|
|
518
|
+
throw exists.error;
|
|
519
|
+
if (exists.status !== 0) {
|
|
520
|
+
console.error(exists.stderr);
|
|
521
|
+
throw new Error('Failed to list Docker network');
|
|
522
|
+
}
|
|
523
|
+
if (exists.stdout) {
|
|
524
|
+
return {
|
|
525
|
+
...config,
|
|
526
|
+
network: {
|
|
527
|
+
...config.network,
|
|
528
|
+
trustedProxyIps: inspectNetwork(config.network.name),
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const create = spawnSync('docker', ['network', 'create', '--driver', 'bridge', config.network.name], { encoding: 'utf-8' });
|
|
533
|
+
if (create.error)
|
|
534
|
+
throw create.error;
|
|
535
|
+
if (create.status !== 0) {
|
|
536
|
+
console.error(create.stderr);
|
|
537
|
+
throw new Error('Failed to create Docker network');
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
...config,
|
|
541
|
+
network: {
|
|
542
|
+
...config.network,
|
|
543
|
+
trustedProxyIps: inspectNetwork(config.network.name),
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function inspectNetwork(name) {
|
|
548
|
+
const result = spawnSync('docker', [
|
|
549
|
+
'network',
|
|
550
|
+
'inspect',
|
|
551
|
+
'--format',
|
|
552
|
+
'{{range .IPAM.Config}}{{println .Subnet}}{{end}}',
|
|
553
|
+
name,
|
|
554
|
+
], { encoding: 'utf-8' });
|
|
555
|
+
if (result.error)
|
|
556
|
+
throw result.error;
|
|
557
|
+
if (result.status !== 0) {
|
|
558
|
+
console.error(result.stderr);
|
|
559
|
+
throw new Error('Failed to inspect Docker network');
|
|
560
|
+
}
|
|
561
|
+
return result.stdout
|
|
562
|
+
.split(/\r?\n/)
|
|
563
|
+
.map((line) => line.trim())
|
|
564
|
+
.filter(Boolean);
|
|
565
|
+
}
|
|
566
|
+
|
|
489
567
|
function writeComposeConfig(config) {
|
|
490
568
|
const file = join(config.cache, config.files.compose);
|
|
491
569
|
const render = renderComposeConfig(config);
|
|
@@ -494,8 +572,9 @@ function writeComposeConfig(config) {
|
|
|
494
572
|
function renderComposeConfig(config) {
|
|
495
573
|
const { dex, nginx, oauth2_proxy, ...services } = config.services;
|
|
496
574
|
return stringify({
|
|
575
|
+
networks: { default: { external: true, name: config.network.name } },
|
|
497
576
|
services: {
|
|
498
|
-
...(config.idp
|
|
577
|
+
...('dex' in config.idp
|
|
499
578
|
? {
|
|
500
579
|
dex: {
|
|
501
580
|
...config.services.dex,
|
|
@@ -526,38 +605,34 @@ function renderComposeConfig(config) {
|
|
|
526
605
|
}
|
|
527
606
|
|
|
528
607
|
function writeDexConfig(config) {
|
|
529
|
-
const file = join(config.cache, config.files.dex[0]);
|
|
530
608
|
const render = renderDexConfig(config);
|
|
609
|
+
const file = join(config.cache, config.files.dex[0]);
|
|
531
610
|
writeFileSync(file, render, 'utf-8');
|
|
532
611
|
}
|
|
533
612
|
function renderDexConfig(config) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
const origin = `https://${config.host}:${config.port}`;
|
|
539
|
-
const redirect = `${origin}/oauth2/callback`;
|
|
540
|
-
const upstream = config.upstreams[idp.upstream];
|
|
613
|
+
if (!('dex' in config.idp))
|
|
614
|
+
throw new Error('Dex config missing');
|
|
615
|
+
const { host, port, oauth2, idp: { dex: { expiry, users }, oidc, upstream, }, } = config;
|
|
541
616
|
return stringify({
|
|
542
|
-
issuer:
|
|
617
|
+
issuer: oidc.issuer,
|
|
543
618
|
storage: { type: 'memory' },
|
|
544
|
-
web: { http: `0.0.0.0:${upstream.port}` },
|
|
619
|
+
web: { http: `0.0.0.0:${upstream.dex.port}` },
|
|
545
620
|
oauth2: { skipApprovalScreen: true },
|
|
546
621
|
staticClients: [
|
|
547
622
|
{
|
|
548
|
-
id:
|
|
623
|
+
id: oauth2.id,
|
|
549
624
|
name: 'Visage',
|
|
550
|
-
...(
|
|
625
|
+
...(oauth2.secret === undefined
|
|
551
626
|
? { public: true }
|
|
552
|
-
: { secret:
|
|
553
|
-
redirectURIs: [
|
|
627
|
+
: { secret: oauth2.secret }),
|
|
628
|
+
redirectURIs: [`https://${host}:${port}/oauth2/callback`],
|
|
554
629
|
},
|
|
555
630
|
],
|
|
556
631
|
enablePasswordDB: true,
|
|
557
|
-
...(
|
|
558
|
-
staticPasswords:
|
|
632
|
+
...(expiry === undefined ? {} : { expiry }),
|
|
633
|
+
staticPasswords: users.map(({ password, ...user }) => ({
|
|
559
634
|
...user,
|
|
560
|
-
hash: hashSync(password
|
|
635
|
+
hash: hashSync(password),
|
|
561
636
|
})),
|
|
562
637
|
});
|
|
563
638
|
}
|
|
@@ -582,6 +657,18 @@ http {
|
|
|
582
657
|
'' close;
|
|
583
658
|
}
|
|
584
659
|
|
|
660
|
+
# Fetch Metadata CSRF guards for cookie-backed locations.
|
|
661
|
+
map $http_sec_fetch_site $csrf_api {
|
|
662
|
+
default 0;
|
|
663
|
+
same-site 1;
|
|
664
|
+
cross-site 1;
|
|
665
|
+
}
|
|
666
|
+
map "$http_sec_fetch_site:$request_method:$http_sec_fetch_mode:$http_sec_fetch_dest" $csrf_app {
|
|
667
|
+
default 0;
|
|
668
|
+
~^(cross-site|same-site):GET:navigate:document$ 0;
|
|
669
|
+
~^(cross-site|same-site): 1;
|
|
670
|
+
}
|
|
671
|
+
|
|
585
672
|
<%_ for (const [name, upstream] of Object.entries(it.upstreams)) { %>
|
|
586
673
|
|
|
587
674
|
upstream <%~ name %> {
|
|
@@ -608,8 +695,21 @@ http {
|
|
|
608
695
|
error_page 497 =301 https://$http_host$request_uri;
|
|
609
696
|
|
|
610
697
|
<%_ for (const [name, upstream] of Object.entries(it.upstreams)) { %>
|
|
611
|
-
|
|
698
|
+
<%_ for (const [path, location] of Object.entries(upstream.locations)) { %>
|
|
612
699
|
location <%~ path %> {
|
|
700
|
+
<%_ if (location.csrf) { %>
|
|
701
|
+
add_header Vary "Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-Dest" always;
|
|
702
|
+
<%_ if (location.csrf === 'app') { %>
|
|
703
|
+
if ($csrf_app) {
|
|
704
|
+
return 403;
|
|
705
|
+
}
|
|
706
|
+
<%_ } else { %>
|
|
707
|
+
if ($csrf_api) {
|
|
708
|
+
return 403;
|
|
709
|
+
}
|
|
710
|
+
<%_ } %>
|
|
711
|
+
|
|
712
|
+
<%_ } %>
|
|
613
713
|
<%_ if (location.auth?.enabled) { %>
|
|
614
714
|
auth_request /oauth2/auth;
|
|
615
715
|
auth_request_set $authorization $upstream_http_authorization;
|
|
@@ -646,7 +746,7 @@ http {
|
|
|
646
746
|
<%_ } %>
|
|
647
747
|
proxy_pass <%~ upstream.scheme %>://<%~ name %>;
|
|
648
748
|
}
|
|
649
|
-
|
|
749
|
+
<%_ } %>
|
|
650
750
|
|
|
651
751
|
<%_ } %>
|
|
652
752
|
}
|
|
@@ -703,11 +803,15 @@ function renderOauth2ProxyConfig(config) {
|
|
|
703
803
|
const data = {
|
|
704
804
|
http_address: `0.0.0.0:${config.upstreams.oauth2_proxy.port}`,
|
|
705
805
|
provider: 'oidc',
|
|
706
|
-
oidc_issuer_url: config.idp.issuer,
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
806
|
+
oidc_issuer_url: config.idp.oidc.issuer,
|
|
807
|
+
...('authorization' in config.idp.oidc
|
|
808
|
+
? {
|
|
809
|
+
skip_oidc_discovery: true,
|
|
810
|
+
login_url: config.idp.oidc.authorization,
|
|
811
|
+
redeem_url: config.idp.oidc.token,
|
|
812
|
+
oidc_jwks_url: config.idp.oidc.jwks,
|
|
813
|
+
}
|
|
814
|
+
: {}),
|
|
711
815
|
redirect_url: `https://${config.host}:${config.port}/oauth2/callback`,
|
|
712
816
|
client_id: config.oauth2.id,
|
|
713
817
|
...(config.oauth2.secret === undefined
|
|
@@ -723,9 +827,16 @@ function renderOauth2ProxyConfig(config) {
|
|
|
723
827
|
cookie_csrf_per_request: true,
|
|
724
828
|
cookie_csrf_per_request_limit: 16,
|
|
725
829
|
email_domains: config.oauth2.emailDomains,
|
|
726
|
-
whitelist_domains: [
|
|
830
|
+
whitelist_domains: [
|
|
831
|
+
config.host,
|
|
832
|
+
`${config.host}:${config.port}`,
|
|
833
|
+
...(!('dex' in config.idp) && config.idp.oidc.end_session_endpoint
|
|
834
|
+
? [new URL(config.idp.oidc.end_session_endpoint).host]
|
|
835
|
+
: []),
|
|
836
|
+
],
|
|
727
837
|
scope: config.oauth2.scopes.join(' '),
|
|
728
838
|
reverse_proxy: true,
|
|
839
|
+
trusted_proxy_ips: config.network.trustedProxyIps,
|
|
729
840
|
set_xauthrequest: true,
|
|
730
841
|
set_authorization_header: true,
|
|
731
842
|
pass_access_token: true,
|
|
@@ -746,36 +857,19 @@ function renderOauth2ProxyConfig(config) {
|
|
|
746
857
|
.join('\n')}\n`;
|
|
747
858
|
}
|
|
748
859
|
|
|
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
860
|
function createVisageServer(options) {
|
|
861
|
+
const cache = join(process.cwd(), '.visage');
|
|
759
862
|
const config = resolveConfig(resolveOptions({
|
|
760
863
|
...options,
|
|
761
864
|
upstreams: {
|
|
762
865
|
...options.upstreams,
|
|
763
866
|
vite: resolveViteUpstream(options.upstreams?.vite),
|
|
764
867
|
},
|
|
765
|
-
}),
|
|
868
|
+
}), cache);
|
|
766
869
|
let stop;
|
|
767
870
|
return {
|
|
768
871
|
async listen() {
|
|
769
|
-
|
|
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));
|
|
872
|
+
stop ??= await startVisageServer(config);
|
|
779
873
|
},
|
|
780
874
|
close() {
|
|
781
875
|
stop?.();
|
|
@@ -783,6 +877,22 @@ function createVisageServer(options) {
|
|
|
783
877
|
},
|
|
784
878
|
};
|
|
785
879
|
}
|
|
880
|
+
async function startVisageServer(config) {
|
|
881
|
+
const logs = join(config.cache, 'logs');
|
|
882
|
+
rmSync(logs, { recursive: true, force: true });
|
|
883
|
+
mkdirSync(logs, { recursive: true });
|
|
884
|
+
await ensureCerts(config);
|
|
885
|
+
ensureHostEntry(config);
|
|
886
|
+
const renderConfig = ensureNginxNetwork(config);
|
|
887
|
+
writeComposeConfig(renderConfig);
|
|
888
|
+
if ('dex' in renderConfig.idp) {
|
|
889
|
+
writeDexConfig(renderConfig);
|
|
890
|
+
}
|
|
891
|
+
writeNginxConfig(renderConfig);
|
|
892
|
+
writeOauth2ProxyConfig(renderConfig);
|
|
893
|
+
return startCompose(renderConfig);
|
|
894
|
+
}
|
|
895
|
+
|
|
786
896
|
function visage(options = {}) {
|
|
787
897
|
const resolvedOptions = resolveOptions(options);
|
|
788
898
|
let stop;
|
|
@@ -813,21 +923,6 @@ function visage(options = {}) {
|
|
|
813
923
|
printUrls();
|
|
814
924
|
viteDevServer.config.logger.info(visageUrl ?? 'Visage failed to start');
|
|
815
925
|
};
|
|
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
926
|
// monkey patch vite's listen to get vite's auto-resolved port
|
|
832
927
|
const listen = viteDevServer.listen.bind(viteDevServer);
|
|
833
928
|
viteDevServer.listen = async (port, isRestart) => {
|
|
@@ -836,11 +931,19 @@ function visage(options = {}) {
|
|
|
836
931
|
if (!address || typeof address === 'string') {
|
|
837
932
|
throw new Error('Failed to resolve port for Visage');
|
|
838
933
|
}
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
...options
|
|
842
|
-
|
|
843
|
-
|
|
934
|
+
const cache = join(viteDevServer.config.cacheDir, 'visage');
|
|
935
|
+
const config = resolveConfig(resolveOptions({
|
|
936
|
+
...options,
|
|
937
|
+
upstreams: {
|
|
938
|
+
...options.upstreams,
|
|
939
|
+
vite: resolveViteUpstream({
|
|
940
|
+
port: address.port,
|
|
941
|
+
...options.upstreams?.vite,
|
|
942
|
+
}),
|
|
943
|
+
},
|
|
944
|
+
}), cache);
|
|
945
|
+
visageUrl = formatVisageUrlLog(config.host, config.port);
|
|
946
|
+
stop = await startVisageServer(config);
|
|
844
947
|
viteDevServer.httpServer?.once('close', closeBundle);
|
|
845
948
|
return result;
|
|
846
949
|
};
|
|
@@ -856,4 +959,4 @@ function formatVisageUrlLog(host, port) {
|
|
|
856
959
|
return ` ${AnsiGreen}➜${AnsiReset} ${AnsiBold}Visage${AnsiReset}: ${AnsiCyan}https://${host}:${AnsiBold}${port}${AnsiReset}${AnsiCyan}/${AnsiReset}`;
|
|
857
960
|
}
|
|
858
961
|
|
|
859
|
-
export { createVisageServer, visage as default };
|
|
962
|
+
export { createVisageServer, visage as default, visage };
|