vite-plugin-auth 0.0.1

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.
@@ -0,0 +1,34 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface AppearanceOptions {
4
+ title?: string;
5
+ descriptionHtml?: string;
6
+ backgroundColor?: string;
7
+ backgroundImage?: string;
8
+ }
9
+ interface AuthGateOptions {
10
+ users?: Array<{
11
+ username: string;
12
+ password: string;
13
+ }>;
14
+ username?: string;
15
+ password?: string;
16
+ sessionDuration?: number;
17
+ cookieName?: string;
18
+ loginPath?: string;
19
+ allowedIPs?: string[];
20
+ title?: string;
21
+ description?: string;
22
+ appearance?: AppearanceOptions;
23
+ sessionFile?: string;
24
+ }
25
+ interface ResolvedAppearance {
26
+ title: string;
27
+ descriptionHtml: string;
28
+ backgroundColor: string;
29
+ backgroundImage: string;
30
+ }
31
+
32
+ declare const auth: (raw?: AuthGateOptions) => Plugin;
33
+
34
+ export { type AppearanceOptions, type AuthGateOptions, type ResolvedAppearance, auth, auth as default };
package/dist/index.js ADDED
@@ -0,0 +1,288 @@
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
3
+ };
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
9
+
10
+ // src/session.ts
11
+ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "fs";
12
+ import { dirname } from "path";
13
+
14
+ // src/crypto.ts
15
+ import crypto from "crypto";
16
+ var generateToken = () => crypto.randomBytes(48).toString("hex");
17
+ var safeEqual = (a, b) => {
18
+ try {
19
+ return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
20
+ } catch {
21
+ return false;
22
+ }
23
+ };
24
+
25
+ // src/session.ts
26
+ var CLEANUP_INTERVAL = 5 * 60 * 1e3;
27
+ var _sessions, _timer, _filePath, _SessionStore_instances, load_fn, save_fn, cleanup_fn;
28
+ var SessionStore = class {
29
+ constructor(filePath) {
30
+ __privateAdd(this, _SessionStore_instances);
31
+ __privateAdd(this, _sessions, /* @__PURE__ */ new Map());
32
+ __privateAdd(this, _timer);
33
+ __privateAdd(this, _filePath);
34
+ __privateSet(this, _filePath, filePath);
35
+ if (__privateGet(this, _filePath)) __privateMethod(this, _SessionStore_instances, load_fn).call(this);
36
+ }
37
+ create(username, ttlMs) {
38
+ const token = generateToken();
39
+ const now = Date.now();
40
+ __privateGet(this, _sessions).set(token, { username, createdAt: now, expiresAt: now + ttlMs });
41
+ __privateMethod(this, _SessionStore_instances, save_fn).call(this);
42
+ return token;
43
+ }
44
+ get(token) {
45
+ const s = __privateGet(this, _sessions).get(token);
46
+ if (!s) return null;
47
+ if (s.expiresAt <= Date.now()) {
48
+ __privateGet(this, _sessions).delete(token);
49
+ __privateMethod(this, _SessionStore_instances, save_fn).call(this);
50
+ return null;
51
+ }
52
+ return s;
53
+ }
54
+ startCleanup() {
55
+ __privateSet(this, _timer, setInterval(() => __privateMethod(this, _SessionStore_instances, cleanup_fn).call(this), CLEANUP_INTERVAL));
56
+ if (__privateGet(this, _timer)?.unref) __privateGet(this, _timer).unref();
57
+ }
58
+ destroy() {
59
+ clearInterval(__privateGet(this, _timer));
60
+ __privateGet(this, _sessions).clear();
61
+ }
62
+ };
63
+ _sessions = new WeakMap();
64
+ _timer = new WeakMap();
65
+ _filePath = new WeakMap();
66
+ _SessionStore_instances = new WeakSet();
67
+ load_fn = function() {
68
+ try {
69
+ if (!existsSync(__privateGet(this, _filePath))) return;
70
+ const raw = readFileSync(__privateGet(this, _filePath), "utf-8");
71
+ const data = JSON.parse(raw);
72
+ const now = Date.now();
73
+ for (const [token, s] of Object.entries(data)) {
74
+ const session = s;
75
+ if (session.expiresAt > now) __privateGet(this, _sessions).set(token, session);
76
+ }
77
+ } catch {
78
+ }
79
+ };
80
+ save_fn = function() {
81
+ if (!__privateGet(this, _filePath)) return;
82
+ const data = {};
83
+ for (const [token, s] of __privateGet(this, _sessions)) data[token] = s;
84
+ const dir = dirname(__privateGet(this, _filePath));
85
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
86
+ const tmp = __privateGet(this, _filePath) + ".tmp";
87
+ writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
88
+ renameSync(tmp, __privateGet(this, _filePath));
89
+ };
90
+ cleanup_fn = function() {
91
+ const now = Date.now();
92
+ let changed = false;
93
+ for (const [token, s] of __privateGet(this, _sessions)) {
94
+ if (s.expiresAt <= now) {
95
+ __privateGet(this, _sessions).delete(token);
96
+ changed = true;
97
+ }
98
+ }
99
+ if (changed) __privateMethod(this, _SessionStore_instances, save_fn).call(this);
100
+ };
101
+
102
+ // src/utils.ts
103
+ import { isIP } from "net";
104
+ var parseCookies = (header) => {
105
+ const map = {};
106
+ if (!header) return map;
107
+ for (const pair of header.split(";")) {
108
+ const idx = pair.indexOf("=");
109
+ if (idx === -1) continue;
110
+ map[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
111
+ }
112
+ return map;
113
+ };
114
+ var collectBody = (req) => new Promise((resolve2, reject) => {
115
+ const chunks = [];
116
+ req.on("data", (c) => chunks.push(c));
117
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
118
+ req.on("error", reject);
119
+ });
120
+ var getClientIP = (req) => {
121
+ const forwarded = req.headers["x-forwarded-for"];
122
+ if (typeof forwarded === "string") {
123
+ const first = forwarded.split(",")[0].trim();
124
+ if (isIP(first)) return first;
125
+ }
126
+ const raw = req.socket.remoteAddress ?? "";
127
+ return raw === "::1" || raw === "::ffff:127.0.0.1" ? "127.0.0.1" : raw;
128
+ };
129
+ var matchCIDR = (ip, cidr) => {
130
+ if (!cidr.includes("/")) return ip === cidr;
131
+ const [range, bitsStr] = cidr.split("/");
132
+ const bits = parseInt(bitsStr, 10);
133
+ if (isNaN(bits) || bits < 0 || bits > 32) return false;
134
+ const ipNum = ip.split(".").reduce((acc, oct) => (acc << 8) + parseInt(oct, 10), 0);
135
+ const rangeNum = range.split(".").reduce((acc, oct) => (acc << 8) + parseInt(oct, 10), 0);
136
+ const mask = ~(2 ** (32 - bits) - 1);
137
+ return (ipNum & mask) === (rangeNum & mask);
138
+ };
139
+ var buildCookie = (cookieName, token, secure) => {
140
+ const parts = [
141
+ `${cookieName}=${token}`,
142
+ "HttpOnly",
143
+ "SameSite=Lax",
144
+ "Path=/"
145
+ ];
146
+ if (secure) parts.push("Secure");
147
+ return parts.join("; ");
148
+ };
149
+
150
+ // src/auth-login.html
151
+ var auth_login_default = '<!doctype html>\n<html lang="en">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width,initial-scale=1" />\n <title>Restricted Access</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n body {\n font-family:\n -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n }\n .card {\n background: #fff;\n padding: 2rem;\n border-radius: 8px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n width: 100%;\n max-width: 360px;\n }\n h1 {\n font-size: 1.25rem;\n margin-bottom: 1.5rem;\n text-align: center;\n color: #333;\n }\n label {\n display: block;\n margin-bottom: 0.25rem;\n font-size: 0.875rem;\n color: #555;\n }\n .field {\n margin-bottom: 1rem;\n }\n .input-wrap {\n display: flex;\n align-items: center;\n border: 1px solid #ccc;\n border-radius: 4px;\n background: #fff;\n }\n .input-wrap input {\n flex: 1;\n padding: 0.5rem;\n border: none;\n outline: none;\n font-size: 0.875rem;\n background: transparent;\n }\n .input-wrap button {\n background: transparent;\n border: none;\n padding: 0 0.6rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n color: #999;\n font-size: 1.1rem;\n line-height: 1;\n }\n .input-wrap button:hover {\n color: #333;\n }\n .input-wrap:focus-within {\n border-color: #1877f2;\n box-shadow: 0 0 0 1px #1877f2;\n }\n button[type="submit"] {\n width: 100%;\n padding: 0.5rem;\n background: #1877f2;\n color: #fff;\n border: none;\n border-radius: 4px;\n font-size: 0.875rem;\n cursor: pointer;\n margin-top: 0.5rem;\n }\n button[type="submit"]:hover {\n background: #166fe5;\n }\n .error {\n color: #d32f2f;\n font-size: 0.8rem;\n margin-bottom: 0.75rem;\n text-align: center;\n }\n .logo {\n text-align: center;\n margin-bottom: 0.5rem;\n color: #1877f2;\n }\n .logo svg {\n width: 48px;\n height: 48px;\n }\n .sub {\n text-align: center;\n color: #888;\n font-size: 0.8rem;\n margin-bottom: 1.25rem;\n }\n </style>\n </head>\n <body style="background: {BG_COLOR};{BG_IMAGE}">\n <div class="card">\n <div class="logo">\n <svg\n viewBox="0 0 24 24"\n width="48"\n height="48"\n fill="none"\n stroke="currentColor"\n stroke-width="1.5"\n stroke-linecap="round"\n stroke-linejoin="round"\n >\n <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />\n <path d="m9 12 2 2 4-4" />\n </svg>\n </div>\n <h1>{TITLE}</h1>\n <form id="auth-form">\n <div class="field">\n <label for="u">Username</label>\n <div class="input-wrap">\n <input\n type="text"\n id="u"\n name="username"\n required\n autocomplete="username"\n />\n </div>\n </div>\n <div class="field">\n <label for="p">Password</label>\n <div class="input-wrap">\n <input\n type="password"\n id="p"\n name="password"\n required\n autocomplete="current-password"\n />\n <button\n type="button"\n id="toggle-pw"\n tabindex="-1"\n aria-label="Show password"\n >\n <svg\n viewBox="0 0 24 24"\n width="20"\n height="20"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n >\n <path\n d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"\n />\n <line x1="1" y1="1" x2="23" y2="23" /></svg\n ><svg\n viewBox="0 0 24 24"\n width="20"\n height="20"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n style="display: none"\n >\n <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />\n <circle cx="12" cy="12" r="3" />\n </svg>\n </button>\n </div>\n </div>\n <button type="submit">Sign in</button>\n </form>\n {ERROR_HTML}\n <br/>\n <div class="sub">\n {DESCRIPTION}\n </div>\n </div>\n <script>\n (function () {\n var form = document.getElementById("auth-form");\n var pw = document.getElementById("p");\n var toggle = document.getElementById("toggle-pw");\n toggle.addEventListener("click", function () {\n var show = pw.type === "password";\n pw.type = show ? "text" : "password";\n toggle.querySelector("svg:first-child").style.display = show\n ? "none"\n : "";\n toggle.querySelector("svg:last-child").style.display = show\n ? ""\n : "none";\n toggle.setAttribute(\n "aria-label",\n show ? "Hide password" : "Show password",\n );\n });\n form.addEventListener("submit", function (e) {\n e.preventDefault();\n var fd = new FormData(form);\n var params = new URLSearchParams();\n for (var p of fd) params.append(p[0], p[1]);\n fetch("{LOGIN_PATH}", { method: "POST", body: params }).then(\n function (r) {\n if (r.ok) window.location.href = "/";\n else\n r.text().then(function (html) {\n document.open();\n document.write(html);\n document.close();\n });\n },\n );\n });\n })();\n </script>\n </body>\n</html>\n';
152
+
153
+ // src/login-page.ts
154
+ var renderLoginPage = (loginPath, opts) => {
155
+ const errorHtml = opts?.error ? `<p class="error">${escapeHtml(opts.error)}</p>` : "";
156
+ const bgImage = opts?.backgroundImage ? `background-image: url('${escapeHtml(opts.backgroundImage)}'); background-size: cover; background-position: center; background-repeat: no-repeat;` : "";
157
+ return auth_login_default.replace("{LOGIN_PATH}", loginPath).replace("{ERROR_HTML}", errorHtml).replace("{TITLE}", escapeHtml(opts?.title ?? "vite-plugin-auth")).replace("{DESCRIPTION}", opts?.descriptionHtml ?? "").replace("{BG_COLOR}", opts?.backgroundColor ?? "#f0f2f5").replace("{BG_IMAGE}", bgImage);
158
+ };
159
+ var escapeHtml = (str) => str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
160
+
161
+ // src/middleware.ts
162
+ var createAuthMiddleware = (ctx) => async (req, res, next) => {
163
+ try {
164
+ await handleRequest(req, res, next, ctx);
165
+ } catch (err) {
166
+ console.error("[vite-auth-gate]", err);
167
+ if (!res.writableEnded) {
168
+ res.statusCode = 500;
169
+ res.end("Internal Server Error");
170
+ }
171
+ }
172
+ };
173
+ var sendLoginPage = (res, loginPath, ctx) => {
174
+ res.statusCode = 200;
175
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
176
+ res.end(renderLoginPage(loginPath, ctx.appearance));
177
+ };
178
+ var handleLogin = async (req, res, ctx) => {
179
+ const body = await collectBody(req);
180
+ const params = new URLSearchParams(body);
181
+ const username = params.get("username") ?? "";
182
+ const password = params.get("password") ?? "";
183
+ const user = ctx.users.find((u) => u.username === username);
184
+ const valid = user && safeEqual(user.password, password);
185
+ if (!valid) {
186
+ res.statusCode = 401;
187
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
188
+ res.end(renderLoginPage(ctx.loginPath, { ...ctx.appearance, error: "Invalid username or password" }));
189
+ return;
190
+ }
191
+ const token = ctx.sessions.create(username, ctx.sessionTtl);
192
+ res.statusCode = 302;
193
+ res.setHeader("Location", "/");
194
+ res.setHeader("Set-Cookie", buildCookie(ctx.cookieName, token, ctx.secure));
195
+ res.end();
196
+ };
197
+ var handleRequest = async (req, res, next, ctx) => {
198
+ if (ctx.allowedIPs.length) {
199
+ const clientIP = getClientIP(req);
200
+ const allowed = ctx.allowedIPs.some((entry) => matchCIDR(clientIP, entry));
201
+ if (allowed) return next();
202
+ }
203
+ if (req.method === "POST" && req.url === ctx.loginPath) {
204
+ return handleLogin(req, res, ctx);
205
+ }
206
+ if (req.url === ctx.loginPath) {
207
+ return sendLoginPage(res, ctx.loginPath, ctx);
208
+ }
209
+ const token = parseCookies(req.headers.cookie)[ctx.cookieName];
210
+ if (token && ctx.sessions.get(token)) {
211
+ return next();
212
+ }
213
+ sendLoginPage(res, ctx.loginPath, ctx);
214
+ };
215
+
216
+ // src/config.ts
217
+ import { readFileSync as readFileSync2 } from "fs";
218
+ import { extname, resolve } from "path";
219
+ var MIME_TYPES = {
220
+ ".png": "image/png",
221
+ ".jpg": "image/jpeg",
222
+ ".jpeg": "image/jpeg",
223
+ ".gif": "image/gif",
224
+ ".svg": "image/svg+xml",
225
+ ".webp": "image/webp",
226
+ ".bmp": "image/bmp",
227
+ ".ico": "image/x-icon"
228
+ };
229
+ var resolveBgImage = (image) => {
230
+ if (image.startsWith("data:") || image.startsWith("http://") || image.startsWith("https://")) {
231
+ return image;
232
+ }
233
+ const filePath = resolve(process.cwd(), image);
234
+ const ext = extname(filePath).toLowerCase();
235
+ const mime = MIME_TYPES[ext] || "image/png";
236
+ const data = readFileSync2(filePath);
237
+ return `data:${mime};base64,${data.toString("base64")}`;
238
+ };
239
+ var ensureOpts = (raw = {}) => {
240
+ const users = raw.users?.length ? raw.users : raw.username ? [{ username: raw.username, password: raw.password ?? "" }] : [];
241
+ const a = raw.appearance ?? {};
242
+ return {
243
+ users,
244
+ sessionTtl: (raw.sessionDuration || 3600) * 1e3,
245
+ cookieName: raw.cookieName || "vite_plugin_auth",
246
+ loginPath: raw.loginPath || "/__vite_plugin_auth/login",
247
+ allowedIPs: raw.allowedIPs ?? [],
248
+ sessionFile: raw.sessionFile || resolve(process.cwd(), ".vite-plugin-auth/sessions.json"),
249
+ appearance: {
250
+ title: a.title ?? raw.title ?? "vite-plugin-auth",
251
+ descriptionHtml: a.descriptionHtml ?? raw.description ?? "Password-based access control for your Vite dev server. Ideal for staging, preview deployments, and internal tools that need quick protection without a full authentication system.",
252
+ backgroundColor: a.backgroundColor ?? "#f0f2f5",
253
+ backgroundImage: a.backgroundImage ? resolveBgImage(a.backgroundImage) : ""
254
+ }
255
+ };
256
+ };
257
+
258
+ // src/index.ts
259
+ var auth = (raw = {}) => {
260
+ const cfg = ensureOpts(raw);
261
+ const sessions = new SessionStore(cfg.sessionFile);
262
+ sessions.startCleanup();
263
+ return {
264
+ name: "vite-auth-gate",
265
+ configureServer(server) {
266
+ const secure = !!server.config.server?.https;
267
+ const middleware = createAuthMiddleware({
268
+ sessions,
269
+ users: cfg.users,
270
+ sessionTtl: cfg.sessionTtl,
271
+ cookieName: cfg.cookieName,
272
+ loginPath: cfg.loginPath,
273
+ secure,
274
+ allowedIPs: cfg.allowedIPs,
275
+ appearance: cfg.appearance
276
+ });
277
+ server.middlewares.use(middleware);
278
+ },
279
+ closeBundle() {
280
+ sessions.destroy();
281
+ }
282
+ };
283
+ };
284
+ var index_default = auth;
285
+ export {
286
+ auth,
287
+ index_default as default
288
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "vite-plugin-auth",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Password-based authentication plugin for Vite. Protects dev servers, preview deployments, and internal tools with a login form, session persistence, and IP allowlisting.",
6
+ "keywords": [
7
+ "vite",
8
+ "vite-plugin",
9
+ "auth",
10
+ "authentication",
11
+ "login",
12
+ "password",
13
+ "basic-auth",
14
+ "access-control",
15
+ "dev-server",
16
+ "security"
17
+ ],
18
+ "files": [
19
+ "dist",
20
+ "LICENSE"
21
+ ],
22
+ "license": "MIT",
23
+ "author": "Willyams Yujra <yracnet@gmail.com>",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/yracnet/vite-plugin-auth"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "homepage": "https://github.com/yracnet/vite-plugin-auth",
35
+ "bugs": "https://github.com/yracnet/vite-plugin-auth/issues",
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "tar": "yarn pack",
39
+ "prepublish": "yarn build",
40
+ "prepack": "yarn build"
41
+ },
42
+ "dependencies": {
43
+ },
44
+ "peerDependencies": {
45
+ "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.14.1",
49
+ "globals": "^16.0.0",
50
+ "tsup": "^8.4.0",
51
+ "typescript": "~5.7.2",
52
+ "vite": "^6.2.0"
53
+ }
54
+ }