or3-provider-basic-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.
Files changed (73) hide show
  1. package/README.md +99 -0
  2. package/dist/module.d.mts +5 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +42 -0
  5. package/dist/runtime/components/BasicAuthChangePasswordModal.client.d.vue.ts +12 -0
  6. package/dist/runtime/components/BasicAuthChangePasswordModal.client.vue +133 -0
  7. package/dist/runtime/components/BasicAuthChangePasswordModal.client.vue.d.ts +12 -0
  8. package/dist/runtime/components/BasicAuthRegisterModal.client.d.vue.ts +11 -0
  9. package/dist/runtime/components/BasicAuthRegisterModal.client.vue +164 -0
  10. package/dist/runtime/components/BasicAuthRegisterModal.client.vue.d.ts +11 -0
  11. package/dist/runtime/components/BasicAuthSignInModal.client.d.vue.ts +13 -0
  12. package/dist/runtime/components/BasicAuthSignInModal.client.vue +136 -0
  13. package/dist/runtime/components/BasicAuthSignInModal.client.vue.d.ts +13 -0
  14. package/dist/runtime/components/BasicAuthUserMenu.client.d.vue.ts +12 -0
  15. package/dist/runtime/components/BasicAuthUserMenu.client.vue +106 -0
  16. package/dist/runtime/components/BasicAuthUserMenu.client.vue.d.ts +12 -0
  17. package/dist/runtime/components/SidebarAuthButtonBasic.client.d.vue.ts +2 -0
  18. package/dist/runtime/components/SidebarAuthButtonBasic.client.vue +154 -0
  19. package/dist/runtime/components/SidebarAuthButtonBasic.client.vue.d.ts +2 -0
  20. package/dist/runtime/lib/constants.d.ts +7 -0
  21. package/dist/runtime/lib/constants.js +7 -0
  22. package/dist/runtime/plugins/auth-status.client.d.ts +2 -0
  23. package/dist/runtime/plugins/auth-status.client.js +38 -0
  24. package/dist/runtime/plugins/basic-auth-ui.client.d.ts +2 -0
  25. package/dist/runtime/plugins/basic-auth-ui.client.js +46 -0
  26. package/dist/runtime/server/admin/adapters/auth-basic-auth.d.ts +2 -0
  27. package/dist/runtime/server/admin/adapters/auth-basic-auth.js +34 -0
  28. package/dist/runtime/server/api/basic-auth/_helpers.d.ts +6 -0
  29. package/dist/runtime/server/api/basic-auth/_helpers.js +26 -0
  30. package/dist/runtime/server/api/basic-auth/change-password.post.d.ts +8 -0
  31. package/dist/runtime/server/api/basic-auth/change-password.post.js +49 -0
  32. package/dist/runtime/server/api/basic-auth/refresh.post.d.ts +8 -0
  33. package/dist/runtime/server/api/basic-auth/refresh.post.js +78 -0
  34. package/dist/runtime/server/api/basic-auth/register.post.d.ts +8 -0
  35. package/dist/runtime/server/api/basic-auth/register.post.js +112 -0
  36. package/dist/runtime/server/api/basic-auth/sign-in.post.d.ts +8 -0
  37. package/dist/runtime/server/api/basic-auth/sign-in.post.js +75 -0
  38. package/dist/runtime/server/api/basic-auth/sign-out.post.d.ts +8 -0
  39. package/dist/runtime/server/api/basic-auth/sign-out.post.js +37 -0
  40. package/dist/runtime/server/auth/basic-auth-provider.d.ts +2 -0
  41. package/dist/runtime/server/auth/basic-auth-provider.js +41 -0
  42. package/dist/runtime/server/auth/index.d.ts +1 -0
  43. package/dist/runtime/server/auth/index.js +1 -0
  44. package/dist/runtime/server/db/client.d.ts +3 -0
  45. package/dist/runtime/server/db/client.js +106 -0
  46. package/dist/runtime/server/db/schema.d.ts +21 -0
  47. package/dist/runtime/server/db/schema.js +0 -0
  48. package/dist/runtime/server/lib/config.d.ts +22 -0
  49. package/dist/runtime/server/lib/config.js +94 -0
  50. package/dist/runtime/server/lib/cookies.d.ts +4 -0
  51. package/dist/runtime/server/lib/cookies.js +42 -0
  52. package/dist/runtime/server/lib/errors.d.ts +4 -0
  53. package/dist/runtime/server/lib/errors.js +25 -0
  54. package/dist/runtime/server/lib/jwt.d.ts +37 -0
  55. package/dist/runtime/server/lib/jwt.js +77 -0
  56. package/dist/runtime/server/lib/password.d.ts +2 -0
  57. package/dist/runtime/server/lib/password.js +8 -0
  58. package/dist/runtime/server/lib/rate-limit.d.ts +6 -0
  59. package/dist/runtime/server/lib/rate-limit.js +163 -0
  60. package/dist/runtime/server/lib/request-identity.d.ts +16 -0
  61. package/dist/runtime/server/lib/request-identity.js +32 -0
  62. package/dist/runtime/server/lib/request-security.d.ts +2 -0
  63. package/dist/runtime/server/lib/request-security.js +37 -0
  64. package/dist/runtime/server/lib/session-store.d.ts +46 -0
  65. package/dist/runtime/server/lib/session-store.js +190 -0
  66. package/dist/runtime/server/plugins/register.d.ts +2 -0
  67. package/dist/runtime/server/plugins/register.js +63 -0
  68. package/dist/runtime/server/token-broker/basic-auth-token-broker.d.ts +6 -0
  69. package/dist/runtime/server/token-broker/basic-auth-token-broker.js +8 -0
  70. package/dist/runtime/server/token-broker/index.d.ts +1 -0
  71. package/dist/runtime/server/token-broker/index.js +1 -0
  72. package/dist/types.d.mts +7 -0
  73. package/package.json +70 -0
