gramobase 1.0.0

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,226 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var jwt = require('jsonwebtoken');
5
+ var bcrypt = require('bcryptjs');
6
+ var zod = require('zod');
7
+ var fs = require('fs');
8
+
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ var n = Object.create(null);
12
+ if (e) {
13
+ Object.keys(e).forEach(function (k) {
14
+ if (k !== 'default') {
15
+ var d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: function () { return e[k]; }
19
+ });
20
+ }
21
+ });
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
+ }
26
+
27
+ var jwt__namespace = /*#__PURE__*/_interopNamespace(jwt);
28
+ var bcrypt__namespace = /*#__PURE__*/_interopNamespace(bcrypt);
29
+
30
+ // gramobase — Telegram as your free, infinite backend
31
+
32
+ zod.z.object({
33
+ email: zod.z.string().email(),
34
+ passwordHash: zod.z.string(),
35
+ roles: zod.z.array(zod.z.string()).default(["user"]),
36
+ metadata: zod.z.record(zod.z.unknown()).optional(),
37
+ createdAt: zod.z.string(),
38
+ updatedAt: zod.z.string()
39
+ });
40
+ function resolveJwtSecret(configSecret) {
41
+ if (configSecret) return configSecret;
42
+ if (process.env["JWT_SECRET"]) return process.env["JWT_SECRET"];
43
+ const secretFile = "./jwt_secret.txt";
44
+ if (fs.existsSync(secretFile)) return fs.readFileSync(secretFile, "utf-8").trim();
45
+ const ephemeral = crypto.randomBytes(32).toString("hex");
46
+ console.warn(
47
+ "[gramobase Auth] WARNING: No JWT_SECRET provided. Using an ephemeral random secret. Tokens will be invalidated on restart. Set JWT_SECRET env variable for production!"
48
+ );
49
+ return ephemeral;
50
+ }
51
+ function validatePasswordStrength(password) {
52
+ if (password.length < 8) {
53
+ throw new Error("[Auth] Password must be at least 8 characters long");
54
+ }
55
+ }
56
+ var GramoBaseAuth = class {
57
+ constructor(users, config) {
58
+ this.users = users;
59
+ this.config = config;
60
+ this.resolvedSecret = resolveJwtSecret(config.jwtSecret);
61
+ }
62
+ users;
63
+ config;
64
+ DEFAULT_ROUNDS = 12;
65
+ resolvedSecret;
66
+ // ─── Registration ─────────────────────────────────────────────────────
67
+ async register(email, password, roles = ["user"], metadata) {
68
+ if (!email || typeof email !== "string") {
69
+ throw new Error("[Auth] Invalid email");
70
+ }
71
+ validatePasswordStrength(password);
72
+ const existing = await this.users.findOne({ email: { $eq: email } });
73
+ if (existing) throw new Error("[Auth] Email already registered");
74
+ const passwordHash = await bcrypt__namespace.hash(
75
+ password,
76
+ this.config.bcryptRounds ?? this.DEFAULT_ROUNDS
77
+ );
78
+ const now = (/* @__PURE__ */ new Date()).toISOString();
79
+ const doc = await this.users.insertOne({
80
+ email,
81
+ passwordHash,
82
+ roles,
83
+ metadata,
84
+ createdAt: now,
85
+ updatedAt: now
86
+ });
87
+ const user = doc;
88
+ const session = this.createSession(user);
89
+ await this.config.onSignIn?.(user);
90
+ return { user, session };
91
+ }
92
+ // ─── Login ────────────────────────────────────────────────────────────
93
+ async login(email, password) {
94
+ const doc = await this.users.findOne({ email: { $eq: email } });
95
+ if (!doc) {
96
+ await bcrypt__namespace.compare(password, "$2a$12$invalidhashtopreventtimingattacks");
97
+ throw new Error("[Auth] Invalid credentials");
98
+ }
99
+ const user = doc;
100
+ const valid = await bcrypt__namespace.compare(password, user.passwordHash);
101
+ if (!valid) throw new Error("[Auth] Invalid credentials");
102
+ const session = this.createSession(user);
103
+ await this.config.onSignIn?.(user);
104
+ return { user, session };
105
+ }
106
+ // ─── Token verification ───────────────────────────────────────────────
107
+ verifyToken(token) {
108
+ try {
109
+ const payload = jwt__namespace.verify(token, this.resolvedSecret, {
110
+ algorithms: ["HS256"]
111
+ });
112
+ return payload;
113
+ } catch {
114
+ throw new Error("[Auth] Invalid or expired token");
115
+ }
116
+ }
117
+ // ─── Role-based access ────────────────────────────────────────────────
118
+ requireRole(session, role) {
119
+ if (!session.roles.includes(role) && !session.roles.includes("admin")) {
120
+ throw new Error(`[Auth] Requires role: ${role}`);
121
+ }
122
+ }
123
+ requireAnyRole(session, roles) {
124
+ const hasRole = roles.some(
125
+ (r) => session.roles.includes(r) || session.roles.includes("admin")
126
+ );
127
+ if (!hasRole) {
128
+ throw new Error(`[Auth] Requires one of roles: ${roles.join(", ")}`);
129
+ }
130
+ }
131
+ // ─── Password management ──────────────────────────────────────────────
132
+ async changePassword(userId, oldPassword, newPassword) {
133
+ const doc = await this.users.findById(userId);
134
+ if (!doc) throw new Error("[Auth] User not found");
135
+ const user = doc;
136
+ const valid = await bcrypt__namespace.compare(oldPassword, user.passwordHash);
137
+ if (!valid) throw new Error("[Auth] Old password incorrect");
138
+ validatePasswordStrength(newPassword);
139
+ const newHash = await bcrypt__namespace.hash(
140
+ newPassword,
141
+ this.config.bcryptRounds ?? this.DEFAULT_ROUNDS
142
+ );
143
+ await this.users.findByIdAndUpdate(userId, {
144
+ $set: { passwordHash: newHash, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
145
+ });
146
+ }
147
+ async resetPassword(userId, newPassword) {
148
+ validatePasswordStrength(newPassword);
149
+ const newHash = await bcrypt__namespace.hash(
150
+ newPassword,
151
+ this.config.bcryptRounds ?? this.DEFAULT_ROUNDS
152
+ );
153
+ await this.users.findByIdAndUpdate(userId, {
154
+ $set: { passwordHash: newHash, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
155
+ });
156
+ }
157
+ // ─── User management ─────────────────────────────────────────────────
158
+ async getUserById(id) {
159
+ const doc = await this.users.findById(id);
160
+ return doc ? doc : null;
161
+ }
162
+ async getUserByEmail(email) {
163
+ const doc = await this.users.findOne({ email: { $eq: email } });
164
+ return doc ? doc : null;
165
+ }
166
+ async updateRoles(userId, roles) {
167
+ await this.users.findByIdAndUpdate(userId, {
168
+ $set: { roles, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
169
+ });
170
+ }
171
+ async deleteUser(userId) {
172
+ await this.users.deleteById(userId);
173
+ await this.config.onSignOut?.(userId);
174
+ }
175
+ // ─── Session helpers ──────────────────────────────────────────────────
176
+ createSession(user) {
177
+ const expiresIn = this.config.jwtExpiresIn ?? "7d";
178
+ const payload = {
179
+ sub: user._id,
180
+ userId: user._id,
181
+ roles: user.roles,
182
+ expiresAt: Date.now() + this.parseExpiry(expiresIn)
183
+ };
184
+ const token = jwt__namespace.sign(payload, this.resolvedSecret, {
185
+ expiresIn,
186
+ algorithm: "HS256"
187
+ });
188
+ return { ...payload, token };
189
+ }
190
+ parseExpiry(s) {
191
+ const n = parseInt(s);
192
+ if (s.endsWith("d")) return n * 864e5;
193
+ if (s.endsWith("h")) return n * 36e5;
194
+ if (s.endsWith("m")) return n * 6e4;
195
+ return n * 1e3;
196
+ }
197
+ // ─── Middleware factory (Express/Fastify compatible) ──────────────────
198
+ middleware() {
199
+ return (req, res, next) => {
200
+ const auth = req.headers["authorization"];
201
+ if (!auth?.startsWith("Bearer ")) {
202
+ return res.status(401).json({ error: "Missing token" });
203
+ }
204
+ try {
205
+ req.session = this.verifyToken(auth.slice(7));
206
+ next();
207
+ } catch {
208
+ res.status(401).json({ error: "Unauthorized" });
209
+ }
210
+ };
211
+ }
212
+ requireRoleMiddleware(role) {
213
+ return (req, res, next) => {
214
+ try {
215
+ this.requireRole(req.session, role);
216
+ next();
217
+ } catch {
218
+ res.status(403).json({ error: "Forbidden" });
219
+ }
220
+ };
221
+ }
222
+ };
223
+
224
+ exports.GramoBaseAuth = GramoBaseAuth;
225
+ //# sourceMappingURL=index.cjs.map
226
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/GramoBaseAuth.ts"],"names":["z","existsSync","readFileSync","randomBytes","bcrypt","jwt"],"mappings":"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","file":"index.cjs","sourcesContent":["import { randomUUID, randomBytes } from 'crypto';\nimport * as jwt from 'jsonwebtoken';\nimport * as bcrypt from 'bcryptjs';\nimport { z } from 'zod';\nimport { AuthConfig, User, Session } from '../types/index.js';\nimport { Collection } from '../orm/Collection.js';\nimport { existsSync, readFileSync } from 'fs';\n\nconst UserSchema = z.object({\n email: z.string().email(),\n passwordHash: z.string(),\n roles: z.array(z.string()).default(['user']),\n metadata: z.record(z.unknown()).optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n});\n\n// TODO(security): Consider adding OAuth 2.0 provider support (e.g., Google, GitHub).\n// TODO(security): Consider adding MFA (TOTP/WebAuthn) for high-security deployments.\n// TODO(security): Consider adding pwned-password checking via haveibeenpwned.com API.\n\n/**\n * Resolve JWT secret with multi-tiered fallback:\n * 1. Explicit config value\n * 2. Environment variable\n * 3. Local jwt_secret.txt file\n * 4. Ephemeral random generation with warning (NOT suitable for multi-instance prod)\n *\n * MUST NOT hardcode or use default literal fallbacks.\n */\nfunction resolveJwtSecret(configSecret?: string): string {\n if (configSecret) return configSecret;\n if (process.env['JWT_SECRET']) return process.env['JWT_SECRET'];\n const secretFile = './jwt_secret.txt';\n if (existsSync(secretFile)) return readFileSync(secretFile, 'utf-8').trim();\n const ephemeral = randomBytes(32).toString('hex');\n console.warn(\n '[gramobase Auth] WARNING: No JWT_SECRET provided. Using an ephemeral random secret. ' +\n 'Tokens will be invalidated on restart. Set JWT_SECRET env variable for production!'\n );\n return ephemeral;\n}\n\n/**\n * Validate password strength (min 8 chars, no maximum).\n * Does not require specific character types — users choose their own strong passwords.\n */\nfunction validatePasswordStrength(password: string): void {\n if (password.length < 8) {\n throw new Error('[Auth] Password must be at least 8 characters long');\n }\n // TODO(security): Consider rejecting known breached passwords via HaveIBeenPwned API.\n}\n\nexport class GramoBaseAuth {\n private readonly DEFAULT_ROUNDS = 12;\n private readonly resolvedSecret: string;\n\n constructor(\n private users: Collection<typeof UserSchema>,\n private config: AuthConfig\n ) {\n // Resolve secret at construction time; fail safely if no secret available\n this.resolvedSecret = resolveJwtSecret(config.jwtSecret);\n }\n\n // ─── Registration ─────────────────────────────────────────────────────\n\n async register(\n email: string,\n password: string,\n roles: string[] = ['user'],\n metadata?: Record<string, unknown>\n ): Promise<{ user: User; session: Session }> {\n // Validate email format (basic — Zod schema handles full validation)\n if (!email || typeof email !== 'string') {\n throw new Error('[Auth] Invalid email');\n }\n\n // Validate password strength\n validatePasswordStrength(password);\n\n const existing = await this.users.findOne({ email: { $eq: email } } as any);\n if (existing) throw new Error('[Auth] Email already registered');\n\n const passwordHash = await bcrypt.hash(\n password,\n this.config.bcryptRounds ?? this.DEFAULT_ROUNDS\n );\n\n const now = new Date().toISOString();\n const doc = await this.users.insertOne({\n email,\n passwordHash,\n roles,\n metadata,\n createdAt: now,\n updatedAt: now,\n });\n\n const user = doc as unknown as User;\n const session = this.createSession(user);\n await this.config.onSignIn?.(user);\n\n return { user, session };\n }\n\n // ─── Login ────────────────────────────────────────────────────────────\n\n async login(email: string, password: string): Promise<{ user: User; session: Session }> {\n const doc = await this.users.findOne({ email: { $eq: email } } as any);\n if (!doc) {\n // Use constant-time comparison to avoid timing attacks on email existence\n await bcrypt.compare(password, '$2a$12$invalidhashtopreventtimingattacks');\n throw new Error('[Auth] Invalid credentials');\n }\n\n const user = doc as unknown as User;\n const valid = await bcrypt.compare(password, user.passwordHash);\n if (!valid) throw new Error('[Auth] Invalid credentials');\n\n const session = this.createSession(user);\n await this.config.onSignIn?.(user);\n\n return { user, session };\n }\n\n // ─── Token verification ───────────────────────────────────────────────\n\n verifyToken(token: string): Session {\n try {\n // Hardcode algorithm to prevent algorithm confusion attacks; reject 'none'\n const payload = jwt.verify(token, this.resolvedSecret, {\n algorithms: ['HS256'],\n }) as Session;\n return payload;\n } catch {\n throw new Error('[Auth] Invalid or expired token');\n }\n }\n\n // ─── Role-based access ────────────────────────────────────────────────\n\n requireRole(session: Session, role: string): void {\n if (!session.roles.includes(role) && !session.roles.includes('admin')) {\n throw new Error(`[Auth] Requires role: ${role}`);\n }\n }\n\n requireAnyRole(session: Session, roles: string[]): void {\n const hasRole = roles.some(\n (r) => session.roles.includes(r) || session.roles.includes('admin')\n );\n if (!hasRole) {\n throw new Error(`[Auth] Requires one of roles: ${roles.join(', ')}`);\n }\n }\n\n // ─── Password management ──────────────────────────────────────────────\n\n async changePassword(\n userId: string,\n oldPassword: string,\n newPassword: string\n ): Promise<void> {\n const doc = await this.users.findById(userId);\n if (!doc) throw new Error('[Auth] User not found');\n\n const user = doc as unknown as User;\n const valid = await bcrypt.compare(oldPassword, user.passwordHash);\n if (!valid) throw new Error('[Auth] Old password incorrect');\n\n // Validate new password strength\n validatePasswordStrength(newPassword);\n\n const newHash = await bcrypt.hash(\n newPassword,\n this.config.bcryptRounds ?? this.DEFAULT_ROUNDS\n );\n\n await this.users.findByIdAndUpdate(userId, {\n $set: { passwordHash: newHash, updatedAt: new Date().toISOString() } as any,\n });\n }\n\n async resetPassword(userId: string, newPassword: string): Promise<void> {\n validatePasswordStrength(newPassword);\n const newHash = await bcrypt.hash(\n newPassword,\n this.config.bcryptRounds ?? this.DEFAULT_ROUNDS\n );\n await this.users.findByIdAndUpdate(userId, {\n $set: { passwordHash: newHash, updatedAt: new Date().toISOString() } as any,\n });\n }\n\n // ─── User management ─────────────────────────────────────────────────\n\n async getUserById(id: string): Promise<User | null> {\n const doc = await this.users.findById(id);\n return doc ? (doc as unknown as User) : null;\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n const doc = await this.users.findOne({ email: { $eq: email } } as any);\n return doc ? (doc as unknown as User) : null;\n }\n\n async updateRoles(userId: string, roles: string[]): Promise<void> {\n await this.users.findByIdAndUpdate(userId, {\n $set: { roles, updatedAt: new Date().toISOString() } as any,\n });\n }\n\n async deleteUser(userId: string): Promise<void> {\n await this.users.deleteById(userId);\n await this.config.onSignOut?.(userId);\n }\n\n // ─── Session helpers ──────────────────────────────────────────────────\n\n private createSession(user: User): Session {\n const expiresIn = this.config.jwtExpiresIn ?? '7d';\n const payload: Omit<Session, 'token'> & { sub: string } = {\n sub: user._id,\n userId: user._id,\n roles: user.roles,\n expiresAt: Date.now() + this.parseExpiry(expiresIn),\n };\n\n // Sign with hardcoded HS256 algorithm; 'none' algorithm is never used\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const token = (jwt.sign as any)(payload, this.resolvedSecret, {\n expiresIn,\n algorithm: 'HS256',\n }) as string;\n return { ...payload, token };\n }\n\n private parseExpiry(s: string): number {\n const n = parseInt(s);\n if (s.endsWith('d')) return n * 86400_000;\n if (s.endsWith('h')) return n * 3600_000;\n if (s.endsWith('m')) return n * 60_000;\n return n * 1000;\n }\n\n // ─── Middleware factory (Express/Fastify compatible) ──────────────────\n\n middleware() {\n return (req: any, res: any, next: any) => {\n const auth = req.headers['authorization'] as string | undefined;\n if (!auth?.startsWith('Bearer ')) {\n return res.status(401).json({ error: 'Missing token' });\n }\n try {\n req.session = this.verifyToken(auth.slice(7));\n next();\n } catch {\n // Do NOT expose error details to client; log internally if needed\n res.status(401).json({ error: 'Unauthorized' });\n }\n };\n }\n\n requireRoleMiddleware(role: string) {\n return (req: any, res: any, next: any) => {\n try {\n this.requireRole(req.session, role);\n next();\n } catch {\n res.status(403).json({ error: 'Forbidden' });\n }\n };\n }\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import 'zod';
2
+ import '../BotWorkerPool-9ndHQt2g.cjs';
3
+ export { G as GramoBaseAuth } from '../GramoBaseAuth-CHNn2_e5.cjs';
4
+ import 'node-telegram-bot-api';
5
+ import 'eventemitter3';
@@ -0,0 +1,5 @@
1
+ import 'zod';
2
+ import '../BotWorkerPool-9ndHQt2g.js';
3
+ export { G as GramoBaseAuth } from '../GramoBaseAuth-00fg0u_b.js';
4
+ import 'node-telegram-bot-api';
5
+ import 'eventemitter3';
@@ -0,0 +1,203 @@
1
+ import { randomBytes } from 'crypto';
2
+ import * as jwt from 'jsonwebtoken';
3
+ import * as bcrypt from 'bcryptjs';
4
+ import { z } from 'zod';
5
+ import { existsSync, readFileSync } from 'fs';
6
+
7
+ // gramobase — Telegram as your free, infinite backend
8
+
9
+ z.object({
10
+ email: z.string().email(),
11
+ passwordHash: z.string(),
12
+ roles: z.array(z.string()).default(["user"]),
13
+ metadata: z.record(z.unknown()).optional(),
14
+ createdAt: z.string(),
15
+ updatedAt: z.string()
16
+ });
17
+ function resolveJwtSecret(configSecret) {
18
+ if (configSecret) return configSecret;
19
+ if (process.env["JWT_SECRET"]) return process.env["JWT_SECRET"];
20
+ const secretFile = "./jwt_secret.txt";
21
+ if (existsSync(secretFile)) return readFileSync(secretFile, "utf-8").trim();
22
+ const ephemeral = randomBytes(32).toString("hex");
23
+ console.warn(
24
+ "[gramobase Auth] WARNING: No JWT_SECRET provided. Using an ephemeral random secret. Tokens will be invalidated on restart. Set JWT_SECRET env variable for production!"
25
+ );
26
+ return ephemeral;
27
+ }
28
+ function validatePasswordStrength(password) {
29
+ if (password.length < 8) {
30
+ throw new Error("[Auth] Password must be at least 8 characters long");
31
+ }
32
+ }
33
+ var GramoBaseAuth = class {
34
+ constructor(users, config) {
35
+ this.users = users;
36
+ this.config = config;
37
+ this.resolvedSecret = resolveJwtSecret(config.jwtSecret);
38
+ }
39
+ users;
40
+ config;
41
+ DEFAULT_ROUNDS = 12;
42
+ resolvedSecret;
43
+ // ─── Registration ─────────────────────────────────────────────────────
44
+ async register(email, password, roles = ["user"], metadata) {
45
+ if (!email || typeof email !== "string") {
46
+ throw new Error("[Auth] Invalid email");
47
+ }
48
+ validatePasswordStrength(password);
49
+ const existing = await this.users.findOne({ email: { $eq: email } });
50
+ if (existing) throw new Error("[Auth] Email already registered");
51
+ const passwordHash = await bcrypt.hash(
52
+ password,
53
+ this.config.bcryptRounds ?? this.DEFAULT_ROUNDS
54
+ );
55
+ const now = (/* @__PURE__ */ new Date()).toISOString();
56
+ const doc = await this.users.insertOne({
57
+ email,
58
+ passwordHash,
59
+ roles,
60
+ metadata,
61
+ createdAt: now,
62
+ updatedAt: now
63
+ });
64
+ const user = doc;
65
+ const session = this.createSession(user);
66
+ await this.config.onSignIn?.(user);
67
+ return { user, session };
68
+ }
69
+ // ─── Login ────────────────────────────────────────────────────────────
70
+ async login(email, password) {
71
+ const doc = await this.users.findOne({ email: { $eq: email } });
72
+ if (!doc) {
73
+ await bcrypt.compare(password, "$2a$12$invalidhashtopreventtimingattacks");
74
+ throw new Error("[Auth] Invalid credentials");
75
+ }
76
+ const user = doc;
77
+ const valid = await bcrypt.compare(password, user.passwordHash);
78
+ if (!valid) throw new Error("[Auth] Invalid credentials");
79
+ const session = this.createSession(user);
80
+ await this.config.onSignIn?.(user);
81
+ return { user, session };
82
+ }
83
+ // ─── Token verification ───────────────────────────────────────────────
84
+ verifyToken(token) {
85
+ try {
86
+ const payload = jwt.verify(token, this.resolvedSecret, {
87
+ algorithms: ["HS256"]
88
+ });
89
+ return payload;
90
+ } catch {
91
+ throw new Error("[Auth] Invalid or expired token");
92
+ }
93
+ }
94
+ // ─── Role-based access ────────────────────────────────────────────────
95
+ requireRole(session, role) {
96
+ if (!session.roles.includes(role) && !session.roles.includes("admin")) {
97
+ throw new Error(`[Auth] Requires role: ${role}`);
98
+ }
99
+ }
100
+ requireAnyRole(session, roles) {
101
+ const hasRole = roles.some(
102
+ (r) => session.roles.includes(r) || session.roles.includes("admin")
103
+ );
104
+ if (!hasRole) {
105
+ throw new Error(`[Auth] Requires one of roles: ${roles.join(", ")}`);
106
+ }
107
+ }
108
+ // ─── Password management ──────────────────────────────────────────────
109
+ async changePassword(userId, oldPassword, newPassword) {
110
+ const doc = await this.users.findById(userId);
111
+ if (!doc) throw new Error("[Auth] User not found");
112
+ const user = doc;
113
+ const valid = await bcrypt.compare(oldPassword, user.passwordHash);
114
+ if (!valid) throw new Error("[Auth] Old password incorrect");
115
+ validatePasswordStrength(newPassword);
116
+ const newHash = await bcrypt.hash(
117
+ newPassword,
118
+ this.config.bcryptRounds ?? this.DEFAULT_ROUNDS
119
+ );
120
+ await this.users.findByIdAndUpdate(userId, {
121
+ $set: { passwordHash: newHash, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
122
+ });
123
+ }
124
+ async resetPassword(userId, newPassword) {
125
+ validatePasswordStrength(newPassword);
126
+ const newHash = await bcrypt.hash(
127
+ newPassword,
128
+ this.config.bcryptRounds ?? this.DEFAULT_ROUNDS
129
+ );
130
+ await this.users.findByIdAndUpdate(userId, {
131
+ $set: { passwordHash: newHash, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
132
+ });
133
+ }
134
+ // ─── User management ─────────────────────────────────────────────────
135
+ async getUserById(id) {
136
+ const doc = await this.users.findById(id);
137
+ return doc ? doc : null;
138
+ }
139
+ async getUserByEmail(email) {
140
+ const doc = await this.users.findOne({ email: { $eq: email } });
141
+ return doc ? doc : null;
142
+ }
143
+ async updateRoles(userId, roles) {
144
+ await this.users.findByIdAndUpdate(userId, {
145
+ $set: { roles, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
146
+ });
147
+ }
148
+ async deleteUser(userId) {
149
+ await this.users.deleteById(userId);
150
+ await this.config.onSignOut?.(userId);
151
+ }
152
+ // ─── Session helpers ──────────────────────────────────────────────────
153
+ createSession(user) {
154
+ const expiresIn = this.config.jwtExpiresIn ?? "7d";
155
+ const payload = {
156
+ sub: user._id,
157
+ userId: user._id,
158
+ roles: user.roles,
159
+ expiresAt: Date.now() + this.parseExpiry(expiresIn)
160
+ };
161
+ const token = jwt.sign(payload, this.resolvedSecret, {
162
+ expiresIn,
163
+ algorithm: "HS256"
164
+ });
165
+ return { ...payload, token };
166
+ }
167
+ parseExpiry(s) {
168
+ const n = parseInt(s);
169
+ if (s.endsWith("d")) return n * 864e5;
170
+ if (s.endsWith("h")) return n * 36e5;
171
+ if (s.endsWith("m")) return n * 6e4;
172
+ return n * 1e3;
173
+ }
174
+ // ─── Middleware factory (Express/Fastify compatible) ──────────────────
175
+ middleware() {
176
+ return (req, res, next) => {
177
+ const auth = req.headers["authorization"];
178
+ if (!auth?.startsWith("Bearer ")) {
179
+ return res.status(401).json({ error: "Missing token" });
180
+ }
181
+ try {
182
+ req.session = this.verifyToken(auth.slice(7));
183
+ next();
184
+ } catch {
185
+ res.status(401).json({ error: "Unauthorized" });
186
+ }
187
+ };
188
+ }
189
+ requireRoleMiddleware(role) {
190
+ return (req, res, next) => {
191
+ try {
192
+ this.requireRole(req.session, role);
193
+ next();
194
+ } catch {
195
+ res.status(403).json({ error: "Forbidden" });
196
+ }
197
+ };
198
+ }
199
+ };
200
+
201
+ export { GramoBaseAuth };
202
+ //# sourceMappingURL=index.js.map
203
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/GramoBaseAuth.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { randomUUID, randomBytes } from 'crypto';\nimport * as jwt from 'jsonwebtoken';\nimport * as bcrypt from 'bcryptjs';\nimport { z } from 'zod';\nimport { AuthConfig, User, Session } from '../types/index.js';\nimport { Collection } from '../orm/Collection.js';\nimport { existsSync, readFileSync } from 'fs';\n\nconst UserSchema = z.object({\n email: z.string().email(),\n passwordHash: z.string(),\n roles: z.array(z.string()).default(['user']),\n metadata: z.record(z.unknown()).optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n});\n\n// TODO(security): Consider adding OAuth 2.0 provider support (e.g., Google, GitHub).\n// TODO(security): Consider adding MFA (TOTP/WebAuthn) for high-security deployments.\n// TODO(security): Consider adding pwned-password checking via haveibeenpwned.com API.\n\n/**\n * Resolve JWT secret with multi-tiered fallback:\n * 1. Explicit config value\n * 2. Environment variable\n * 3. Local jwt_secret.txt file\n * 4. Ephemeral random generation with warning (NOT suitable for multi-instance prod)\n *\n * MUST NOT hardcode or use default literal fallbacks.\n */\nfunction resolveJwtSecret(configSecret?: string): string {\n if (configSecret) return configSecret;\n if (process.env['JWT_SECRET']) return process.env['JWT_SECRET'];\n const secretFile = './jwt_secret.txt';\n if (existsSync(secretFile)) return readFileSync(secretFile, 'utf-8').trim();\n const ephemeral = randomBytes(32).toString('hex');\n console.warn(\n '[gramobase Auth] WARNING: No JWT_SECRET provided. Using an ephemeral random secret. ' +\n 'Tokens will be invalidated on restart. Set JWT_SECRET env variable for production!'\n );\n return ephemeral;\n}\n\n/**\n * Validate password strength (min 8 chars, no maximum).\n * Does not require specific character types — users choose their own strong passwords.\n */\nfunction validatePasswordStrength(password: string): void {\n if (password.length < 8) {\n throw new Error('[Auth] Password must be at least 8 characters long');\n }\n // TODO(security): Consider rejecting known breached passwords via HaveIBeenPwned API.\n}\n\nexport class GramoBaseAuth {\n private readonly DEFAULT_ROUNDS = 12;\n private readonly resolvedSecret: string;\n\n constructor(\n private users: Collection<typeof UserSchema>,\n private config: AuthConfig\n ) {\n // Resolve secret at construction time; fail safely if no secret available\n this.resolvedSecret = resolveJwtSecret(config.jwtSecret);\n }\n\n // ─── Registration ─────────────────────────────────────────────────────\n\n async register(\n email: string,\n password: string,\n roles: string[] = ['user'],\n metadata?: Record<string, unknown>\n ): Promise<{ user: User; session: Session }> {\n // Validate email format (basic — Zod schema handles full validation)\n if (!email || typeof email !== 'string') {\n throw new Error('[Auth] Invalid email');\n }\n\n // Validate password strength\n validatePasswordStrength(password);\n\n const existing = await this.users.findOne({ email: { $eq: email } } as any);\n if (existing) throw new Error('[Auth] Email already registered');\n\n const passwordHash = await bcrypt.hash(\n password,\n this.config.bcryptRounds ?? this.DEFAULT_ROUNDS\n );\n\n const now = new Date().toISOString();\n const doc = await this.users.insertOne({\n email,\n passwordHash,\n roles,\n metadata,\n createdAt: now,\n updatedAt: now,\n });\n\n const user = doc as unknown as User;\n const session = this.createSession(user);\n await this.config.onSignIn?.(user);\n\n return { user, session };\n }\n\n // ─── Login ────────────────────────────────────────────────────────────\n\n async login(email: string, password: string): Promise<{ user: User; session: Session }> {\n const doc = await this.users.findOne({ email: { $eq: email } } as any);\n if (!doc) {\n // Use constant-time comparison to avoid timing attacks on email existence\n await bcrypt.compare(password, '$2a$12$invalidhashtopreventtimingattacks');\n throw new Error('[Auth] Invalid credentials');\n }\n\n const user = doc as unknown as User;\n const valid = await bcrypt.compare(password, user.passwordHash);\n if (!valid) throw new Error('[Auth] Invalid credentials');\n\n const session = this.createSession(user);\n await this.config.onSignIn?.(user);\n\n return { user, session };\n }\n\n // ─── Token verification ───────────────────────────────────────────────\n\n verifyToken(token: string): Session {\n try {\n // Hardcode algorithm to prevent algorithm confusion attacks; reject 'none'\n const payload = jwt.verify(token, this.resolvedSecret, {\n algorithms: ['HS256'],\n }) as Session;\n return payload;\n } catch {\n throw new Error('[Auth] Invalid or expired token');\n }\n }\n\n // ─── Role-based access ────────────────────────────────────────────────\n\n requireRole(session: Session, role: string): void {\n if (!session.roles.includes(role) && !session.roles.includes('admin')) {\n throw new Error(`[Auth] Requires role: ${role}`);\n }\n }\n\n requireAnyRole(session: Session, roles: string[]): void {\n const hasRole = roles.some(\n (r) => session.roles.includes(r) || session.roles.includes('admin')\n );\n if (!hasRole) {\n throw new Error(`[Auth] Requires one of roles: ${roles.join(', ')}`);\n }\n }\n\n // ─── Password management ──────────────────────────────────────────────\n\n async changePassword(\n userId: string,\n oldPassword: string,\n newPassword: string\n ): Promise<void> {\n const doc = await this.users.findById(userId);\n if (!doc) throw new Error('[Auth] User not found');\n\n const user = doc as unknown as User;\n const valid = await bcrypt.compare(oldPassword, user.passwordHash);\n if (!valid) throw new Error('[Auth] Old password incorrect');\n\n // Validate new password strength\n validatePasswordStrength(newPassword);\n\n const newHash = await bcrypt.hash(\n newPassword,\n this.config.bcryptRounds ?? this.DEFAULT_ROUNDS\n );\n\n await this.users.findByIdAndUpdate(userId, {\n $set: { passwordHash: newHash, updatedAt: new Date().toISOString() } as any,\n });\n }\n\n async resetPassword(userId: string, newPassword: string): Promise<void> {\n validatePasswordStrength(newPassword);\n const newHash = await bcrypt.hash(\n newPassword,\n this.config.bcryptRounds ?? this.DEFAULT_ROUNDS\n );\n await this.users.findByIdAndUpdate(userId, {\n $set: { passwordHash: newHash, updatedAt: new Date().toISOString() } as any,\n });\n }\n\n // ─── User management ─────────────────────────────────────────────────\n\n async getUserById(id: string): Promise<User | null> {\n const doc = await this.users.findById(id);\n return doc ? (doc as unknown as User) : null;\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n const doc = await this.users.findOne({ email: { $eq: email } } as any);\n return doc ? (doc as unknown as User) : null;\n }\n\n async updateRoles(userId: string, roles: string[]): Promise<void> {\n await this.users.findByIdAndUpdate(userId, {\n $set: { roles, updatedAt: new Date().toISOString() } as any,\n });\n }\n\n async deleteUser(userId: string): Promise<void> {\n await this.users.deleteById(userId);\n await this.config.onSignOut?.(userId);\n }\n\n // ─── Session helpers ──────────────────────────────────────────────────\n\n private createSession(user: User): Session {\n const expiresIn = this.config.jwtExpiresIn ?? '7d';\n const payload: Omit<Session, 'token'> & { sub: string } = {\n sub: user._id,\n userId: user._id,\n roles: user.roles,\n expiresAt: Date.now() + this.parseExpiry(expiresIn),\n };\n\n // Sign with hardcoded HS256 algorithm; 'none' algorithm is never used\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const token = (jwt.sign as any)(payload, this.resolvedSecret, {\n expiresIn,\n algorithm: 'HS256',\n }) as string;\n return { ...payload, token };\n }\n\n private parseExpiry(s: string): number {\n const n = parseInt(s);\n if (s.endsWith('d')) return n * 86400_000;\n if (s.endsWith('h')) return n * 3600_000;\n if (s.endsWith('m')) return n * 60_000;\n return n * 1000;\n }\n\n // ─── Middleware factory (Express/Fastify compatible) ──────────────────\n\n middleware() {\n return (req: any, res: any, next: any) => {\n const auth = req.headers['authorization'] as string | undefined;\n if (!auth?.startsWith('Bearer ')) {\n return res.status(401).json({ error: 'Missing token' });\n }\n try {\n req.session = this.verifyToken(auth.slice(7));\n next();\n } catch {\n // Do NOT expose error details to client; log internally if needed\n res.status(401).json({ error: 'Unauthorized' });\n }\n };\n }\n\n requireRoleMiddleware(role: string) {\n return (req: any, res: any, next: any) => {\n try {\n this.requireRole(req.session, role);\n next();\n } catch {\n res.status(403).json({ error: 'Forbidden' });\n }\n };\n }\n}\n"]}