@blakearoberts/visage 0.0.1-rc.26 → 0.0.1-rc.27
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 +41 -22
- package/dist/certs.d.ts.map +1 -1
- package/dist/index.js +74 -21
- package/package.json +12 -9
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Visage
|
|
2
2
|
|
|
3
|
-
Visage (`/vit·ɛdʒ/`) is a Vite plugin for local development with HMR and OIDC
|
|
3
|
+
Visage (`/vit·ɛdʒ/`) is a Vite plugin for local development with HMR and OIDC
|
|
4
|
+
session cookie lifecycle semantics.
|
|
4
5
|
|
|
5
6
|
## Getting Started
|
|
6
7
|
|
|
@@ -27,21 +28,32 @@ Start Vite normally:
|
|
|
27
28
|
vite
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
By default, you can reach the app at `https://localhost:9001`. You will be
|
|
31
|
+
By default, you can reach the app at `https://localhost:9001`. You will be
|
|
32
|
+
redirected to Dex to sign in. The default username and password is
|
|
33
|
+
`user@example.com` and `pass`.
|
|
31
34
|
|
|
32
35
|
## Why Visage
|
|
33
36
|
|
|
34
|
-
Visage is a local development harness for web apps that run behind an
|
|
37
|
+
Visage is a local development harness for web apps that run behind an
|
|
38
|
+
auth-protected edge, where browser sessions are represented by secure cookies
|
|
39
|
+
backed by OIDC tokens.
|
|
35
40
|
|
|
36
|
-
Visage narrows the gap between local development, automated tests, and
|
|
41
|
+
Visage narrows the gap between local development, automated tests, and
|
|
42
|
+
production by bringing production-like session lifecycle semantics to local Vite
|
|
43
|
+
development without giving up HMR. That makes it practical to iterate on SSR
|
|
44
|
+
identity injection, session timeout recovery, lock screens, and authenticated
|
|
45
|
+
API calls.
|
|
37
46
|
|
|
38
|
-
Visage can also use a hosted IdP, so local frontend code can call hosted backend
|
|
47
|
+
Visage can also use a hosted IdP, so local frontend code can call hosted backend
|
|
48
|
+
APIs with real credentials. That avoids frontend-only auth mocks or backend-only
|
|
49
|
+
local bypasses: code can be written for production and still work locally.
|
|
39
50
|
|
|
40
51
|
## Configuration
|
|
41
52
|
|
|
42
53
|
Visage is configured through `visage(options?)` in `vite.config.ts`.
|
|
43
54
|
|
|
44
|
-
The top-level `host` and `port` configure the local Visage origin that the
|
|
55
|
+
The top-level `host` and `port` configure the local Visage origin that the
|
|
56
|
+
browser visits:
|
|
45
57
|
|
|
46
58
|
```ts
|
|
47
59
|
visage({ host: 'localhost', port: 9001 });
|
|
@@ -94,8 +106,7 @@ visage({
|
|
|
94
106
|
```
|
|
95
107
|
|
|
96
108
|
OAuth2 Proxy identity values can also be mapped explicitly through headers such
|
|
97
|
-
as `$auth_user`, `$auth_email`, `$auth_groups`, and
|
|
98
|
-
`$auth_preferred_username`.
|
|
109
|
+
as `$auth_user`, `$auth_email`, `$auth_groups`, and `$auth_preferred_username`.
|
|
99
110
|
|
|
100
111
|
Authenticated locations also get Fetch Metadata CSRF checks by default. The
|
|
101
112
|
built-in Vite root location uses `csrf: 'app'`, which allows same-origin
|
|
@@ -141,15 +152,12 @@ flowchart LR
|
|
|
141
152
|
|
|
142
153
|
## Required Tools
|
|
143
154
|
|
|
144
|
-
- [Docker](https://docs.docker.com/get-started/get-docker/) with Compose v2
|
|
155
|
+
- [Docker](https://docs.docker.com/get-started/get-docker/) with Compose v2
|
|
156
|
+
support through `docker compose`.
|
|
157
|
+
- [`mkcert`](https://github.com/FiloSottile/mkcert#installation) installed on
|
|
158
|
+
`PATH`, or configured with `VISAGE_MKCERT=/path/to/mkcert`.
|
|
145
159
|
|
|
146
|
-
## Managed
|
|
147
|
-
|
|
148
|
-
### mkcert
|
|
149
|
-
|
|
150
|
-
Visage downloads [`mkcert`](https://github.com/FiloSottile/mkcert) from `dl.filippo.io` into `$XDG_CACHE_HOME/visage/bin/mkcert-<platform>-<arch>` when the Vite dev server starts. Visage uses it to install a local certificate authority and generate HTTPS certificates for the local proxy.
|
|
151
|
-
|
|
152
|
-
### Docker Images
|
|
160
|
+
## Managed Docker Images
|
|
153
161
|
|
|
154
162
|
Visage pulls these as needed based on configuration:
|
|
155
163
|
|
|
@@ -161,9 +169,15 @@ Visage pulls these as needed based on configuration:
|
|
|
161
169
|
|
|
162
170
|
## Security Notes
|
|
163
171
|
|
|
164
|
-
Visage is local-development tooling. It starts local auth infrastructure,
|
|
172
|
+
Visage is local-development tooling. It starts local auth infrastructure,
|
|
173
|
+
terminates local HTTPS, and forwards authenticated identity or token material to
|
|
174
|
+
configured upstreams.
|
|
175
|
+
|
|
176
|
+
Please report suspected vulnerabilities through GitHub private vulnerability
|
|
177
|
+
reporting as described in [Security Policy](SECURITY.md).
|
|
165
178
|
|
|
166
|
-
Do not treat the managed Dex and OAuth2 Proxy defaults as production auth
|
|
179
|
+
Do not treat the managed Dex and OAuth2 Proxy defaults as production auth
|
|
180
|
+
infrastructure.
|
|
167
181
|
|
|
168
182
|
Visage's CSRF policy is an edge request-isolation guard for cookie-backed
|
|
169
183
|
locations. It is not a replacement for application-owned CSRF tokens where an
|
|
@@ -172,14 +186,19 @@ application accepts form posts or other browser-submitted mutations. CSP,
|
|
|
172
186
|
|
|
173
187
|
## Troubleshooting
|
|
174
188
|
|
|
175
|
-
- If startup fails immediately, confirm Docker is running and `docker compose`
|
|
189
|
+
- If startup fails immediately, confirm Docker is running and `docker compose`
|
|
190
|
+
works.
|
|
176
191
|
- If NGINX cannot start, check whether the configured `port` is already in use.
|
|
177
|
-
- If the hostname cannot be resolved, Visage may need permission to update
|
|
178
|
-
|
|
192
|
+
- If the hostname cannot be resolved, Visage may need permission to update
|
|
193
|
+
`/etc/hosts`.
|
|
194
|
+
- If the browser rejects the certificate, allow the local certificate authority
|
|
195
|
+
prompt from `mkcert`; CI test runners should be configured to ignore local
|
|
196
|
+
HTTPS errors.
|
|
179
197
|
|
|
180
198
|
## TO-DO
|
|
181
199
|
|
|
182
|
-
- [ ] Harden the default security posture by addressing the
|
|
200
|
+
- [ ] Harden the default security posture by addressing the
|
|
201
|
+
[security hardening backlog](docs/security-hardening.md).
|
|
183
202
|
- [ ] Support configuring [Dex connectors](https://dexidp.io/docs/connectors/).
|
|
184
203
|
- [ ] Support configuring Dex on a distinct subdomain, such as `auth.localhost`.
|
|
185
204
|
- [ ] Support optional [HTTP mode without local TLS](docs/tls-http-mode.md).
|
package/dist/certs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"certs.d.ts","sourceRoot":"","sources":["../src/certs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"certs.d.ts","sourceRoot":"","sources":["../src/certs.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAI7C,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCrE"}
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,9 @@ import { spawnSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { isIP } from 'node:net';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { readFileSync, mkdirSync, chmodSync, openSync, rmSync,
|
|
5
|
+
import { readFileSync, mkdirSync, chmodSync, openSync, rmSync, appendFileSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { parse, stringify } from 'yaml';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
-
import { Readable } from 'node:stream';
|
|
9
|
-
import { pipeline } from 'node:stream/promises';
|
|
10
8
|
import { hashSync } from 'bcryptjs';
|
|
11
9
|
import { Eta } from 'eta';
|
|
12
10
|
|
|
@@ -465,13 +463,14 @@ async function ensureCerts(config) {
|
|
|
465
463
|
const CAROOT = join(CACHE_HOME, 'visage/ca');
|
|
466
464
|
mkdirSync(CAROOT, { recursive: true, mode: 0o700 });
|
|
467
465
|
chmodSync(CAROOT, 0o700);
|
|
468
|
-
const mkcert =
|
|
466
|
+
const mkcert = resolveMkcert();
|
|
469
467
|
const out = openSync(join(config.cache, 'logs', 'mkcert.log'), 'w');
|
|
470
468
|
const env = { CAROOT, TRUST_STORES: 'system', ...process.env };
|
|
471
469
|
const tty = process.stdin.isTTY;
|
|
472
470
|
const stdio = [tty ? 'inherit' : 'ignore', out, out];
|
|
473
471
|
if (process.env.CI !== 'true') {
|
|
474
|
-
// mkcert -install is idempotent;
|
|
472
|
+
// mkcert -install is idempotent;
|
|
473
|
+
// CA files alone don't prove trust-store state.
|
|
475
474
|
const result = spawnSync(mkcert, ['-install'], { env, stdio });
|
|
476
475
|
if (result.error)
|
|
477
476
|
throw result.error;
|
|
@@ -497,23 +496,77 @@ async function ensureCerts(config) {
|
|
|
497
496
|
chmodSync(cert, 0o600);
|
|
498
497
|
chmodSync(key, 0o600);
|
|
499
498
|
}
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (!response.ok || !response.body) {
|
|
512
|
-
throw new Error('Failed to download mkcert');
|
|
499
|
+
function resolveMkcert() {
|
|
500
|
+
const env = process.env;
|
|
501
|
+
const options = { encoding: 'utf8', env };
|
|
502
|
+
const mkcert = findMkcert();
|
|
503
|
+
const result = spawnSync(mkcert, ['-version'], options);
|
|
504
|
+
if (result.error || result.status !== 0) {
|
|
505
|
+
throw new Error([
|
|
506
|
+
`Visage found mkcert at "${mkcert}", but could not execute it.`,
|
|
507
|
+
'',
|
|
508
|
+
mkcertInstallInstructions(),
|
|
509
|
+
].join('\n'));
|
|
513
510
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
511
|
+
return mkcert;
|
|
512
|
+
}
|
|
513
|
+
function findMkcert() {
|
|
514
|
+
const env = process.env;
|
|
515
|
+
const exec = env.VISAGE_MKCERT || 'mkcert';
|
|
516
|
+
const options = { encoding: 'utf8', env };
|
|
517
|
+
const result = process.platform === 'win32'
|
|
518
|
+
? spawnSync('where', [exec], options)
|
|
519
|
+
: spawnSync('sh', ['-c', `command -v ${exec}`], options);
|
|
520
|
+
const path = result.stdout
|
|
521
|
+
.split(/\r?\n/)
|
|
522
|
+
.map((line) => line.trim())
|
|
523
|
+
.find(Boolean);
|
|
524
|
+
if (result.error || result.status !== 0 || !path) {
|
|
525
|
+
throw new Error([
|
|
526
|
+
'Visage requires mkcert to configure HTTPS, but mkcert was not found.',
|
|
527
|
+
'',
|
|
528
|
+
mkcertInstallInstructions(),
|
|
529
|
+
].join('\n'));
|
|
530
|
+
}
|
|
531
|
+
return path;
|
|
532
|
+
}
|
|
533
|
+
function mkcertInstallInstructions() {
|
|
534
|
+
const common = [
|
|
535
|
+
'After installing mkcert, run `mkcert -install` once when local ' +
|
|
536
|
+
'certificates should be trusted.',
|
|
537
|
+
'Install docs: https://github.com/FiloSottile/mkcert#installation',
|
|
538
|
+
'Set VISAGE_MKCERT=/path/to/mkcert to use a custom executable.',
|
|
539
|
+
];
|
|
540
|
+
const platform = process.platform;
|
|
541
|
+
if (platform === 'darwin') {
|
|
542
|
+
return [
|
|
543
|
+
'Install mkcert with Homebrew:',
|
|
544
|
+
' brew install mkcert',
|
|
545
|
+
' brew install nss # optional, for Firefox',
|
|
546
|
+
...common,
|
|
547
|
+
].join('\n');
|
|
548
|
+
}
|
|
549
|
+
if (platform === 'win32') {
|
|
550
|
+
return [
|
|
551
|
+
'Install mkcert with Chocolatey or Scoop:',
|
|
552
|
+
' choco install mkcert',
|
|
553
|
+
' scoop install mkcert',
|
|
554
|
+
...common,
|
|
555
|
+
].join('\n');
|
|
556
|
+
}
|
|
557
|
+
if (platform === 'linux') {
|
|
558
|
+
return [
|
|
559
|
+
'Install mkcert with your Linux package manager. Common commands:',
|
|
560
|
+
' sudo apt install mkcert libnss3-tools',
|
|
561
|
+
' sudo dnf install mkcert nss-tools',
|
|
562
|
+
' sudo pacman -Syu mkcert nss',
|
|
563
|
+
...common,
|
|
564
|
+
].join('\n');
|
|
565
|
+
}
|
|
566
|
+
return [
|
|
567
|
+
'Install mkcert for your operating system and make it available on PATH.',
|
|
568
|
+
...common,
|
|
569
|
+
].join('\n');
|
|
517
570
|
}
|
|
518
571
|
|
|
519
572
|
let stopRef;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blakearoberts/visage",
|
|
3
|
-
"version": "0.0.1-rc.
|
|
3
|
+
"version": "0.0.1-rc.27",
|
|
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",
|
|
@@ -48,14 +48,22 @@
|
|
|
48
48
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
49
49
|
"example": "npm run example:simple",
|
|
50
50
|
"example:simple": "cd examples/simple && npm run dev",
|
|
51
|
-
"format": "prettier --write
|
|
52
|
-
"format:check": "prettier --check
|
|
51
|
+
"format": "prettier --write .",
|
|
52
|
+
"format:check": "prettier --check .",
|
|
53
53
|
"promote:codex": "scripts/promote-codex.sh",
|
|
54
54
|
"test": "npm run typecheck && npm run test:unit",
|
|
55
55
|
"test:all": "npm test && npm run test:e2e",
|
|
56
56
|
"test:e2e": "playwright test test/e2e",
|
|
57
57
|
"test:unit": "node --experimental-strip-types --test test/unit/*.test.ts",
|
|
58
|
-
"typecheck": "tsc --noEmit
|
|
58
|
+
"typecheck": "tsc --noEmit"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"bcryptjs": "^3.0.3",
|
|
62
|
+
"eta": "^4.6.0",
|
|
63
|
+
"yaml": "^2.9.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"vite": "^8.0.0"
|
|
59
67
|
},
|
|
60
68
|
"devDependencies": {
|
|
61
69
|
"@playwright/test": "^1.60.0",
|
|
@@ -66,10 +74,5 @@
|
|
|
66
74
|
"tslib": "^2.8.1",
|
|
67
75
|
"typescript": "^6.0.3",
|
|
68
76
|
"vite": "^8.0.13"
|
|
69
|
-
},
|
|
70
|
-
"dependencies": {
|
|
71
|
-
"bcryptjs": "^3.0.3",
|
|
72
|
-
"eta": "^4.6.0",
|
|
73
|
-
"yaml": "^2.9.0"
|
|
74
77
|
}
|
|
75
78
|
}
|