@@ -0,0 +1,77 @@
1
+ import { randomUUID, createHash } from "node:crypto";
2
+ import { getCookie } from "h3";
3
+ import jwt from "jsonwebtoken";
4
+ import { ACCESS_COOKIE_NAME, REFRESH_COOKIE_NAME } from "../../lib/constants.js";
5
+ import { getBasicAuthConfig } from "./config.js";
6
+ export async function signAccessToken(input) {
7
+ const config = getBasicAuthConfig();
8
+ return jwt.sign(
9
+ {
10
+ sid: input.sid,
11
+ ver: input.ver,
12
+ typ: "access",
13
+ email: input.email,
14
+ display_name: input.display_name ?? null
15
+ },
16
+ config.jwtSecret,
17
+ {
18
+ expiresIn: config.accessTtlSeconds,
19
+ subject: input.sub
20
+ }
21
+ );
22
+ }
23
+ export async function signRefreshToken(input) {
24
+ const config = getBasicAuthConfig();
25
+ return jwt.sign(
26
+ {
27
+ sid: input.sid,
28
+ ver: input.ver,
29
+ typ: "refresh",
30
+ jti: randomUUID()
31
+ },
32
+ config.refreshSecret,
33
+ {
34
+ expiresIn: config.refreshTtlSeconds,
35
+ subject: input.sub
36
+ }
37
+ );
38
+ }
39
+ function isAccessClaims(payload) {
40
+ return payload.typ === "access" && typeof payload.sub === "string" && typeof payload.sid === "string" && typeof payload.ver === "number";
41
+ }
42
+ function isRefreshClaims(payload) {
43
+ return payload.typ === "refresh" && typeof payload.sub === "string" && typeof payload.sid === "string" && typeof payload.ver === "number" && typeof payload.jti === "string";
44
+ }
45
+ export async function verifyAccessToken(token) {
46
+ try {
47
+ const config = getBasicAuthConfig();
48
+ const payload = jwt.verify(token, config.jwtSecret, {
49
+ algorithms: ["HS256"]
50
+ });
51
+ if (!isAccessClaims(payload)) return null;
52
+ return payload;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ export async function verifyRefreshToken(token) {
58
+ try {
59
+ const config = getBasicAuthConfig();
60
+ const payload = jwt.verify(token, config.refreshSecret, {
61
+ algorithms: ["HS256"]
62
+ });
63
+ if (!isRefreshClaims(payload)) return null;
64
+ return payload;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+ export function getAccessTokenFromEvent(event) {
70
+ return getCookie(event, ACCESS_COOKIE_NAME) ?? null;
71
+ }
72
+ export function getRefreshTokenFromEvent(event) {
73
+ return getCookie(event, REFRESH_COOKIE_NAME) ?? null;
74
+ }
75
+ export function hashRefreshToken(token) {
76
+ return createHash("sha256").update(token).digest("hex");
77
+ }
@@ -0,0 +1,2 @@
1
+ export declare function hashPassword(password: string): Promise<string>;
2
+ export declare function verifyPassword(password: string, hash: string): Promise<boolean>;
@@ -0,0 +1,8 @@
1
+ import bcrypt from "bcryptjs";
2
+ const PASSWORD_COST = 12;
3
+ export async function hashPassword(password) {
4
+ return await bcrypt.hash(password, PASSWORD_COST);
5
+ }
6
+ export async function verifyPassword(password, hash) {
7
+ return await bcrypt.compare(password, hash);
8
+ }
@@ -0,0 +1,6 @@
1
+ import { type H3Event } from 'h3';
2
+ type Operation = 'basic-auth:sign-in' | 'basic-auth:register' | 'basic-auth:refresh' | 'basic-auth:sign-out' | 'basic-auth:change-password';
3
+ export declare function enforceBasicAuthRateLimit(event: H3Event, operation: Operation): void;
4
+ export declare function resetBasicAuthRateLimitStore(): void;
5
+ export declare function getBasicAuthRateLimitStoreSizeForTests(): number;
6
+ export {};
@@ -0,0 +1,163 @@
1
+ import { createError, setResponseHeader } from "h3";
2
+ import {
3
+ getClientIp as getProxyClientIp,
4
+ normalizeProxyTrustConfig
5
+ } from "./request-identity.js";
6
+ import { getBasicAuthDb } from "../db/client.js";
7
+ const RULES = {
8
+ "basic-auth:sign-in": { windowMs: 6e4, maxRequests: 20 },
9
+ "basic-auth:register": { windowMs: 6e4, maxRequests: 10 },
10
+ "basic-auth:refresh": { windowMs: 6e4, maxRequests: 120 },
11
+ "basic-auth:sign-out": { windowMs: 6e4, maxRequests: 60 },
12
+ "basic-auth:change-password": { windowMs: 6e4, maxRequests: 20 }
13
+ };
14
+ const store = /* @__PURE__ */ new Map();
15
+ const MAX_WINDOW_MS = Math.max(...Object.values(RULES).map((rule) => rule.windowMs));
16
+ const SWEEP_INTERVAL_MS = 6e4;
17
+ let lastSweepAt = 0;
18
+ const RATE_LIMIT_BACKEND_ENV = "OR3_BASIC_AUTH_RATE_LIMIT_BACKEND";
19
+ const DEFAULT_RATE_LIMIT_BACKEND = "sqlite";
20
+ function getClientIp(event) {
21
+ const runtimeConfig = useRuntimeConfig(event);
22
+ const proxyConfig = normalizeProxyTrustConfig(runtimeConfig.security?.proxy);
23
+ return getProxyClientIp(event, proxyConfig) ?? event.node.req.socket.remoteAddress ?? "unknown";
24
+ }
25
+ function getKey(subject, operation) {
26
+ return `${subject}:${operation}`;
27
+ }
28
+ function getWindowKey(subject, operation, windowStartMs) {
29
+ return `${subject}:${operation}:${windowStartMs}`;
30
+ }
31
+ function resolveRateLimitBackend() {
32
+ const raw = (process.env[RATE_LIMIT_BACKEND_ENV] ?? "").trim().toLowerCase();
33
+ if (raw === "memory") return "memory";
34
+ return DEFAULT_RATE_LIMIT_BACKEND;
35
+ }
36
+ function pruneRateLimitStore(now) {
37
+ if (now - lastSweepAt < SWEEP_INTERVAL_MS) {
38
+ return;
39
+ }
40
+ const cutoff = now - MAX_WINDOW_MS;
41
+ for (const [key, entry] of store) {
42
+ const recent = entry.timestamps.filter((timestamp) => timestamp > cutoff);
43
+ if (recent.length === 0) {
44
+ store.delete(key);
45
+ continue;
46
+ }
47
+ entry.timestamps = recent;
48
+ }
49
+ lastSweepAt = now;
50
+ }
51
+ function pruneSqliteRateLimitStore(now) {
52
+ if (now - lastSweepAt < SWEEP_INTERVAL_MS) {
53
+ return;
54
+ }
55
+ getBasicAuthDb().prepare("DELETE FROM basic_auth_rate_limits WHERE updated_at <= ?").run(now - MAX_WINDOW_MS);
56
+ lastSweepAt = now;
57
+ }
58
+ function enforceInMemoryRateLimit(event, operation, now) {
59
+ pruneRateLimitStore(now);
60
+ const subject = getClientIp(event);
61
+ const key = getKey(subject, operation);
62
+ const rule = RULES[operation];
63
+ const windowStart = now - rule.windowMs;
64
+ const current = store.get(key) ?? { timestamps: [] };
65
+ const recent = current.timestamps.filter((timestamp) => timestamp > windowStart);
66
+ if (recent.length >= rule.maxRequests) {
67
+ const oldest = recent[0] ?? now;
68
+ const retryAfterMs = Math.max(0, oldest + rule.windowMs - now);
69
+ const retryAfterSeconds = Math.ceil(retryAfterMs / 1e3);
70
+ setResponseHeader(event, "Retry-After", retryAfterSeconds);
71
+ setResponseHeader(event, "X-RateLimit-Limit", String(rule.maxRequests));
72
+ setResponseHeader(event, "X-RateLimit-Remaining", "0");
73
+ throw createError({
74
+ statusCode: 429,
75
+ statusMessage: "Too many requests"
76
+ });
77
+ }
78
+ recent.push(now);
79
+ store.set(key, { timestamps: recent });
80
+ setResponseHeader(event, "X-RateLimit-Limit", String(rule.maxRequests));
81
+ setResponseHeader(
82
+ event,
83
+ "X-RateLimit-Remaining",
84
+ String(Math.max(0, rule.maxRequests - recent.length))
85
+ );
86
+ }
87
+ function enforceSqliteRateLimit(event, operation, now) {
88
+ pruneSqliteRateLimitStore(now);
89
+ const subject = getClientIp(event);
90
+ const rule = RULES[operation];
91
+ const windowStart = Math.floor(now / rule.windowMs) * rule.windowMs;
92
+ const windowKey = getWindowKey(subject, operation, windowStart);
93
+ const db = getBasicAuthDb();
94
+ const loadCount = db.prepare(
95
+ "SELECT request_count FROM basic_auth_rate_limits WHERE key = ?"
96
+ );
97
+ const insertCount = db.prepare(`
98
+ INSERT INTO basic_auth_rate_limits (key, subject, operation, window_started_at, request_count, updated_at)
99
+ VALUES (?, ?, ?, ?, 1, ?)
100
+ `);
101
+ const incrementCount = db.prepare(`
102
+ UPDATE basic_auth_rate_limits
103
+ SET request_count = request_count + 1, updated_at = ?
104
+ WHERE key = ?
105
+ `);
106
+ const evaluate = db.transaction(() => {
107
+ const existing = loadCount.get(windowKey);
108
+ if (!existing) {
109
+ insertCount.run(windowKey, subject, operation, windowStart, now);
110
+ return { blocked: false, requestCount: 1 };
111
+ }
112
+ if (existing.request_count >= rule.maxRequests) {
113
+ return { blocked: true, requestCount: existing.request_count };
114
+ }
115
+ incrementCount.run(now, windowKey);
116
+ return { blocked: false, requestCount: existing.request_count + 1 };
117
+ });
118
+ const result = evaluate.immediate();
119
+ if (result.blocked) {
120
+ const retryAfterMs = Math.max(0, windowStart + rule.windowMs - now);
121
+ const retryAfterSeconds = Math.ceil(retryAfterMs / 1e3);
122
+ setResponseHeader(event, "Retry-After", retryAfterSeconds);
123
+ setResponseHeader(event, "X-RateLimit-Limit", String(rule.maxRequests));
124
+ setResponseHeader(event, "X-RateLimit-Remaining", "0");
125
+ throw createError({
126
+ statusCode: 429,
127
+ statusMessage: "Too many requests"
128
+ });
129
+ }
130
+ setResponseHeader(event, "X-RateLimit-Limit", String(rule.maxRequests));
131
+ setResponseHeader(
132
+ event,
133
+ "X-RateLimit-Remaining",
134
+ String(Math.max(0, rule.maxRequests - result.requestCount))
135
+ );
136
+ }
137
+ export function enforceBasicAuthRateLimit(event, operation) {
138
+ const now = Date.now();
139
+ if (resolveRateLimitBackend() === "memory") {
140
+ enforceInMemoryRateLimit(event, operation, now);
141
+ return;
142
+ }
143
+ enforceSqliteRateLimit(event, operation, now);
144
+ }
145
+ export function resetBasicAuthRateLimitStore() {
146
+ store.clear();
147
+ lastSweepAt = 0;
148
+ try {
149
+ getBasicAuthDb().prepare("DELETE FROM basic_auth_rate_limits").run();
150
+ } catch {
151
+ }
152
+ }
153
+ export function getBasicAuthRateLimitStoreSizeForTests() {
154
+ if (resolveRateLimitBackend() === "memory") {
155
+ return store.size;
156
+ }
157
+ try {
158
+ const row = getBasicAuthDb().prepare("SELECT COUNT(*) as count FROM basic_auth_rate_limits").get();
159
+ return row?.count ?? 0;
160
+ } catch {
161
+ return 0;
162
+ }
163
+ }
@@ -0,0 +1,16 @@
1
+ import { type H3Event } from 'h3';
2
+ export type ForwardedForHeader = 'x-forwarded-for' | 'x-real-ip';
3
+ export type ForwardedHostHeader = 'x-forwarded-host';
4
+ export interface ProxyTrustConfig {
5
+ trustProxy: boolean;
6
+ forwardedForHeader: ForwardedForHeader;
7
+ forwardedHostHeader: ForwardedHostHeader;
8
+ }
9
+ export interface ProxyTrustConfigInput {
10
+ trustProxy?: boolean;
11
+ forwardedForHeader?: string;
12
+ forwardedHostHeader?: string;
13
+ }
14
+ export declare function normalizeProxyTrustConfig(input?: ProxyTrustConfigInput): ProxyTrustConfig;
15
+ export declare function getClientIp(event: H3Event, config: ProxyTrustConfig): string | null;
16
+ export declare function getProxyRequestHost(event: H3Event, config: ProxyTrustConfig): string | null;
@@ -0,0 +1,32 @@
1
+ import { getRequestHeader } from "h3";
2
+ const IP_REGEX = /^(\d{1,3}\.){3}\d{1,3}$|^[0-9a-fA-F:.]+$/;
3
+ export function normalizeProxyTrustConfig(input) {
4
+ return {
5
+ trustProxy: input?.trustProxy === true,
6
+ forwardedForHeader: input?.forwardedForHeader === "x-real-ip" ? "x-real-ip" : "x-forwarded-for",
7
+ forwardedHostHeader: "x-forwarded-host"
8
+ };
9
+ }
10
+ function parseForwardedFor(value) {
11
+ const firstIp = value.split(",")[0]?.trim();
12
+ if (!firstIp) return null;
13
+ if (!IP_REGEX.test(firstIp)) return null;
14
+ return firstIp;
15
+ }
16
+ export function getClientIp(event, config) {
17
+ if (config.trustProxy) {
18
+ const forwarded = getRequestHeader(event, config.forwardedForHeader);
19
+ if (!forwarded) return null;
20
+ return parseForwardedFor(forwarded);
21
+ }
22
+ return event.node.req.socket.remoteAddress ?? null;
23
+ }
24
+ export function getProxyRequestHost(event, config) {
25
+ if (config.trustProxy) {
26
+ const forwardedHost = getRequestHeader(event, config.forwardedHostHeader);
27
+ const firstHost = forwardedHost?.split(",")[0]?.trim().toLowerCase();
28
+ return firstHost && firstHost.length > 0 ? firstHost : null;
29
+ }
30
+ const host = getRequestHeader(event, "host");
31
+ return host ? host.trim().toLowerCase() : null;
32
+ }
@@ -0,0 +1,2 @@
1
+ import { type H3Event } from 'h3';
2
+ export declare function enforceMutationOriginPolicy(event: H3Event): void;
@@ -0,0 +1,37 @@
1
+ import { createError, getRequestHeader } from "h3";
2
+ import { getProxyRequestHost, normalizeProxyTrustConfig } from "./request-identity.js";
3
+ function getOriginHost(originOrReferer) {
4
+ try {
5
+ return new URL(originOrReferer).host.toLowerCase();
6
+ } catch {
7
+ return null;
8
+ }
9
+ }
10
+ function normalizeHost(host) {
11
+ return host.trim().toLowerCase();
12
+ }
13
+ function getRequestHost(event) {
14
+ const runtimeConfig = useRuntimeConfig(event);
15
+ const proxyConfig = normalizeProxyTrustConfig(runtimeConfig.security?.proxy);
16
+ return getProxyRequestHost(event, proxyConfig);
17
+ }
18
+ function isSafeMethod(method) {
19
+ const normalized = (method ?? "GET").toUpperCase();
20
+ return normalized === "GET" || normalized === "HEAD" || normalized === "OPTIONS";
21
+ }
22
+ export function enforceMutationOriginPolicy(event) {
23
+ if (isSafeMethod(event.method)) return;
24
+ const originHeader = getRequestHeader(event, "origin") ?? getRequestHeader(event, "referer");
25
+ if (!originHeader) return;
26
+ const originHost = getOriginHost(originHeader);
27
+ if (!originHost) {
28
+ throw createError({ statusCode: 403, statusMessage: "Forbidden" });
29
+ }
30
+ const requestHost = getRequestHost(event);
31
+ if (!requestHost) {
32
+ throw createError({ statusCode: 403, statusMessage: "Forbidden" });
33
+ }
34
+ if (normalizeHost(originHost) !== normalizeHost(requestHost)) {
35
+ throw createError({ statusCode: 403, statusMessage: "Forbidden" });
36
+ }
37
+ }
@@ -0,0 +1,46 @@
1
+ import { type H3Event } from 'h3';
2
+ import type { BasicAuthAccount, BasicAuthSession } from '../db/schema.js';
3
+ export interface CreateAccountInput {
4
+ email: string;
5
+ passwordHash: string;
6
+ displayName?: string | null;
7
+ }
8
+ export interface SessionMetadata {
9
+ ipAddress?: string | null;
10
+ userAgent?: string | null;
11
+ }
12
+ export interface CreateSessionInput {
13
+ accountId: string;
14
+ sessionId: string;
15
+ refreshTokenHash: string;
16
+ expiresAtMs: number;
17
+ rotatedFromSessionId?: string | null;
18
+ metadata?: SessionMetadata;
19
+ }
20
+ export interface RotationInput {
21
+ currentSessionId: string;
22
+ currentRefreshHash: string;
23
+ newSessionId: string;
24
+ newRefreshHash: string;
25
+ newExpiresAtMs: number;
26
+ metadata?: SessionMetadata;
27
+ }
28
+ export interface RotationResult {
29
+ ok: boolean;
30
+ reason?: 'not_found' | 'expired' | 'revoked' | 'replayed';
31
+ accountId?: string;
32
+ sessionId?: string;
33
+ }
34
+ export declare function normalizeEmail(email: string): string;
35
+ export declare function getSessionMetadataFromEvent(event: H3Event): SessionMetadata;
36
+ export declare function findAccountByEmail(email: string): BasicAuthAccount | null;
37
+ export declare function findAccountById(id: string): BasicAuthAccount | null;
38
+ export declare function createAccount(input: CreateAccountInput): BasicAuthAccount;
39
+ export declare function ensureBootstrapAccount(input: CreateAccountInput): BasicAuthAccount;
40
+ export declare function createAuthSession(input: CreateSessionInput): BasicAuthSession;
41
+ export declare function findSessionById(sessionId: string): BasicAuthSession | null;
42
+ export declare function revokeSessionById(sessionId: string, revokedAtMs?: number): void;
43
+ export declare function revokeAllSessionsForAccount(accountId: string, revokedAtMs?: number): void;
44
+ export declare function rotateSession(input: RotationInput): RotationResult;
45
+ export declare function updatePasswordAndRevokeSessions(accountId: string, newPasswordHash: string): void;
46
+ export declare function isSessionUsable(session: BasicAuthSession | null): boolean;
@@ -0,0 +1,190 @@
1
+ import { randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { getRequestHeader } from "h3";
3
+ import { getBasicAuthDb } from "../db/client.js";
4
+ import {
5
+ getClientIp as getProxyClientIp,
6
+ normalizeProxyTrustConfig
7
+ } from "./request-identity.js";
8
+ export function normalizeEmail(email) {
9
+ return email.trim().toLowerCase();
10
+ }
11
+ function toHexBuffer(value) {
12
+ return Buffer.from(value, "hex");
13
+ }
14
+ function safeHexEquals(left, right) {
15
+ if (left.length !== right.length) return false;
16
+ const leftBuffer = toHexBuffer(left);
17
+ const rightBuffer = toHexBuffer(right);
18
+ if (leftBuffer.length !== rightBuffer.length) return false;
19
+ return timingSafeEqual(leftBuffer, rightBuffer);
20
+ }
21
+ export function getSessionMetadataFromEvent(event) {
22
+ const proxyConfig = normalizeProxyTrustConfig(useRuntimeConfig(event).security?.proxy);
23
+ return {
24
+ ipAddress: getProxyClientIp(event, proxyConfig) ?? event.node.req.socket.remoteAddress ?? null,
25
+ userAgent: getRequestHeader(event, "user-agent") ?? null
26
+ };
27
+ }
28
+ export function findAccountByEmail(email) {
29
+ const db = getBasicAuthDb();
30
+ const row = db.prepare("SELECT * FROM basic_auth_accounts WHERE email = ? LIMIT 1").get(normalizeEmail(email));
31
+ return row ?? null;
32
+ }
33
+ export function findAccountById(id) {
34
+ const db = getBasicAuthDb();
35
+ const row = db.prepare("SELECT * FROM basic_auth_accounts WHERE id = ? LIMIT 1").get(id);
36
+ return row ?? null;
37
+ }
38
+ export function createAccount(input) {
39
+ const db = getBasicAuthDb();
40
+ const now = Date.now();
41
+ const account = {
42
+ id: randomUUID(),
43
+ email: normalizeEmail(input.email),
44
+ password_hash: input.passwordHash,
45
+ display_name: input.displayName ?? null,
46
+ token_version: 0,
47
+ created_at: now,
48
+ updated_at: now
49
+ };
50
+ db.prepare(
51
+ `INSERT INTO basic_auth_accounts (
52
+ id, email, password_hash, display_name, token_version, created_at, updated_at
53
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`
54
+ ).run(
55
+ account.id,
56
+ account.email,
57
+ account.password_hash,
58
+ account.display_name,
59
+ account.token_version,
60
+ account.created_at,
61
+ account.updated_at
62
+ );
63
+ return account;
64
+ }
65
+ export function ensureBootstrapAccount(input) {
66
+ const existing = findAccountByEmail(input.email);
67
+ if (existing) return existing;
68
+ return createAccount(input);
69
+ }
70
+ export function createAuthSession(input) {
71
+ const db = getBasicAuthDb();
72
+ const createdAt = Date.now();
73
+ const row = {
74
+ id: input.sessionId,
75
+ account_id: input.accountId,
76
+ refresh_token_hash: input.refreshTokenHash,
77
+ expires_at: input.expiresAtMs,
78
+ revoked_at: null,
79
+ created_at: createdAt,
80
+ rotated_from_session_id: input.rotatedFromSessionId ?? null,
81
+ replaced_by_session_id: null,
82
+ ip_address: input.metadata?.ipAddress ?? null,
83
+ user_agent: input.metadata?.userAgent ?? null
84
+ };
85
+ db.prepare(
86
+ `INSERT INTO basic_auth_sessions (
87
+ id, account_id, refresh_token_hash, expires_at, revoked_at, created_at,
88
+ rotated_from_session_id, replaced_by_session_id, ip_address, user_agent
89
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
90
+ ).run(
91
+ row.id,
92
+ row.account_id,
93
+ row.refresh_token_hash,
94
+ row.expires_at,
95
+ row.revoked_at,
96
+ row.created_at,
97
+ row.rotated_from_session_id,
98
+ row.replaced_by_session_id,
99
+ row.ip_address,
100
+ row.user_agent
101
+ );
102
+ return row;
103
+ }
104
+ export function findSessionById(sessionId) {
105
+ const db = getBasicAuthDb();
106
+ const row = db.prepare("SELECT * FROM basic_auth_sessions WHERE id = ? LIMIT 1").get(sessionId);
107
+ return row ?? null;
108
+ }
109
+ export function revokeSessionById(sessionId, revokedAtMs = Date.now()) {
110
+ const db = getBasicAuthDb();
111
+ db.prepare(
112
+ "UPDATE basic_auth_sessions SET revoked_at = COALESCE(revoked_at, ?) WHERE id = ?"
113
+ ).run(revokedAtMs, sessionId);
114
+ }
115
+ export function revokeAllSessionsForAccount(accountId, revokedAtMs = Date.now()) {
116
+ const db = getBasicAuthDb();
117
+ db.prepare(
118
+ "UPDATE basic_auth_sessions SET revoked_at = COALESCE(revoked_at, ?) WHERE account_id = ?"
119
+ ).run(revokedAtMs, accountId);
120
+ }
121
+ export function rotateSession(input) {
122
+ const db = getBasicAuthDb();
123
+ const now = Date.now();
124
+ const currentSession = findSessionById(input.currentSessionId);
125
+ if (!currentSession) {
126
+ return { ok: false, reason: "not_found" };
127
+ }
128
+ if (currentSession.expires_at <= now) {
129
+ revokeSessionById(currentSession.id, now);
130
+ return { ok: false, reason: "expired" };
131
+ }
132
+ const isSameTokenHash = safeHexEquals(
133
+ currentSession.refresh_token_hash,
134
+ input.currentRefreshHash
135
+ );
136
+ if (currentSession.revoked_at !== null) {
137
+ const replayedRotatedToken = currentSession.replaced_by_session_id !== null && isSameTokenHash;
138
+ if (replayedRotatedToken) {
139
+ revokeAllSessionsForAccount(currentSession.account_id, now);
140
+ return {
141
+ ok: false,
142
+ reason: "replayed"
143
+ };
144
+ }
145
+ return { ok: false, reason: "revoked" };
146
+ }
147
+ if (!isSameTokenHash) {
148
+ revokeAllSessionsForAccount(currentSession.account_id, now);
149
+ return {
150
+ ok: false,
151
+ reason: "replayed"
152
+ };
153
+ }
154
+ db.transaction(() => {
155
+ createAuthSession({
156
+ accountId: currentSession.account_id,
157
+ sessionId: input.newSessionId,
158
+ refreshTokenHash: input.newRefreshHash,
159
+ expiresAtMs: input.newExpiresAtMs,
160
+ rotatedFromSessionId: currentSession.id,
161
+ metadata: input.metadata
162
+ });
163
+ db.prepare(
164
+ "UPDATE basic_auth_sessions SET revoked_at = ?, replaced_by_session_id = ? WHERE id = ?"
165
+ ).run(now, input.newSessionId, currentSession.id);
166
+ })();
167
+ return {
168
+ ok: true,
169
+ accountId: currentSession.account_id,
170
+ sessionId: input.newSessionId
171
+ };
172
+ }
173
+ export function updatePasswordAndRevokeSessions(accountId, newPasswordHash) {
174
+ const db = getBasicAuthDb();
175
+ const now = Date.now();
176
+ db.transaction(() => {
177
+ db.prepare(
178
+ `UPDATE basic_auth_accounts
179
+ SET password_hash = ?, token_version = token_version + 1, updated_at = ?
180
+ WHERE id = ?`
181
+ ).run(newPasswordHash, now, accountId);
182
+ revokeAllSessionsForAccount(accountId, now);
183
+ })();
184
+ }
185
+ export function isSessionUsable(session) {
186
+ if (!session) return false;
187
+ if (session.revoked_at !== null) return false;
188
+ if (session.expires_at <= Date.now()) return false;
189
+ return true;
190
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,63 @@
1
+ import { BASIC_AUTH_PROVIDER_ID } from "../../lib/constants.js";
2
+ import { registerAuthProvider } from "~~/server/auth/registry";
3
+ import { registerProviderAdminAdapter } from "~~/server/admin/providers/registry";
4
+ import { registerProviderTokenBroker } from "~~/server/auth/token-broker/registry";
5
+ import { basicAuthProvider } from "../auth/basic-auth-provider.js";
6
+ import { createBasicAuthTokenBroker } from "../token-broker/basic-auth-token-broker.js";
7
+ import {
8
+ BASIC_AUTH_INSECURE_DEV_ESCAPE_HATCH_ENV,
9
+ getBasicAuthConfig,
10
+ isBasicAuthInsecureDevEscapeHatchEnabled,
11
+ validateBasicAuthConfig
12
+ } from "../lib/config.js";
13
+ import { hashPassword } from "../lib/password.js";
14
+ import { ensureBootstrapAccount, findAccountByEmail } from "../lib/session-store.js";
15
+ import { basicAuthAdminAdapter } from "../admin/adapters/auth-basic-auth.js";
16
+ async function ensureBootstrapUserIfConfigured() {
17
+ const config = getBasicAuthConfig();
18
+ const email = config.bootstrapEmail?.trim();
19
+ const password = config.bootstrapPassword;
20
+ if (!email || !password) {
21
+ return;
22
+ }
23
+ if (findAccountByEmail(email)) {
24
+ return;
25
+ }
26
+ const passwordHash = await hashPassword(password);
27
+ ensureBootstrapAccount({
28
+ email,
29
+ passwordHash,
30
+ displayName: email
31
+ });
32
+ }
33
+ export default defineNitroPlugin(async () => {
34
+ const runtimeConfig = useRuntimeConfig();
35
+ const diagnostics = validateBasicAuthConfig(runtimeConfig);
36
+ for (const warning of diagnostics.warnings) {
37
+ console.warn(`[basic-auth] ${warning}`);
38
+ }
39
+ if (!diagnostics.config.enabled) {
40
+ return;
41
+ }
42
+ if (diagnostics.config.providerId !== BASIC_AUTH_PROVIDER_ID) {
43
+ return;
44
+ }
45
+ if (!diagnostics.isValid) {
46
+ const message = `${diagnostics.errors.join(" ")} Install/configure basic-auth provider env values and restart.`;
47
+ if (isBasicAuthInsecureDevEscapeHatchEnabled() && !diagnostics.config.strict) {
48
+ console.warn(
49
+ `[basic-auth] ${message} Startup continues because ${BASIC_AUTH_INSECURE_DEV_ESCAPE_HATCH_ENV}=true (development only).`
50
+ );
51
+ return;
52
+ }
53
+ throw new Error(message);
54
+ }
55
+ await ensureBootstrapUserIfConfigured();
56
+ registerAuthProvider({
57
+ id: BASIC_AUTH_PROVIDER_ID,
58
+ order: 100,
59
+ create: () => basicAuthProvider
60
+ });
61
+ registerProviderTokenBroker(BASIC_AUTH_PROVIDER_ID, createBasicAuthTokenBroker);
62
+ registerProviderAdminAdapter(basicAuthAdminAdapter);
63
+ });
@@ -0,0 +1,6 @@
1
+ import type { H3Event } from 'h3';
2
+ import type { ProviderTokenBroker, ProviderTokenRequest } from '~~/server/auth/token-broker/types';
3
+ export declare class BasicAuthTokenBroker implements ProviderTokenBroker {
4
+ getProviderToken(_event: H3Event, _req: ProviderTokenRequest): Promise<string | null>;
5
+ }
6
+ export declare function createBasicAuthTokenBroker(): ProviderTokenBroker;
@@ -0,0 +1,8 @@
1
+ export class BasicAuthTokenBroker {
2
+ async getProviderToken(_event, _req) {
3
+ return null;
4
+ }
5
+ }
6
+ export function createBasicAuthTokenBroker() {
7
+ return new BasicAuthTokenBroker();
8
+ }
@@ -0,0 +1 @@
1
+ export { BasicAuthTokenBroker, createBasicAuthTokenBroker } from './basic-auth-token-broker.js';
@@ -0,0 +1 @@
1
+ export { BasicAuthTokenBroker, createBasicAuthTokenBroker } from "./basic-auth-token-broker.js";