diff --git a/backend/app.js b/backend/app.js
index 447a2c321..7fb9a38a8 100644
--- a/backend/app.js
+++ b/backend/app.js
@@ -16,6 +16,9 @@ app.set('trust proxy', 1);
const GC = require('./utils/GC');
const { getPublicIp } = require('./utils/network');
+// Initialize Queue Workers
+require('./queues/emailQueue');
+
// Middleware
app.use(cors());
app.use(express.json());
@@ -34,7 +37,7 @@ const { authLimiter } = require('./middleware/auth_limiter');
const adminWhitelist = ['https://urbackend.bitbros.in'];
-// to allow localhost in developmentt
+// DEV LOCALHOST
if (process.env.NODE_ENV === 'development') {
adminWhitelist.push('http://localhost:5173');
}
@@ -46,8 +49,6 @@ const adminCorsOptions = {
const allowed = !origin || adminWhitelist.includes(origin);
const end = process.hrtime.bigint();
- console.log("Pure CORS check time:",
- Number(end - start) / 1e6, "ms");
if (allowed) {
callback(null, true);
@@ -65,7 +66,7 @@ if (process.env.NODE_ENV !== 'test') {
}
-// rate limiter and loggerr IMPORTS
+// LOGGING
const { limiter, logger } = require('./middleware/api_usage');
// Route Imports
@@ -75,6 +76,7 @@ const dataRoute = require('./routes/data');
const userAuthRoute = require('./routes/userAuth');
const storageRoute = require('./routes/storage');
const schemaRoute = require('./routes/schemas');
+const releaseRoute = require('./routes/releases');
// ROUTES SETUP
app.use('/api/auth/login', authLimiter); // Strict limiter on login
@@ -85,6 +87,7 @@ app.use('/api/userAuth', limiter, logger, userAuthRoute);
app.use('/api/data', limiter, cors(adminCorsOptions), logger, dataRoute);
app.use('/api/schemas', limiter, cors(adminCorsOptions), logger, schemaRoute);
app.use('/api/storage', limiter, cors(adminCorsOptions), logger, storageRoute);
+app.use('/api/releases', releaseRoute);
app.get('/api/server-ip', async (req, res) => {
const ip = await getPublicIp();
@@ -111,8 +114,7 @@ app.use((err, req, res, next) => {
message: err.message
});
});
-// DB and server initialization
-// (Only connect if NOT in Test Mode)
+// INITIALIZATION
if (process.env.NODE_ENV !== 'test') {
const PORT = process.env.PORT || 1234;
@@ -145,7 +147,7 @@ if (process.env.NODE_ENV !== 'test') {
console.log(`Server running on port ${PORT}`);
});
- // handle gracefll shutdwn
+ // SHUTDOWN
const gracefulShutdown = async () => {
console.log('🛑 SIGTERM/SIGINT received. Shutting down gracefully...');
diff --git a/backend/controllers/auth.controller.js b/backend/controllers/auth.controller.js
index 5ae7f2296..59a58b46e 100644
--- a/backend/controllers/auth.controller.js
+++ b/backend/controllers/auth.controller.js
@@ -4,7 +4,7 @@ const Project = require("../models/Project")
const bcrypt = require("bcryptjs");
const z = require("zod");
const jwt = require("jsonwebtoken");
-const sendOtp = require("../utils/emailService");
+const { sendOtp } = require("../utils/emailService");
const crypto = require("crypto");
const {
loginSchema,
@@ -55,7 +55,7 @@ async function validateOtp(userId, passedOtp) {
module.exports.register = async (req, res) => {
try {
- // Validate with Zod
+ // POST FOR - REGISTER
const { email, password } = loginSchema.parse(req.body);
const existingUser = await Developer.findOne({ email });
@@ -86,9 +86,9 @@ module.exports.login = async (req, res) => {
const validPass = await bcrypt.compare(password, dev.password);
if (!validPass) return res.status(400).json({ error: "Invalid password" });
- // FIX 1: JWT now expires in 7 days
+ // JWT EXPIRE
const token = jwt.sign(
- { _id: dev._id, isVerified: dev.isVerified },
+ { _id: dev._id, isVerified: dev.isVerified, maxProjects: dev.maxProjects },
process.env.JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
@@ -108,6 +108,7 @@ module.exports.login = async (req, res) => {
module.exports.changePassword = async (req, res) => {
try {
+ // POST FOR - CHANGE PASSWORD
const { currentPassword, newPassword } = changePasswordSchema.parse(req.body);
const dev = await Developer.findById(req.user._id);
@@ -132,6 +133,7 @@ module.exports.changePassword = async (req, res) => {
module.exports.deleteAccount = async (req, res) => {
try {
+ // POST FOR - DELETE ACCOUNT
const { password } = deleteAccountSchema.parse(req.body);
const dev = await Developer.findById(req.user._id);
@@ -174,6 +176,7 @@ module.exports.sendOtp = async (req, res) => {
module.exports.verifyOtp = async (req, res) => {
try {
+ // POST FOR - VERIFY OTP
const { email, otp } = verifyOtpSchema.parse(req.body);
const existingUser = await Developer.findOne({ email });
@@ -185,9 +188,9 @@ module.exports.verifyOtp = async (req, res) => {
existingUser.isVerified = true;
await existingUser.save();
- // FIX 1: JWT with expiry
+ // JWT
const token = jwt.sign(
- { _id: existingUser._id, isVerified: true },
+ { _id: existingUser._id, isVerified: true, maxProjects: existingUser.maxProjects },
process.env.JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
@@ -202,7 +205,7 @@ module.exports.verifyOtp = async (req, res) => {
}
-// FIX 5: Forgot Password — generate + send reset OTP
+// FORGOT PASSWORD
module.exports.forgotPassword = async (req, res) => {
try {
const { email } = onlyEmailSchema.parse(req.body);
@@ -223,9 +226,10 @@ module.exports.forgotPassword = async (req, res) => {
}
-// FIX 5: Reset Password — verify OTP then set new password
+// RESET PASSWORD
module.exports.resetPassword = async (req, res) => {
try {
+ // POST FOR - RESET PASSWORD
const { email, otp, newPassword } = resetPasswordSchema.parse(req.body);
const dev = await Developer.findOne({ email });
@@ -233,7 +237,7 @@ module.exports.resetPassword = async (req, res) => {
const otpDoc = await validateOtp(dev._id, otp);
- // OTP matched — update password
+ // UPDATE PASSWORD
await otpDoc.deleteOne();
const salt = await bcrypt.genSalt(10);
dev.password = await bcrypt.hash(newPassword, salt);
diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js
index 39a825c6e..d2e751848 100644
--- a/backend/controllers/project.controller.js
+++ b/backend/controllers/project.controller.js
@@ -1,5 +1,6 @@
const mongoose = require("mongoose")
const Project = require("../models/Project")
+const Developer = require("../models/Developer")
const Log = require("../models/Log")
const { getStorage } = require("../utils/storage.manager");
const { randomUUID } = require("crypto");
@@ -27,25 +28,49 @@ const isExternalStorage = (project) =>
module.exports.createProject = async (req, res) => {
try {
- // Validation Applied
+ // POST FOR - PROJECT CREATION
const { name, description } = createProjectSchema.parse(req.body);
- const rawApiKey = generateApiKey()
- const hashedkey = hashApiKey(rawApiKey);
+ // --- PROJECT LIMIT CHECK ---
+ const ADMIN_EMAIL = process.env.ADMIN_EMAIL;
+
+ // GET MAX PROJECTS
+ const dev = await Developer.findById(req.user._id);
+ const MAX_PROJECTS = dev?.maxProjects || 3;
+
+ const isUserAdmin = req.user.email === ADMIN_EMAIL;
+ const projectCount = await Project.countDocuments({ owner: req.user._id });
+
+ if (!isUserAdmin && projectCount >= MAX_PROJECTS) {
+ return res.status(403).json({
+ error: `Project limit reached. Your current plan allows up to ${MAX_PROJECTS} projects.`,
+ limit: MAX_PROJECTS,
+ current: projectCount
+ });
+ }
+ // ---------------------------
+
+ const rawPublishableKey = generateApiKey('pk_live_');
+ const hashedPublishableKey = hashApiKey(rawPublishableKey);
+
+ const rawSecretKey = generateApiKey('sk_live_');
+ const hashedSecretKey = hashApiKey(rawSecretKey);
- const rawSecret = generateApiKey()
+ const rawJwtSecret = generateApiKey('jwt_');
const newProject = new Project({
name,
description,
owner: req.user._id,
- apiKey: hashedkey,
- jwtSecret: rawSecret
+ publishableKey: hashedPublishableKey,
+ secretKey: hashedSecretKey,
+ jwtSecret: rawJwtSecret
});
await newProject.save();
const projectObj = newProject.toObject();
- projectObj.apiKey = rawApiKey;
+ projectObj.publishableKey = rawPublishableKey;
+ projectObj.secretKey = rawSecretKey;
delete projectObj.jwtSecret;
res.status(201).json(projectObj);
@@ -74,14 +99,15 @@ module.exports.getSingleProject = async (req, res) => {
project = await getProjectById(req.params.projectId);
let projectObj;
if (!project) {
- project = await Project.findOne({ _id: req.params.projectId, owner: req.user._id }).select('-apiKey -jwtSecret');
+ project = await Project.findOne({ _id: req.params.projectId, owner: req.user._id }).select('-publishableKey -secretKey -jwtSecret');
if (!project) return res.status(404).json({ error: "Project not found." });
projectObj = project.toObject();
await setProjectById(req.params.projectId, project);
}
projectObj = project;
- delete projectObj.apiKey;
+ delete projectObj.publishableKey;
+ delete projectObj.secretKey;
delete projectObj.jwtSecret;
res.json(projectObj);
} catch (err) {
@@ -91,32 +117,45 @@ module.exports.getSingleProject = async (req, res) => {
module.exports.regenerateApiKey = async (req, res) => {
try {
- const newApiKey = generateApiKey();
+ const { keyType } = req.body; // 'publishable' or 'secret'
+
+ if (keyType !== 'publishable' && keyType !== 'secret') {
+ return res.status(400).json({ error: "Invalid keyType. Must be 'publishable' or 'secret'." });
+ }
+
+ const prefix = keyType === 'publishable' ? 'pk_live_' : 'sk_live_';
+ const newApiKey = generateApiKey(prefix);
const hashed = hashApiKey(newApiKey);
- const oldApiProj = await Project.findOne({ _id: req.params.projectId, owner: req.user._id }).select('apiKey');
+ const oldApiProj = await Project.findOne({ _id: req.params.projectId, owner: req.user._id })
+ .select('publishableKey secretKey');
if (!oldApiProj) return res.status(404).json({ error: "Project not found." });
- await deleteProjectByApiKeyCache(oldApiProj.apiKey);
+
+ // CLEAR CACHE
+ await deleteProjectByApiKeyCache(oldApiProj.publishableKey);
+ await deleteProjectByApiKeyCache(oldApiProj.secretKey);
+ const updateField = keyType === 'publishable' ? { publishableKey: hashed } : { secretKey: hashed };
const project = await Project.findOneAndUpdate(
{ _id: req.params.projectId, owner: req.user._id },
- { $set: { apiKey: hashed } },
+ { $set: updateField },
{ new: true }
);
if (!project) return res.status(404).json({ error: "Project not found." });
const projectObj = project.toObject();
- delete projectObj.apiKey;
+ delete projectObj.publishableKey;
+ delete projectObj.secretKey;
delete projectObj.jwtSecret;
- res.json({ apiKey: newApiKey, project: projectObj });
+ res.json({ apiKey: newApiKey, keyType, project: projectObj });
} catch (err) {
res.status(500).json({ error: err.message });
}
};
-//function to validate monguri
+// VALIDATE URI
const isSafeUri = (uri) => {
try {
const parsed = new URL(uri);
@@ -132,17 +171,16 @@ module.exports.updateExternalConfig = async (req, res) => {
try {
const { projectId } = req.params;
- // 1. Zod Validation
+ // POST FOR - EXTERNAL CONFIG
const validatedData = updateExternalConfigSchema.parse(req.body);
const { dbUri, storageUrl, storageKey, storageProvider } = validatedData;
const updateData = {};
- // 2. Database URI Check & Encryption
+ // DB CONFIG
if (dbUri) {
if (!isSafeUri(dbUri)) return res.status(400).json({ error: "DB URI is pointing to a restricted host (localhost/internal)." });
- // Naye model structure ke hisaab se save karein
updateData['resources.db.config'] = encrypt(JSON.stringify({ dbUri }));
updateData['resources.db.isExternal'] = true;
@@ -168,7 +206,7 @@ module.exports.updateExternalConfig = async (req, res) => {
// -------------------------
}
- // 3. Storage Config Encryption
+ // STORAGE CONFIG
if (storageUrl && storageKey) {
const storageConfig = {
storageUrl,
@@ -189,7 +227,6 @@ module.exports.updateExternalConfig = async (req, res) => {
res.status(200).json({ message: "External configuration updated successfully." });
} catch (err) {
- // Zod Error handling ko safe banayein
if (err.name === 'ZodError') {
return res.status(400).json({
error: err.errors?.[0]?.message || err.issues?.[0]?.message || "Validation failed"
@@ -263,11 +300,12 @@ module.exports.createCollection = async (req, res) => {
await deleteProjectById(projectId);
await setProjectById(projectId, project);
- await deleteProjectByApiKeyCache(project.apiKey);
- await deleteProjectByApiKeyCache(project.apiKey);
- // Safe Response
+ await deleteProjectByApiKeyCache(project.publishableKey);
+ await deleteProjectByApiKeyCache(project.secretKey);
+ // RESPONSE
const projectObj = project.toObject();
- delete projectObj.apiKey;
+ delete projectObj.publishableKey;
+ delete projectObj.secretKey;
delete projectObj.jwtSecret;
res.status(201).json(projectObj);
@@ -340,11 +378,6 @@ module.exports.getData = async (req, res) => {
const data = await features.query.lean();
- // let data = [];
- // if (collectionsList.length > 0) {
- // data = await mongoose.connection.db.collection(finalCollectionName).find({}).limit(50).toArray();
- // }
-
res.json(data);
} catch (err) {
res.status(500).json({ error: err.message });
@@ -386,7 +419,7 @@ module.exports.insertData = async (req, res) => {
project.databaseUsed = (project.databaseUsed || 0) + docSize;
}
await project.save();
- console.timeEnd("insert data")
+ await project.save();
res.json(result);
} catch (err) {
@@ -454,14 +487,13 @@ module.exports.editRow = async (req, res) => {
const oldSize = Buffer.byteLength(JSON.stringify(docToEdit.toObject()));
- // Apply updates
docToEdit.set(req.body);
const newSize = Buffer.byteLength(JSON.stringify(docToEdit.toObject()));
const sizeDiff = newSize - oldSize;
if (!project.resources.db.isExternal) {
- const limit = project.databaseLimit || 500 * 1024 * 1024; // Default 500MB if not set
+ const limit = project.databaseLimit || 500 * 1024 * 1024;
const currentUsed = project.databaseUsed || 0;
if (currentUsed + sizeDiff > limit) {
@@ -562,7 +594,6 @@ module.exports.uploadFile = async (req, res) => {
res.json({ success: true, path });
} catch (err) {
- console.log("upload file ke catch me hu");
res.status(500).json({ error: err });
}
};
@@ -667,12 +698,45 @@ module.exports.updateProject = async (req, res) => {
{ new: true }
);
if (!project) return res.status(404).json({ error: "Project not found." });
+
+ await deleteProjectById(project._id.toString());
+ await setProjectById(project._id.toString(), project);
+
res.json(project);
} catch (err) {
res.status(500).json({ error: err.message });
}
}
+module.exports.updateAllowedDomains = async (req, res) => {
+ try {
+ const { domains } = req.body;
+ if (!Array.isArray(domains) || !domains.every(d => typeof d === 'string')) {
+ return res.status(400).json({ error: "domains must be an array of strings." });
+ }
+
+ const cleanedDomains = domains
+ .map(d => d.trim())
+ .filter(d => d.length > 0);
+
+ const project = await Project.findOneAndUpdate(
+ { _id: req.params.projectId, owner: req.user._id },
+ { $set: { allowedDomains: cleanedDomains } },
+ { new: true }
+ );
+
+ if (!project) return res.status(404).json({ error: "Project not found or access denied." });
+ await deleteProjectById(project._id.toString());
+ await setProjectById(project._id.toString(), project);
+ await deleteProjectByApiKeyCache(project.publishableKey);
+ await deleteProjectByApiKeyCache(project.secretKey);
+
+ res.json({ message: "Allowed domains updated", allowedDomains: project.allowedDomains });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+}
+
module.exports.deleteProject = async (req, res) => {
try {
const projectId = req.params.projectId;
@@ -689,8 +753,6 @@ module.exports.deleteProject = async (req, res) => {
if (!project) {
return res.status(404).json({ error: "Project not found or access denied." });
}
-
- // collections WILL exist now
for (const col of project.collections) {
const collectionName = `${project._id}_${col.name}`;
try {
@@ -702,7 +764,7 @@ module.exports.deleteProject = async (req, res) => {
await mongoose.connection.db.dropCollection(`${project._id}_users`);
} catch (e) { }
- // DELETE ALL FILES (BYOS SAFE)
+ // DELETE FILES
const supabase = await getStorage(project);
const bucket = getBucket(project);
diff --git a/backend/controllers/release.controller.js b/backend/controllers/release.controller.js
new file mode 100644
index 000000000..04d111423
--- /dev/null
+++ b/backend/controllers/release.controller.js
@@ -0,0 +1,63 @@
+const Release = require("../models/Release");
+const Developer = require("../models/Developer");
+const { emailQueue } = require("../queues/emailQueue");
+
+const ADMIN_EMAIL = process.env.ADMIN_EMAIL;
+
+// GET ALL RELEASES
+exports.getAllReleases = async (req, res) => {
+ try {
+ const releases = await Release.find().sort({ createdAt: -1 });
+ res.json(releases);
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: "Internal Server Error" });
+ }
+};
+
+// CREATE RELEASE (Admin Only)
+exports.createRelease = async (req, res) => {
+ try {
+ const { version, title, content } = req.body;
+
+ // Verify Admin
+ if (req.user.email !== ADMIN_EMAIL) {
+ return res.status(403).json({ error: "Access denied. Admin only." });
+ }
+
+ if (!version || !title || !content) {
+ return res.status(400).json({ error: "Missing version, title, or content" });
+ }
+
+ const newRelease = new Release({
+ version,
+ title,
+ content,
+ publishedBy: req.user.email
+ });
+ await newRelease.save();
+
+ // 1. Fetch all verified developers
+ const developers = await Developer.find({ isVerified: true }, 'email');
+ const emails = developers.map(dev => dev.email);
+
+ // 2. Queue emails
+ await Promise.all(emails.map(email =>
+ emailQueue.add('release-email', {
+ email,
+ version,
+ title,
+ content
+ })
+ ));
+
+ res.status(201).json({
+ message: "Release published! Emails queued.",
+ count: emails.length
+ });
+
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: "Internal Server Error" });
+ }
+};
diff --git a/backend/controllers/schema.controller.js b/backend/controllers/schema.controller.js
index 9ba31580c..7d4cf6b6b 100644
--- a/backend/controllers/schema.controller.js
+++ b/backend/controllers/schema.controller.js
@@ -54,13 +54,15 @@ module.exports.createSchema = async (req, res) => {
// Clear redis cache
await deleteProjectById(projectId.toString());
await setProjectById(projectId.toString(), fullProject);
- await deleteProjectByApiKeyCache(fullProject.apiKey);
+ await deleteProjectByApiKeyCache(fullProject.publishableKey);
+ await deleteProjectByApiKeyCache(fullProject.secretKey);
if (req.hashedApiKey) {
await deleteProjectByApiKeyCache(req.hashedApiKey);
}
const projectObj = fullProject.toObject();
- delete projectObj.apiKey;
+ delete projectObj.publishableKey;
+ delete projectObj.secretKey;
delete projectObj.jwtSecret;
res.status(201).json({ message: "Schema created successfully", project: projectObj });
diff --git a/backend/middleware/projectRateLimiter.js b/backend/middleware/projectRateLimiter.js
new file mode 100644
index 000000000..58f5a1037
--- /dev/null
+++ b/backend/middleware/projectRateLimiter.js
@@ -0,0 +1,30 @@
+const rateLimit = require('express-rate-limit');
+const Project = require('../models/Project');
+
+const projectRateLimiter = rateLimit({
+ windowMs: 15 * 60 * 1000,
+ max: 500,
+
+ keyGenerator: (req, res) => {
+ if (!req.project || !req.project._id) {
+ return 'unauthorized';
+ }
+ return req.project._id.toString();
+ },
+
+ handler: (req, res, next, options) => {
+ res.status(options.statusCode).json({
+ error: "Too Many Requests",
+ message: "Project Rate limit exceeded. Please try again later."
+ });
+ },
+
+ limit: async (req, res) => {
+ if (req.project && req.project.rateLimit) {
+ return req.project.rateLimit;
+ }
+ return 500;
+ }
+});
+
+module.exports = projectRateLimiter;
diff --git a/backend/middleware/requireSecretKey.js b/backend/middleware/requireSecretKey.js
new file mode 100644
index 000000000..4cb925c93
--- /dev/null
+++ b/backend/middleware/requireSecretKey.js
@@ -0,0 +1,8 @@
+module.exports = (req, res, next) => {
+ if (req.keyRole !== 'secret') {
+ return res.status(403).json({
+ error: "Forbidden. This action requires a Secret Key (sk_live_...)."
+ });
+ }
+ next();
+};
diff --git a/backend/middleware/verifyApiKey.js b/backend/middleware/verifyApiKey.js
index d00bd17aa..6553f442b 100644
--- a/backend/middleware/verifyApiKey.js
+++ b/backend/middleware/verifyApiKey.js
@@ -12,15 +12,14 @@ module.exports = async (req, res, next) => {
return res.status(401).json({ error: 'API key not found' });
}
+ const isSecret = apiKey.startsWith('sk_live_');
+ const keyField = isSecret ? 'secretKey' : 'publishableKey';
const hashedApi = hashApiKey(apiKey);
- console.time("get project by api key cache")
let project = await getProjectByApiKeyCache(hashedApi);
- console.timeEnd("get project by api key cache")
if (!project) {
- console.time("get project by api key from db")
- project = await Project.findOne({ apiKey: hashedApi })
+ project = await Project.findOne({ [keyField]: hashedApi })
.select(`
owner
resources
@@ -28,12 +27,12 @@ module.exports = async (req, res, next) => {
databaseLimit
databaseUsed
storageLimit
- storageUsed,
+ storageUsed
jwtSecret
+ allowedDomains
`)
.populate('owner', 'isVerified')
.lean();
- console.timeEnd("get project by api key from db")
if (!project) {
return res.status(401).json({
@@ -45,26 +44,48 @@ module.exports = async (req, res, next) => {
await setProjectByApiKeyCache(hashedApi, project);
}
- console.time("checking if owner is verified")
if (!project.owner.isVerified) {
return res.status(401).json({
error: 'Owner not verified',
fix: 'Verify your account on https://urbackend.bitbros.in/dashboard'
});
}
- console.timeEnd("checking if owner is verified")
- // Ensure defaults are present (crucial for lean objects or cached POJOs)
- console.time("setting defaults")
if (!project.resources) project.resources = {};
if (!project.resources.db) project.resources.db = { isExternal: false };
if (!project.resources.storage) project.resources.storage = { isExternal: false };
- console.timeEnd("setting defaults")
- console.time("setting project and hashed api key")
+ if (!isSecret) {
+ let allowedDomains = project.allowedDomains || ['*'];
+ const origin = req.headers.origin || req.headers.referer;
+
+ if (!allowedDomains.includes('*')) {
+ if (!origin) {
+ return res.status(403).json({ error: "Forbidden: Origin header missing and this key is restricted to specific domains." });
+ }
+
+ try {
+ const originUrl = new URL(origin).origin;
+ const isAllowed = allowedDomains.some(domain => {
+ if (domain.startsWith('*.')) {
+ const baseDomain = domain.substring(2);
+ return originUrl === baseDomain || originUrl.endsWith('.' + baseDomain);
+ }
+ return originUrl === domain;
+ });
+
+ if (!isAllowed) {
+ return res.status(403).json({ error: `Forbidden: Origin ${originUrl} is not allowed by this project's CORS policy.` });
+ }
+ } catch (err) {
+ return res.status(400).json({ error: "Invalid Origin header format." });
+ }
+ }
+ }
+
req.project = project;
req.hashedApiKey = hashedApi;
- console.timeEnd("setting project and hashed api key")
+ req.keyRole = isSecret ? 'secret' : 'publishable';
next();
} catch (err) {
res.status(500).json({ error: err.message });
diff --git a/backend/models/Developer.js b/backend/models/Developer.js
index b34460364..3a33cdc07 100644
--- a/backend/models/Developer.js
+++ b/backend/models/Developer.js
@@ -13,6 +13,10 @@ const developerSchema = new mongoose.Schema({
isVerified: {
type: Boolean,
default: false
+ },
+ maxProjects: {
+ type: Number,
+ default: 3
}
}, { timestamps: true });
diff --git a/backend/models/Project.js b/backend/models/Project.js
index 81254a730..2e8f06d4b 100644
--- a/backend/models/Project.js
+++ b/backend/models/Project.js
@@ -29,7 +29,12 @@ const projectSchema = new mongoose.Schema({
type: mongoose.Schema.Types.ObjectId,
ref: 'Developer'
},
- apiKey: {
+ publishableKey: {
+ type: String,
+ required: true,
+ unique: true
+ },
+ secretKey: {
type: String,
required: true,
unique: true
@@ -40,6 +45,11 @@ const projectSchema = new mongoose.Schema({
},
collections: [collectionSchema],
+ allowedDomains: {
+ type: [String],
+ default: ['*']
+ },
+
// STORAGE LIMITS (Files)
storageUsed: { type: Number, default: 0 },
storageLimit: { type: Number, default: 20 * 1024 * 1024 }, // 20MB default
diff --git a/backend/models/Release.js b/backend/models/Release.js
new file mode 100644
index 000000000..3a13027bc
--- /dev/null
+++ b/backend/models/Release.js
@@ -0,0 +1,24 @@
+const mongoose = require('mongoose');
+
+const ReleaseSchema = new mongoose.Schema({
+ version: {
+ type: String,
+ required: true,
+ trim: true
+ },
+ title: {
+ type: String,
+ required: true,
+ trim: true
+ },
+ content: {
+ type: String,
+ required: true
+ },
+ publishedBy: {
+ type: String,
+ required: true
+ }
+}, { timestamps: true });
+
+module.exports = mongoose.model('Release', ReleaseSchema);
diff --git a/backend/package-lock.json b/backend/package-lock.json
index aa1f1d206..91ab395de 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -11,11 +11,12 @@
"dependencies": {
"@supabase/supabase-js": "^2.84.0",
"bcryptjs": "^3.0.2",
+ "bullmq": "^5.70.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
- "ioredis": "^5.9.2",
+ "ioredis": "^5.10.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.19.2",
"multer": "^2.0.2",
@@ -587,9 +588,9 @@
"license": "MIT"
},
"node_modules/@ioredis/commands": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz",
- "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==",
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz",
+ "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": {
@@ -1038,6 +1039,84 @@
"sparse-bitfield": "^3.0.3"
}
},
+ "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
+ "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
+ "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
+ "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
+ "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
+ "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
+ "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -1967,6 +2046,64 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
+ "node_modules/bullmq": {
+ "version": "5.70.1",
+ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.70.1.tgz",
+ "integrity": "sha512-HjfGHfICkAClrFL0Y07qNbWcmiOCv1l+nusupXUjrvTPuDEyPEJ23MP0lUwUs/QEy1a3pWt/P/sCsSZ1RjRK+w==",
+ "license": "MIT",
+ "dependencies": {
+ "cron-parser": "4.9.0",
+ "ioredis": "5.9.3",
+ "msgpackr": "1.11.5",
+ "node-abort-controller": "3.1.1",
+ "semver": "7.7.4",
+ "tslib": "2.8.1",
+ "uuid": "11.1.0"
+ }
+ },
+ "node_modules/bullmq/node_modules/@ioredis/commands": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz",
+ "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==",
+ "license": "MIT"
+ },
+ "node_modules/bullmq/node_modules/ioredis": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz",
+ "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@ioredis/commands": "1.5.0",
+ "cluster-key-slot": "^1.1.0",
+ "debug": "^4.3.4",
+ "denque": "^2.1.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.isarguments": "^3.1.0",
+ "redis-errors": "^1.2.0",
+ "redis-parser": "^3.0.0",
+ "standard-as-callback": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ioredis"
+ }
+ },
+ "node_modules/bullmq/node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -2343,6 +2480,18 @@
"node": ">= 0.10"
}
},
+ "node_modules/cron-parser": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
+ "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "luxon": "^3.2.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/cross-env": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
@@ -2446,6 +2595,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -3252,12 +3411,12 @@
"license": "ISC"
},
"node_modules/ioredis": {
- "version": "5.9.2",
- "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz",
- "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==",
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz",
+ "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==",
"license": "MIT",
"dependencies": {
- "@ioredis/commands": "1.5.0",
+ "@ioredis/commands": "1.5.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
@@ -4236,6 +4395,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/luxon": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
+ "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
@@ -4525,6 +4693,37 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/msgpackr": {
+ "version": "1.11.5",
+ "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
+ "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
+ "license": "MIT",
+ "optionalDependencies": {
+ "msgpackr-extract": "^3.0.2"
+ }
+ },
+ "node_modules/msgpackr-extract": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
+ "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "node-gyp-build-optional-packages": "5.2.2"
+ },
+ "bin": {
+ "download-msgpackr-prebuilds": "bin/download-prebuilds.js"
+ },
+ "optionalDependencies": {
+ "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
+ "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
+ "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
+ "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
+ "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
+ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
+ }
+ },
"node_modules/multer": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
@@ -4618,6 +4817,27 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-abort-controller": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
+ "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
+ "license": "MIT"
+ },
+ "node_modules/node-gyp-build-optional-packages": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
+ "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^2.0.1"
+ },
+ "bin": {
+ "node-gyp-build-optional-packages": "bin.js",
+ "node-gyp-build-optional-packages-optional": "optional.js",
+ "node-gyp-build-optional-packages-test": "build-test.js"
+ }
+ },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -5182,9 +5402,9 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
diff --git a/backend/package.json b/backend/package.json
index 4c3baf6c4..0d6f68009 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -5,18 +5,19 @@
"main": "app.js",
"scripts": {
"start": "node app.js",
- "test": "cross-env NODE_ENV=test jest --testTimeout=10000"
+ "test": "cross-env NODE_ENV=test jest --testTimeout=10000 --setupFiles ./tests/jest.setup.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"@supabase/supabase-js": "^2.84.0",
"bcryptjs": "^3.0.2",
+ "bullmq": "^5.70.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
- "ioredis": "^5.9.2",
+ "ioredis": "^5.10.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.19.2",
"multer": "^2.0.2",
@@ -29,4 +30,4 @@
"jest": "^30.2.0",
"supertest": "^7.1.4"
}
-}
\ No newline at end of file
+}
diff --git a/backend/queues/emailQueue.js b/backend/queues/emailQueue.js
new file mode 100644
index 000000000..09318f1d1
--- /dev/null
+++ b/backend/queues/emailQueue.js
@@ -0,0 +1,40 @@
+const { Queue, Worker } = require('bullmq');
+const IORedis = require('ioredis');
+const { sendReleaseEmail } = require('../utils/emailService');
+
+const connection = new IORedis({
+ host: process.env.REDIS_HOST || '127.0.0.1',
+ port: process.env.REDIS_PORT || 6379,
+ maxRetriesPerRequest: null,
+});
+
+// Create the email queue
+const emailQueue = new Queue('email-queue', { connection });
+
+// Initialize Worker with Rate Limiting
+const worker = new Worker('email-queue', async (job) => {
+ const { email, version, title, content } = job.data;
+ try {
+ console.log(`[Queue] Processing Release email for: ${email}`);
+ await sendReleaseEmail(email, { version, title, content });
+ } catch (error) {
+ console.error(`[Queue] Failed to send email to ${email}:`, error);
+ throw error;
+ }
+}, {
+ connection,
+ limiter: {
+ max: 1,
+ duration: 900000, // 1 job per 15 minutes (96 per 24 hours) - safe for 100 limit
+ }
+});
+
+worker.on('completed', (job) => {
+ console.log(`[Queue] Job ${job.id} completed successfully`);
+});
+
+worker.on('failed', (job, err) => {
+ console.error(`[Queue] Job ${job.id} failed:`, err);
+});
+
+module.exports = { emailQueue };
diff --git a/backend/routes/data.js b/backend/routes/data.js
index 80cefa9b8..f2b5ef478 100644
--- a/backend/routes/data.js
+++ b/backend/routes/data.js
@@ -1,33 +1,31 @@
const express = require('express');
const router = express.Router();
const verifyApiKey = require('../middleware/verifyApiKey');
+const requireSecretKey = require('../middleware/requireSecretKey');
+const projectRateLimiter = require('../middleware/projectRateLimiter');
+const { getCompiledModel } = require("../utils/injectModel");
const { insertData, getAllData, getSingleDoc, updateSingleData, deleteSingleDoc } = require("../controllers/data.controller")
-// Dynamic POST Route
-// Example: POST /api/data/products
-router.post('/:collectionName', verifyApiKey, insertData);
+// POST REQ TO INSERT DATA
+router.post('/:collectionName', verifyApiKey, projectRateLimiter, requireSecretKey, insertData);
-// GET Route to fetch all data from a collection
-// GET /api/data/products
-router.get('/:collectionName', verifyApiKey, getAllData);
+// GET REQ ALL DATA
+router.get('/:collectionName', verifyApiKey, projectRateLimiter, getAllData);
-// GET Single Item by ID
-// GET /api/data/products/69235c0cc8e73cd3d7bbeab8
-router.get('/:collectionName/:id', verifyApiKey, getSingleDoc);
+// GET REQ SINGLE DATA
+router.get('/:collectionName/:id', verifyApiKey, projectRateLimiter, getSingleDoc);
-// DELETE Single Item by ID
-// DELETE /api/data/products/69235c0cc8e73cd3d7bbeab8
-router.delete('/:collectionName/:id', verifyApiKey, deleteSingleDoc);
+// DELETE REQ SINGLE DATA
+router.delete('/:collectionName/:id', verifyApiKey, projectRateLimiter, requireSecretKey, deleteSingleDoc);
-// UPDATE Single Item by ID
-// PUT /api/data/products/69235c0cc8e73cd3d7bbeab8
-router.put('/:collectionName/:id', verifyApiKey, updateSingleData);
+// PUT REQ SINGLE DATA
+router.put('/:collectionName/:id', verifyApiKey, projectRateLimiter, requireSecretKey, updateSingleData);
module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/projects.js b/backend/routes/projects.js
index 180a289f7..ad91f08a7 100644
--- a/backend/routes/projects.js
+++ b/backend/routes/projects.js
@@ -25,7 +25,8 @@ const {
updateExternalConfig,
deleteExternalDbConfig,
deleteExternalStorageConfig,
- analytics
+ analytics,
+ updateAllowedDomains
} = require("../controllers/project.controller")
const upload = multer({ storage: storage, limits: { fileSize: 10 * 1024 * 1024 } }); // 10MB Limit
@@ -76,6 +77,9 @@ router.delete('/:projectId', authMiddleware, verifyEmail, deleteProject);
// UPDATE PROJECT
router.patch('/:projectId', authMiddleware, updateProject);
+// UPDATE ALLOWED DOMAINS
+router.patch('/:projectId/allowed-domains', authMiddleware, verifyEmail, updateAllowedDomains);
+
// UPDATE EXTERNAL CONFIG
router.patch('/:projectId/byod-config', authMiddleware, updateExternalConfig);
diff --git a/backend/routes/releases.js b/backend/routes/releases.js
new file mode 100644
index 000000000..080cd2650
--- /dev/null
+++ b/backend/routes/releases.js
@@ -0,0 +1,22 @@
+const express = require('express');
+const router = express.Router();
+const authorization = require('../middleware/authMiddleware');
+const { getAllReleases, createRelease } = require('../controllers/release.controller');
+const RateLimit = require('express-rate-limit');
+
+const getAllReleasesLimiter = RateLimit({
+ windowMs: 15 * 60 * 1000,
+ max: 1000,
+});
+
+// GET ALL RELEASES (Public)
+router.get('/', getAllReleasesLimiter, getAllReleases);
+
+const createReleaseLimiter = RateLimit({
+ windowMs: 15 * 60 * 1000,
+ max: 5 ,
+});
+// CREATE RELEASE (Admin Only)
+router.post('/', createReleaseLimiter, createRelease);
+
+module.exports = router;
diff --git a/backend/routes/schemas.js b/backend/routes/schemas.js
index 1ca760eec..3614bb659 100644
--- a/backend/routes/schemas.js
+++ b/backend/routes/schemas.js
@@ -1,14 +1,14 @@
const express = require('express');
const router = express.Router();
const verifyApiKey = require('../middleware/verifyApiKey');
+const requireSecretKey = require('../middleware/requireSecretKey');
+const projectRateLimiter = require('../middleware/projectRateLimiter');
const { checkSchema, createSchema } = require("../controllers/schema.controller");
-// GET Route to check if a schema exists
-// GET /api/schemas/error_logs
-router.get('/:collectionName', verifyApiKey, checkSchema);
+// GET REQ FETCH SCHEMA
+router.get('/:collectionName', verifyApiKey, projectRateLimiter, checkSchema);
-// POST Route to create a new schema
-// POST /api/schemas
-router.post('/', verifyApiKey, createSchema);
+// POST REQ CREATE SCHEMA
+router.post('/', verifyApiKey, projectRateLimiter, requireSecretKey, createSchema);
module.exports = router;
diff --git a/backend/routes/storage.js b/backend/routes/storage.js
index 2303032a5..62c7a8816 100644
--- a/backend/routes/storage.js
+++ b/backend/routes/storage.js
@@ -2,6 +2,8 @@ const express = require('express');
const router = express.Router();
const multer = require('multer');
const verifyApiKey = require('../middleware/verifyApiKey');
+const requireSecretKey = require('../middleware/requireSecretKey');
+const projectRateLimiter = require('../middleware/projectRateLimiter');
const { uploadFile, deleteFile, deleteAllFiles } = require("../controllers/storage.controller")
const storage = multer.memoryStorage();
@@ -10,13 +12,13 @@ const upload = multer({
limits: { fileSize: 10 * 1024 * 1024 } // 10MB Limit
});
-// UPLOAD FILE
-router.post('/upload', verifyApiKey, upload.single('file'), uploadFile);
+// POST REQ UPLOAD FILE
+router.post('/upload', verifyApiKey, projectRateLimiter, requireSecretKey, upload.single('file'), uploadFile);
-// DELETE SINGLE FILE
-router.delete('/file', verifyApiKey, deleteFile);
+// DELETE REQ SINGLE FILE
+router.delete('/file', verifyApiKey, projectRateLimiter, requireSecretKey, deleteFile);
-// DELETE ALL FILES
-router.delete('/all', verifyApiKey, deleteAllFiles);
+// DELETE REQ ALL FILES
+router.delete('/all', verifyApiKey, projectRateLimiter, requireSecretKey, deleteAllFiles);
module.exports = router;
\ No newline at end of file
diff --git a/backend/tests/jest.setup.js b/backend/tests/jest.setup.js
new file mode 100644
index 000000000..0738df37b
--- /dev/null
+++ b/backend/tests/jest.setup.js
@@ -0,0 +1,25 @@
+
+jest.mock('ioredis', () => {
+ const Redis = jest.fn().mockImplementation(() => ({
+ on: jest.fn(),
+ quit: jest.fn().mockResolvedValue('OK'),
+ status: 'ready',
+ set: jest.fn().mockResolvedValue('OK'),
+ get: jest.fn().mockResolvedValue(null),
+ del: jest.fn().mockResolvedValue(1),
+ options: {},
+ }));
+ return Redis;
+});
+
+jest.mock('bullmq', () => ({
+ Queue: jest.fn().mockImplementation(() => ({
+ add: jest.fn().mockResolvedValue({ id: 'mock-job-id' }),
+ on: jest.fn(),
+ close: jest.fn().mockResolvedValue(undefined),
+ })),
+ Worker: jest.fn().mockImplementation(() => ({
+ on: jest.fn(),
+ close: jest.fn().mockResolvedValue(undefined),
+ })),
+}));
diff --git a/backend/utils/api.js b/backend/utils/api.js
index 3b700c311..735cc2689 100644
--- a/backend/utils/api.js
+++ b/backend/utils/api.js
@@ -1,6 +1,6 @@
const crypto = require('crypto');
-function generateApiKey() {
+function generateApiKey(prefix = 'ub_key_') {
// OS level cryptographic randomnesss
const bytes = crypto.randomBytes(32)
@@ -9,7 +9,7 @@ function generateApiKey() {
const key = bytes.toString("base64url");
// console.log(key)
- return `ub_key_${key}`
+ return `${prefix}${key}`
}
diff --git a/backend/utils/emailService.js b/backend/utils/emailService.js
index 7a0eee2bb..8ebbb0afc 100644
--- a/backend/utils/emailService.js
+++ b/backend/utils/emailService.js
@@ -3,65 +3,127 @@ const { Resend } = require('resend');
const dotenv = require('dotenv');
dotenv.config();
-const resend = new Resend(process.env.RESEND_API_KEY || 're_dummy_key_for_testing');
+const resend = new Resend(process.env.RESEND_API_KEY_2 || process.env.RESEND_API_KEY || 're_dummy_key_for_testing');
-async function sendOtp(email, otp, { subject = "Verify your urBackend account" } = {}) {
+async function sendOtp(email, otp, { subject = "Verify your urBackend account", customContent = null } = {}) {
try {
+ const htmlContent = customContent || `
+
+
+
+
+
+
+
+
urBackend
+
Verify your account
+
+ Use the following code to complete your verification process. This code will expire in 5 minutes.
+
+
${otp}
+
+ If you didn't request this code, you can safely ignore this email.
+
+
+
+
+
+ `;
+
const { data, error } = await resend.emails.send({
- from: 'urBackend ',
+ from: 'urBackend ',
to: email,
subject: subject,
- html: `
-
-
-
-
- Verify your account
-
-
-
- Welcome to urBackend . Use the OTP below to complete your verification.
-
-
-
-
- ${otp}
-
-
+ html: htmlContent,
+ replyTo: 'urbackend@apps.bitbros.in',
+ });
-
- This OTP is valid for 5 minutes. Do not share it with anyone.
-
+ if (error) {
+ console.error("[Resend Error]", error);
+ throw new Error(error.message || "Failed to send email");
+ }
+ return { data };
+ } catch (error) {
+ console.error("[Email Service Error]", error);
+ throw error;
+ }
+}
-
+const escapeHtml = (unsafe) => {
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+};
-
- If you didn’t request this, you can safely ignore this email.
-
+async function sendReleaseEmail(email, { version, title, content }) {
+ const sVersion = escapeHtml(version);
+ const sTitle = escapeHtml(title);
+ const sContent = escapeHtml(content);
-
- © ${new Date().getFullYear()} urBackend
-
+ try {
+ const htmlContent = `
+
+
+
+
+
+
+
-
- `,
- replyTo: 'urbackend@bitbros.in',
+
+
+ `;
+
+ const { data, error } = await resend.emails.send({
+ from: 'urBackend
',
+ to: email,
+ subject: `Release: ${version} - ${title}`,
+ html: htmlContent,
+ replyTo: 'urbackend@apps.bitbros.in',
});
- console.log(data);
- console.log(error);
+ if (error) {
+ console.error("[Resend Error]", error);
+ throw new Error(error.message || "Failed to send release email");
+ }
+ return { data };
} catch (error) {
- console.log(error);
+ console.error("[Release Email Error]", error);
+ throw error;
}
}
-module.exports = sendOtp;
\ No newline at end of file
+module.exports = { sendOtp, sendReleaseEmail };
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ea22b58ee..1e667c254 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "frontend",
- "version": "0.0.0",
+ "version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frontend",
- "version": "0.0.0",
+ "version": "0.1.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
@@ -15,12 +15,15 @@
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.13.2",
+ "framer-motion": "^12.35.0",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
+ "react-markdown": "^10.1.0",
"react-router-dom": "^7.9.6",
"recharts": "^3.5.1",
+ "remark-gfm": "^4.0.1",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
@@ -1793,12 +1796,39 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1806,11 +1836,25 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1826,12 +1870,24 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
"node_modules/@vitejs/plugin-react": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz",
@@ -1933,6 +1989,16 @@
"proxy-from-env": "^1.1.0"
}
},
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2039,6 +2105,16 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2056,6 +2132,46 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -2097,6 +2213,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2266,7 +2392,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -2286,6 +2411,19 @@
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2302,6 +2440,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2311,6 +2458,19 @@
"node": ">=8"
}
},
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2638,6 +2798,16 @@
"node": ">=4.0"
}
},
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -2654,6 +2824,12 @@
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2779,6 +2955,33 @@
"node": ">= 6"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.35.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.0.tgz",
+ "integrity": "sha512-w8hghCMQ4oq10j6aZh3U2yeEQv5K69O/seDI/41PK4HtgkLrcBovUNc0ayBC3UyyU7V1mrY2yLzvYdWJX9pGZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.35.0",
+ "motion-utils": "^12.29.2",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2951,6 +3154,46 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -2968,6 +3211,16 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3015,6 +3268,12 @@
"node": ">=0.8.19"
}
},
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@@ -3024,6 +3283,40 @@
"node": ">=12"
}
},
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3047,6 +3340,28 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3426,6 +3741,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3454,6 +3779,16 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -3463,68 +3798,927 @@
"node": ">= 0.4"
}
},
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"engines": {
- "node": ">= 0.6"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
"license": "MIT",
"dependencies": {
- "mime-db": "1.52.0"
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
},
- "engines": {
- "node": ">= 0.6"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "license": "ISC",
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
},
- "engines": {
- "node": "*"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
"license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
},
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "12.35.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.35.0.tgz",
+ "integrity": "sha512-FFMLEnIejK/zDABn+vqGVAUN4T0+3fw+cVAY8MMT65yR+j5uMuvWdd4npACWhh94OVWQs79CrBBuwOwGRZAQiA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.29.2"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.29.2",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
+ "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"license": "MIT"
@@ -3599,6 +4793,31 @@
"node": ">=6"
}
},
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3675,6 +4894,16 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3736,6 +4965,33 @@
"license": "MIT",
"peer": true
},
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
@@ -3852,6 +5108,72 @@
"redux": "^5.0.0"
}
},
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
@@ -3963,6 +5285,30 @@
"node": ">=0.10.0"
}
},
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -3976,6 +5322,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -4030,6 +5394,26 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -4049,6 +5433,93 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
@@ -4099,6 +5570,34 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
@@ -4263,6 +5762,16 @@
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index 29b18021f..ce808562f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,12 +17,15 @@
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.13.2",
+ "framer-motion": "^12.35.0",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
+ "react-markdown": "^10.1.0",
"react-router-dom": "^7.9.6",
"recharts": "^3.5.1",
+ "remark-gfm": "^4.0.1",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
@@ -36,4 +39,4 @@
"globals": "^16.5.0",
"vite": "^7.2.4"
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 9a148a6b5..15e945fbd 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -11,6 +11,8 @@ import CreateProject from './pages/CreateProject';
import CreateCollection from './pages/CreateCollection';
import NotFound from './pages/NotFound';
import Analytics from './pages/Analytics';
+import Releases from './pages/Releases';
+import AdminCreateRelease from './pages/AdminCreateRelease';
import Database from './pages/Database';
import Storage from './pages/Storage';
@@ -107,6 +109,20 @@ function App() {
} />
+
+
+
+ } />
+
+
+
+
+
+
+ } />
+
} />
diff --git a/frontend/src/components/Layout/Footer.jsx b/frontend/src/components/Layout/Footer.jsx
index 804490ebc..e39bb6a2e 100644
--- a/frontend/src/components/Layout/Footer.jsx
+++ b/frontend/src/components/Layout/Footer.jsx
@@ -1,5 +1,6 @@
import { Link } from 'react-router-dom';
import { Github, ArrowRight, Database } from 'lucide-react';
+import { ADMIN_EMAIL } from '../../config';
export default function Footer() {
return (
@@ -40,7 +41,7 @@ export default function Footer() {
Connect
Discord
Github
- Email
+ Email
diff --git a/frontend/src/components/Layout/Sidebar.jsx b/frontend/src/components/Layout/Sidebar.jsx
index eba7252ea..2b8e3b79b 100644
--- a/frontend/src/components/Layout/Sidebar.jsx
+++ b/frontend/src/components/Layout/Sidebar.jsx
@@ -2,7 +2,7 @@ import { Link, useLocation, useParams } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import {
LayoutDashboard, Database, Shield, HardDrive, Settings, BarChart2,
- ArrowLeft, FileText, UserCog, LogOut, X // Import X for close
+ ArrowLeft, FileText, UserCog, LogOut, X, Rocket // Import Rocket
} from 'lucide-react';
function Sidebar({ logo, isOpen, onClose }) { // Props received
@@ -76,6 +76,9 @@ function Sidebar({ logo, isOpen, onClose }) { // Props received
Account Settings
+
+ Changelog
+
>
)}
diff --git a/frontend/src/components/TryItPanel.jsx b/frontend/src/components/TryItPanel.jsx
index a0a80c6b9..2e9633837 100644
--- a/frontend/src/components/TryItPanel.jsx
+++ b/frontend/src/components/TryItPanel.jsx
@@ -20,7 +20,7 @@ export default function TryItPanel({ endpoint, method = "POST" }) {
async function sendRequest() {
// Stop if API key is missing
if (!apiKey || apiKey.trim() === "") {
- setError("You need an API key. Go to Dashboard → Create Project → Copy API key.");
+ setError("You need an API key. Go to your Project Details to copy your Publishable or Secret key.");
return;
}
@@ -60,7 +60,7 @@ export default function TryItPanel({ endpoint, method = "POST" }) {
className="input-field"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
- placeholder="YOUR API KEY"
+ placeholder="pk_live_... OR sk_live_..."
/>
{endpoint.includes(":") && (
diff --git a/frontend/src/config.js b/frontend/src/config.js
index 8fcd2c990..f84d25eb3 100644
--- a/frontend/src/config.js
+++ b/frontend/src/config.js
@@ -1,2 +1,3 @@
// frontend/src/config.js
-export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:1234';
\ No newline at end of file
+export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:1234';
+export const ADMIN_EMAIL = 'yashpouranik124@gmail.com';
\ No newline at end of file
diff --git a/frontend/src/pages/AdminCreateRelease.jsx b/frontend/src/pages/AdminCreateRelease.jsx
new file mode 100644
index 000000000..9212c4cc7
--- /dev/null
+++ b/frontend/src/pages/AdminCreateRelease.jsx
@@ -0,0 +1,124 @@
+import { useState, useEffect } from 'react';
+import axios from 'axios';
+import { API_URL, ADMIN_EMAIL } from '../config';
+import { useAuth } from '../context/AuthContext';
+import { useNavigate } from 'react-router-dom';
+import toast from 'react-hot-toast';
+import { Send, ArrowLeft, AlertCircle } from 'lucide-react';
+
+export default function AdminCreateRelease() {
+ const { user, token } = useAuth();
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(false);
+ const [formData, setFormData] = useState({
+ version: '',
+ title: '',
+ content: ''
+ });
+
+ const isAdmin = user?.email === ADMIN_EMAIL;
+
+ useEffect(() => {
+ if (!isAdmin) {
+ toast.error("Unauthorized access.");
+ navigate('/releases');
+ }
+ }, [isAdmin, navigate]);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!formData.version || !formData.title || !formData.content) {
+ return toast.error("Please fill all fields.");
+ }
+
+ setLoading(true);
+ const loadToast = toast.loading("Publishing release and queuing emails...");
+
+ try {
+ const res = await axios.post(`${API_URL}/api/releases`, formData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ toast.dismiss(loadToast);
+ toast.success(res.data.message);
+ navigate('/releases');
+ } catch (err) {
+ toast.dismiss(loadToast);
+ toast.error(err.response?.data?.error || "Failed to publish release.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (!isAdmin) return null;
+
+ return (
+
+
navigate('/releases')}
+ style={{ background: 'transparent', border: 'none', color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', marginBottom: '24px', fontSize: '1rem' }}
+ >
+ Back to Changelog
+
+
+
Create New Release
+
+ This will be visible on the public changelog and sent to all verified users.
+
+
+
+
+
+ Warning: Emails will be queued immediately upon submission.
+ Due to rate limits, it may take several hours to reach all users.
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/CreateProject.jsx b/frontend/src/pages/CreateProject.jsx
index f3041c24e..ca2f8f611 100644
--- a/frontend/src/pages/CreateProject.jsx
+++ b/frontend/src/pages/CreateProject.jsx
@@ -94,38 +94,34 @@ function CreateProject() {
-
Save this API Key immediately
+
Save these API Keys immediately
- For security reasons, this key will only be shown once . If you lose it, you will need to regenerate it, which may interrupt your application.
+ For security reasons, these keys will only be shown once . If you lose them, you will need to regenerate them.
+
+
+ Publishable API Key (Frontend safe)
+
+
+
+ {newProject.publishableKey}
+
+
copyToClipboard(newProject.publishableKey)} className="btn btn-secondary" title="Copy Publishable API Key" style={{ height: 'auto', padding: '0 15px' }}>
+
+
+
-
Anon Public Key
-
-
- {newProject.apiKey}
+
+ Secret API Key (Backend only)
+
+
+
+ {newProject.secretKey}
-
copyToClipboard(newProject.apiKey)}
- className="btn btn-secondary"
- title="Copy API Key"
- style={{ height: 'auto', padding: '0 15px' }}
- >
-
-
+
copyToClipboard(newProject.secretKey)} className="btn btn-secondary" style={{ height: 'auto', padding: '0 15px', color: '#ef4444', border: '1px solid rgba(239, 68, 68, 0.3)' }} title="Copy Secret API Key">
diff --git a/frontend/src/pages/LandingPage/index.jsx b/frontend/src/pages/LandingPage/index.jsx
index a8c84ce26..d6d3243a6 100644
--- a/frontend/src/pages/LandingPage/index.jsx
+++ b/frontend/src/pages/LandingPage/index.jsx
@@ -22,6 +22,7 @@ import {
ChevronUp,
Play
} from 'lucide-react';
+import { motion as Motion } from 'framer-motion';
import Footer from '../../components/Layout/Footer';
import './style.css';
@@ -150,25 +151,46 @@ function LandingPage() {
Public Alpha v0.1.0
-
+
Instant Backend.
Just for Frontend Devs.
-
-
-
+
+
+
No boilerplate. No servers. Get Database, Auth, and Storage APIs in seconds.
-
-
-
+
+
+
Start Building
Documentation
-
-
-
+
+
+
@@ -198,9 +220,9 @@ function LandingPage() {
) : apiResponse ? (
<>
// Status: 200 OK • Time: {apiResponse.time}
-
+
{JSON.stringify(apiResponse.data, null, 2)}
-
+
>
) : (
@@ -210,11 +232,23 @@ function LandingPage() {
)}
+
+
+
+
+
Integrates with your favorite stack
+
+
Flutter
+
React
+
Vue
+
+
Webflow
-
-
+
+
+
Backend Architecture, Simplified.
We handle the complex infrastructure so you can ship professional apps faster.
@@ -249,8 +283,9 @@ function LandingPage() {
-
-
+
+
+
Complete Backend Suite
Enterprise-grade tools packaged for individual developers.
@@ -389,8 +424,57 @@ function LandingPage() {
-
+
+
+
+
Transparent Pricing
+
Start building for free. Scale as you grow.
+
+
+
+
+
ACTIVE
+
Developer Beta
+
$0/mo
+
Perfect for side projects, MVPs, and learning.
+
+ Unlimited Projects
+ 500MB Storage
+ Community Support
+ BYO Infrastructure
+
+
Get Started Now
+
+
+
+
Pro Plan
+
Coming Soon
+
For growing teams and production-grade apps.
+
+ Custom Domains
+ Advanced Analytics
+ Priority Support
+ Automated Backups
+
+
+
+
+
+
+
+
Ready to ship?
+
Join hundreds of developers building the future without the backend headaches.
+
+ Create Your Project
+ Back to Console
+
+
+
+
+
+
+
Common Questions
diff --git a/frontend/src/pages/LandingPage/style.css b/frontend/src/pages/LandingPage/style.css
index 4ea291597..42d748cd9 100644
--- a/frontend/src/pages/LandingPage/style.css
+++ b/frontend/src/pages/LandingPage/style.css
@@ -1,12 +1,12 @@
/* --- LANDING SPECIFIC STYLES --- */
.landing-page {
- background-color: #000;
- color: #f7f7f8;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+ background-color: #030303;
+ color: #fcfcfc;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
- letter-spacing: -0.015em;
- line-height: 1.5;
+ letter-spacing: -0.01em;
+ line-height: 1.6;
position: relative;
}
@@ -28,17 +28,28 @@
.hero-glow {
position: absolute;
- top: -20%;
+ top: -15%;
left: 50%;
transform: translateX(-50%);
- width: 600px;
- height: 600px;
- background: radial-gradient(circle, rgba(62,207,142,0.15) 0%, rgba(64,158,255,0.05) 50%, transparent 70%);
+ width: 800px;
+ height: 800px;
+ background: radial-gradient(circle, rgba(62,207,142,0.1) 0%, rgba(64,158,255,0.06) 40%, transparent 70%);
border-radius: 50%;
- filter: blur(80px);
+ filter: blur(100px);
+ z-index: 0;
+ pointer-events: none;
+ animation: pulseGlow 10s ease-in-out infinite alternate;
+}
+
+.section-glow {
+ position: absolute;
+ width: 800px;
+ height: 800px;
+ background: radial-gradient(circle, rgba(62,207,142,0.08) 0%, transparent 70%);
+ border-radius: 50%;
+ filter: blur(100px);
z-index: 0;
pointer-events: none;
- animation: pulseGlow 8s ease-in-out infinite alternate;
}
@keyframes pulseGlow {
@@ -48,14 +59,14 @@
/* NAVBAR */
.nav-glass {
- background: rgba(0, 0, 0, 0.6);
- backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
- border-bottom: 1px solid rgba(255, 255, 255, 0.08); /* slightly stronger subtle border */
+ background: rgba(3, 3, 3, 0.7);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
position: sticky;
top: 0;
- z-index: 50;
- transition: transform 0.3s ease, background 0.3s ease;
+ z-index: 100;
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav-hidden {
@@ -131,7 +142,7 @@
}
.btn-hero-secondary {
- background: rgba(255,255,255,0.03);
+ background: rgba(255,255,255,0.02);
color: #fff;
padding: 0.8rem 2.5rem;
font-size: 1.05rem;
@@ -141,8 +152,8 @@
display: flex;
align-items: center;
justify-content: center;
- border: 1px solid rgba(255,255,255,0.1);
- transition: all 0.3s;
+ border: 1px solid rgba(255,255,255,0.05);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
}
@@ -209,13 +220,13 @@
.demo-wrapper {
margin-top: 5rem;
width: 100%;
- max-width: 900px;
+ max-width: 1000px;
background: #000;
- border: 1px solid rgba(255, 255, 255, 0.08); /* sleek ultra-thin border */
- border-radius: 12px;
- box-shadow: 0 30px 60px -15px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255,255,255,0.02) inset;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ border-radius: 16px;
+ box-shadow: 0 40px 80px -20px rgba(0, 0, 0, 0.9), 0 0 0 1px rgba(255,255,255,0.01) inset;
overflow: hidden;
- animation: fadeInUp 1.2s ease-out;
+ animation: fadeInUp 1.2s cubic-bezier(0.2, 0.8, 0.2, 1);
position: relative;
}
@@ -449,12 +460,40 @@
overflow: hidden;
}
-.use-case-card:hover, .glass-card:hover {
+.use-case-card:hover, .glass-card:hover, .pricing-card:hover {
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-4px);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 20px 40px -15px rgba(0,0,0,0.6);
}
+.pricing-card {
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.integration-section div {
+ transition: all 0.3s ease;
+}
+
+.integration-section div:hover {
+ opacity: 1 !important;
+ transform: translateY(-2px);
+ color: #3ECF8E !important;
+}
+
+.demo-main pre {
+ scrollbar-width: thin;
+ scrollbar-color: #333 transparent;
+}
+
+.demo-main pre::-webkit-scrollbar {
+ height: 4px;
+}
+
+.demo-main pre::-webkit-scrollbar-thumb {
+ background: #333;
+ border-radius: 10px;
+}
+
/* FAQ */
.faq-item {
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
diff --git a/frontend/src/pages/ProjectDetails.jsx b/frontend/src/pages/ProjectDetails.jsx
index 45d640e7f..81c71ddc2 100644
--- a/frontend/src/pages/ProjectDetails.jsx
+++ b/frontend/src/pages/ProjectDetails.jsx
@@ -17,7 +17,7 @@ function ProjectDetails() {
const [project, setProject] = useState(null);
const [loading, setLoading] = useState(true);
const [newKey, setNewKey] = useState(null);
- const [isRegenerating, setIsRegenerating] = useState(false);
+ const [isRegenerating, setIsRegenerating] = useState(null); // 'publishable', 'secret', or null
useEffect(() => {
const fetchProject = async () => {
@@ -35,20 +35,20 @@ function ProjectDetails() {
fetchProject();
}, [projectId, token]);
- const handleRegenerateKey = async () => {
- if (!window.confirm("Are you sure? The old key will stop working immediately.")) return;
+ const handleRegenerateKey = async (keyType) => {
+ if (!window.confirm(`Are you sure you want to roll your ${keyType} key? The old key will stop working immediately.`)) return;
- setIsRegenerating(true);
+ setIsRegenerating(keyType);
try {
- const res = await axios.patch(`${API_URL}/api/projects/${projectId}/regenerate-key`, {}, {
+ const res = await axios.patch(`${API_URL}/api/projects/${projectId}/regenerate-key`, { keyType }, {
headers: { Authorization: `Bearer ${token}` }
});
- setNewKey(res.data.apiKey);
- toast.success("New API Key Generated!");
+ setNewKey({ key: res.data.apiKey, type: keyType });
+ toast.success(`New ${keyType === 'publishable' ? 'Publishable' : 'Secret'} API Key Generated!`);
} catch {
toast.error("Failed to regenerate key");
} finally {
- setIsRegenerating(false);
+ setIsRegenerating(null);
}
};
@@ -76,12 +76,12 @@ function ProjectDetails() {
display: 'flex', justifyContent: 'center', alignItems: 'center',
backdropFilter: 'blur(5px)'
}}>
-
+
-
+
-
New API Key Generated
+
New {newKey.type === 'publishable' ? 'Publishable' : 'Secret'} Key Generated
Please copy this key immediately. For security reasons, it will not be displayed again.
@@ -93,15 +93,15 @@ function ProjectDetails() {
background: 'var(--color-bg-input)',
border: '1px solid var(--color-border)',
borderRadius: '8px',
- color: '#3ECF8E',
+ color: newKey.type === 'secret' ? '#ef4444' : '#3ECF8E',
fontFamily: 'monospace',
wordBreak: 'break-all',
fontSize: '0.9rem'
}}>
- {newKey}
+ {newKey.key}
copyToClipboard(newKey)}
+ onClick={() => copyToClipboard(newKey.key)}
style={{ position: 'absolute', top: '8px', right: '8px', background: 'rgba(255,255,255,0.1)', border: 'none', color: '#fff', padding: '6px', borderRadius: '4px', cursor: 'pointer' }}
title="Copy"
>
@@ -165,45 +165,38 @@ function ProjectDetails() {
+
+
+
+ Publishable API Key
+
+ handleRegenerateKey('publishable')} disabled={!!isRegenerating} style={{ background: 'none', border: 'none', color: 'var(--color-text-muted)', fontSize: '0.8rem', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '4px' }}>
+ Roll Key
+
+
+
+
Safe for frontend use (React, Next.js). Read-only access.
+
+
-
+
Secret API Key
-
- {isRegenerating ? : } Roll Key
+ handleRegenerateKey('secret')} disabled={!!isRegenerating} style={{ background: 'none', border: 'none', color: 'var(--color-text-muted)', fontSize: '0.8rem', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '4px' }}>
+ Roll Key
-
-
-
-
-
- This key grants full write access. Keep it secure in your backend environment (.env).
-
+
+
+
Grants full write/delete access. Keep secure in your backend (.env).
diff --git a/frontend/src/pages/ProjectSettings.jsx b/frontend/src/pages/ProjectSettings.jsx
index 386bf4f42..abebda62e 100644
--- a/frontend/src/pages/ProjectSettings.jsx
+++ b/frontend/src/pages/ProjectSettings.jsx
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from "react-router-dom";
import axios from "axios";
import { useAuth } from "../context/AuthContext";
import toast from "react-hot-toast";
-import { Trash2, AlertTriangle, Save, CheckCircle, Copy, Server } from "lucide-react";
+import { Trash2, AlertTriangle, Save, CheckCircle, Copy, Server, Globe, Plus, X } from "lucide-react";
import { API_URL } from "../config";
import ConfirmationModal from "./ConfirmationModal";
@@ -183,7 +183,13 @@ export default function ProjectSettings() {
{/* External Configuration */}
-
+
+
);
}
+
+function AllowedDomainsForm({ project, projectId, token, onProjectUpdate }) {
+ const [domains, setDomains] = useState(project?.allowedDomains || []);
+ const [newDomain, setNewDomain] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ if (project?.allowedDomains) {
+ setDomains(project.allowedDomains);
+ }
+ }, [project]);
+
+ const handleUpdate = async (updatedDomains) => {
+ setLoading(true);
+ try {
+ await axios.patch(
+ `${API_URL}/api/projects/${projectId}/allowed-domains`,
+ { domains: updatedDomains },
+ { headers: { Authorization: `Bearer ${token}` } }
+ );
+ toast.success("Allowed domains updated!");
+ setDomains(updatedDomains);
+ onProjectUpdate((prev) => ({ ...prev, allowedDomains: updatedDomains }));
+ } catch (err) {
+ toast.error(err.response?.data?.error || "Failed to update allowed domains");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const addDomain = () => {
+ const domain = newDomain.trim();
+ if (!domain) return;
+
+ // basic validation
+ if (domains.includes(domain)) {
+ return toast.error("Domain already added");
+ }
+
+ const updated = [...domains, domain];
+ handleUpdate(updated);
+ setNewDomain("");
+ };
+
+ const removeDomain = (domainToRemove) => {
+ const updated = domains.filter((d) => d !== domainToRemove);
+ handleUpdate(updated);
+ };
+
+ return (
+
+
+
+
+
+ Allowed Domains (CORS)
+
+
+ Restrict which websites can use your Publishable API Key .
+ Use * to allow all domains, or specify domains like https://example.com or *.example.com.
+
+
+
+
+
+
setNewDomain(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ addDomain();
+ }
+ }}
+ style={{
+ flex: 1,
+ padding: "10px 12px",
+ background: "var(--color-bg-input)",
+ border: "1px solid var(--color-border)",
+ borderRadius: "8px",
+ color: "#fff",
+ }}
+ />
+
+ Add
+
+
+
+ {domains.length === 0 ? (
+
+ No domains configured. Your publishable key won't work on the web until you add * or specific domains.
+
+ ) : (
+
+ {domains.map((domain) => (
+
+
+ {domain === "*" ? (
+
+ ALLOW ALL (*)
+
+ ) : (
+
{domain}
+ )}
+
+ removeDomain(domain)}
+ disabled={loading}
+ aria-label="Remove domain"
+ style={{
+ background: "transparent",
+ border: "none",
+ color: "var(--color-text-muted)",
+ cursor: "pointer",
+ padding: "4px",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ borderRadius: "4px"
+ }}
+ onMouseOver={(e) => e.currentTarget.style.color = "#ea5455"}
+ onMouseOut={(e) => e.currentTarget.style.color = "var(--color-text-muted)"}
+ >
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/pages/Releases.jsx b/frontend/src/pages/Releases.jsx
new file mode 100644
index 000000000..bf8f0470b
--- /dev/null
+++ b/frontend/src/pages/Releases.jsx
@@ -0,0 +1,192 @@
+import { useState, useEffect } from 'react';
+import axios from 'axios';
+import { API_URL, ADMIN_EMAIL } from '../config';
+import { useAuth } from '../context/AuthContext';
+import { Rocket, Clock, Plus } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+
+export default function Releases() {
+ const [releases, setReleases] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const { user } = useAuth();
+ const navigate = useNavigate();
+
+ const isAdmin = user?.email === ADMIN_EMAIL;
+
+ useEffect(() => {
+ const fetchReleases = async () => {
+ try {
+ const res = await axios.get(`${API_URL}/api/releases`);
+ setReleases(res.data);
+ } catch (err) {
+ console.error("Failed to fetch releases", err);
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchReleases();
+ }, []);
+
+ return (
+
+
+
+
Changelog
+
+ The latest updates and improvements to urBackend.
+
+
+ {isAdmin && (
+
navigate('/admin/create-release')}
+ className="btn btn-primary"
+ style={{ gap: '8px' }}
+ >
+ New Release
+
+ )}
+
+
+ {loading ? (
+
+ ) : releases.length === 0 ? (
+
+
+
No releases yet
+
Stay tuned for our first major update!
+
+ ) : (
+
+ {releases.map((release) => (
+
+
+
+
+
+ {release.version}
+
+
+ {new Date(release.createdAt).toLocaleDateString()}
+
+
+
+
{release.title}
+
+
+
+ {release.content}
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Signup.jsx b/frontend/src/pages/Signup.jsx
index 67543120a..72521621a 100644
--- a/frontend/src/pages/Signup.jsx
+++ b/frontend/src/pages/Signup.jsx
@@ -34,9 +34,7 @@ function Signup() {
toast.dismiss(loadingToast);
toast.success(response.data.message);
- await axios.post(`${API_URL}/api/auth/send-otp`, { email: formData.email });
-
- toast.success("OTP Sent! Please verify your email.");
+ // Redirect to OTP verification page, which automatically sends the OTP
navigate('/verify-otp', { state: { email: formData.email } });
} catch (err) {