@thezelijah/majik-user
v1.0.6
Published
Majik User is a framework-agnostic, self-defending user domain model for managing identity, profile data, verification state, and application settings in modern apps and websites. It’s designed to be extensible, serializable, and safe by default, acting a
Downloads
659
Maintainers
Readme
Majik User
Majik User is a framework-agnostic, security-hardened user domain model for modern applications. It provides a strongly typed foundation for managing identity, profile data, and settings, that enforces plain-text input, aggressively validates user-controlled fields, and reduces XSS risk by default through strict domain-level policies.
This package is designed to be the isomorphic source of truth—ensuring that user data remains clean, validated, and secure as it moves between your frontend, backend, and database.
- Majik User
Why Majik User?
Secure by Default: Enforces a strict plain-text input policy, protocol-safe URI validation, and defensive sanitization for externally sourced data—reducing XSS risk at the domain layer.
Most apps scatter user logic across:
- database schemas
- auth provider objects
- API DTOs
- frontend state
Majik User centralizes all of that logic into one predictable, reusable domain object.
It is:
- Strongly typed (TypeScript-first)
- Serializable and persistence-ready
- Extensible via generics
- Safe by default (public vs private data)
- Compatible with Supabase (optional)
Features
Security & Integrity
- Plain-Text Enforcement: HTML and unsafe markup are rejected or stripped from user-controlled fields.
- XSS Risk Mitigation: Optional DOMPurify integration normalizes externally sourced input into safe plain text.
- Defensive Setters: Setters validate and normalize data before it reaches internal state.
- Safe URI Enforcement: Profile pictures and social links are restricted to safe protocols (
https, base64, etc.), blocking javascript: injection. - Readonly State: Getters return deep copies or readonly versions of data to prevent accidental state mutation.
Core User Management
- Unique user ID generation (UUID)
- Email + display name validation
- Automatic timestamps (
createdAt,lastUpdate) - SHA-256–based hashed identifier
Rich Profile Metadata
- Full name handling (first, middle, last, suffix)
- Profile picture, bio, phone, gender
- Birthdate with age calculation
- Address formatting
- Social links
- Language and timezone preferences
Verification System
- Email verification
- Phone verification
- Identity (KYC-style) verification
- Combined
isFullyVerifiedstatus
Settings & Restrictions
- Notification preferences
- System-level restrictions
- Temporary or permanent account restriction
- Restriction expiration checks
Serialization & Interop
toJSON()for database persistencefromJSON()for hydrationtoPublicJSON()for safe public exposuretoSupabaseJSON()andfromSupabase()helpers
Developer Ergonomics
- Generic metadata support
- Profile completeness scoring
- Built-in validation with error reporting
- Cloneable user instances
- Designed to be subclassed
Installation
npm install @thezelijah/majik-userUsing Cloudflare Workers?
Majik User uses isomorphic-dompurify for high-grade security. To run this in a Cloudflare Worker environment, you must enable Node.js compatibility in your wrangler configuration.
Add the following to your wrangler.json (or .toml):
{
"$schema": "node_modules/wrangler/config-schema.json",
"compatibility_date": "2025-09-27",
"compatibility_flags": ["nodejs_compat"]
}Usage
import { MajikUser } from "@thezelijah/majik-user";Initializing a New User
const user = MajikUser.initialize(
"[email protected]",
"Zelijah"
);
What this does:
- Generates a UUID if no ID is provided
- Hashes the user ID
- Sets default metadata and settings
- Sets createdAt and lastUpdate
- Validates email and display name
You can optionally provide your own ID:
const generatedID: string = customIDGenerator();
const user = MajikUser.initialize(
"[email protected]",
"Zelijah",
generatedID
);
Updating User Data
Security in Action
// 1. Protection against XSS
try {
user.displayName = "<script>alert('hacked')</script> Josef";
} catch (e) {
// Throws: "Display name contains suspicious HTML tags"
}
// 2. Protocol Safety
try {
user.setPicture("javascript:alert('xss')");
} catch (e) {
// Throws: "Invalid or unsafe URL protocol detected."
}
// 3. Auto-Sanitization on Metadata
user.setMetadata("bio", "I love <b>coding</b> <img src=x onerror=alert(1)>");
console.log(user.metadata.bio);
// Output: "I love <b>coding</b>" (Harmful tags stripped automatically)
Basic Info
user.email = "[email protected]";
user.displayName = "Josef";
Changing email or phone automatically marks them as unverified.
Profile Metadata
user.setName({
first_name: "Josef",
last_name: "Fabian",
});
user.setBio("Creative technologist and builder.");
user.setPicture("https://thezelijah.world/avatar.png");
user.setPhone("+639123456789");
user.setGender("male");
user.setLanguage("en");
user.setTimezone("Asia/Manila");
Birthdate
user.setBirthdate("1995-10-26");
// or
user.setBirthdate(new Date("1995-10-26"));
You can then access:
user.age; // number | null
user.birthday; // YYYY-MM-DD | null
Address
user.setAddress({
street: "123 Main St",
city: "Manila",
country: "PH",
});
You can then access:
user.address; // "123 ABC St, Manila, PH"
Social Links
user.setSocialLink("Instagram", "https://instagram.com/thezelijah");
user.removeSocialLink("Instagram");
Verification Methods
user.verifyEmail();
user.verifyPhone();
user.verifyIdentity();
user.isEmailVerified;
user.isFullyVerified;
You can also unverify:
user.unverifyEmail();
user.unverifyPhone();
user.unverifyIdentity();
Restricting a user
// Restrict indefinitely
user.restrict();
// Restrict until a specific date
user.restrict(new Date("2026-01-01"));
user.isCurrentlyRestricted(); // boolean
To remove restriction:
user.unrestrict();
Reading Computed Properties
user.fullName;
user.formattedName;
user.initials;
user.profileCompletionPercentage;
user.hasCompleteProfile();
Validation
This validates not just formats, but also inspects the entire user object (including nested addresses and social links) for unsafe markup and suspicious input.
const result = user.validate();
if (!result.isValid) {
console.log(result.errors);
}
This validates:
- Required fields
- Email format
- Dates
- Phone number format
- Birthdate format
Serialization & Parsing
Serialize for storage
const json = user.toJSON();
This output is safe to store in:
- SQL
- NoSQL
- APIs
- Files
Parse from JSON
const user = MajikUser.fromJSON(json);
// or
const user = MajikUser.fromJSON(jsonString);
Public-safe JSON (no sensitive data)
const publicUser = user.toPublicJSON();
Includes only:
- id
- displayName
- picture
- bio
- createdAt
Perfect for feeds, comments, and public profiles.
Extending Majik User
Majik User is generic-first and designed to be extended.
interface MyAppUserMetadata extends UserBasicInformation {
role: "admin" | "user";
subscriptionTier?: string;
}
class MyAppUser extends MajikUser<MyAppUserMetadata> {}
//Example
const user = MajikUser.initialize<MyAppUserMetadata>(
"[email protected]",
"Zelijah"
);
Now your app has a fully typed, domain-safe user model.
Data Integrity & Security
Majik User ensures that your data is not only well-structured but also safe and meaningful across your entire stack.
| Feature | Description |
| :--------------------- | :------------------------------------------------------------------------------------- |
| Isomorphic | Runs everywhere—Works seamlessly in the Browser, Node.js, and Edge Functions. |
| Smart Mapping | Automatically normalizes messy, flat metadata into structured, nested objects. |
| Calculated Getters | Values like .age, .initials, and .isFullyVerified are computed on the fly. |
| XSS Risk Reduction | Plain-text enforcement and optional DOMPurify normalization reduce XSS attack surface. |
Security Guarantees & Non-Guarantees
Majik User is designed to reduce risk, not to provide absolute security guarantees.
What Majik User guarantees
- User-controlled fields are treated as untrusted by default
- HTML and unsafe markup are rejected or normalized into plain text
- Public-facing data is intentionally limited and sanitized
- Input validation is enforced across initialization, mutation, and serialization
What Majik User does NOT guarantee
- Complete protection against XSS in rendering contexts
- Safety against misuse (e.g. unsafe
innerHTMLusage) - Replacement for frontend escaping, CSP, or framework-level protections
- Defense against logic bugs or application-level vulnerabilities
Majik User is intended to be used as one layer in a defense-in-depth strategy.
Supabase Integration (Optional)
Majik User is designed to sit cleanly on top of Supabase Auth, acting as your domain layer while Supabase handles authentication and sessions.
The recommended pattern is:
- Let Supabase create and authenticate the user
- Convert the Supabase user →
MajikUser - Store, validate, update, and serialize using
MajikUser
Public Signup (POST /api/users)
This example shows a public signup endpoint using Supabase Auth with email/password, followed by normalization into a MajikUser.
// POST /api/users (Public Signup)
router.post('/', async (request, env: Env): Promise<Response> => {
console.log('[POST] /users/');
const errorResponse = await applyMiddleware(request, env);
if (errorResponse instanceof Response) return errorResponse;
const body = (await request.json()) as API_SUPABASE_SIGN_UP_BODY;
if (!body?.email || !body.password || !body?.options?.data) {
return error('Missing required signup fields', 400, 'MISSING_FIELDS');
}
try {
const supabase = createSupabaseAPIClient(env);
const { data, error: sbError } =
await supabase.auth.signUp(body as SignUpWithPasswordCredentials);
if (sbError) {
const isDup = sbError.message.includes('already registered');
return error(
isDup ? 'This email is already registered.' : sbError.message,
isDup ? 409 : 400,
isDup ? 'EMAIL_ALREADY_EXISTS' : undefined,
);
}
// Supabase returns a user even if the email already exists
if (!data.user?.identities || data.user.identities.length <= 0) {
return error('Email already exists. Try logging in.', 409, 'EMAIL_ALREADY_EXISTS');
}
// Normalize Supabase user → MajikUser
const userJSON = MajikUser
.fromSupabase(data.user)
.toJSON();
return jsonResponse(
{
message: 'Signup successful! Check your email.',
user: userJSON,
session: data.session,
requiresEmailConfirmation: !data.session,
},
201,
corsHeaders,
);
} catch {
return error('Internal server error', 500, 'INTERNAL_ERROR');
}
});Why this works well
- Supabase handles authentication & sessions
- Majik User becomes your single source of truth
- You get validation, normalization, and timestamps for free
- The returned user object is safe to store or cache
Fetching a User by ID (GET /api/users/:id)
This endpoint retrieves a user directly from Supabase Admin, then converts it into a MajikUser.
// GET /api/users/:id
router.get('/:id', async (request, env: Env): Promise<Response> => {
console.log('[GET] /users/:id');
const errorResponse = await applyMiddleware(request, env);
if (errorResponse instanceof Response) return errorResponse;
const { id } = request.params;
const supabase = createSupabaseAPIClient(env);
const { data, error: sbError } =
await supabase.auth.admin.getUserById(id);
if (sbError || !data?.user) {
return error('User not found', 404, 'USER_NOT_FOUND');
}
const userJSON = MajikUser
.fromSupabase(data.user)
.toJSON();
return jsonResponse(userJSON, 200, corsHeaders);
});
Notes
- Keeps Supabase-specific logic at the edge
- Everything beyond this point deals only with MajikUser
- Ideal for admin panels, dashboards, or internal APIs
Updating a User (PUT /api/users/:id)
This example demonstrates safe user updates using MajikUser validation before writing back to Supabase.
// PUT /api/users/:id
router.put('/:id', async (request, env: Env): Promise<Response> => {
console.log('[PUT] /users/:id');
const errorResponse = await applyMiddleware(request, env);
if (errorResponse instanceof Response) return errorResponse;
const { id } = request.params;
const body = (await request.json()) as MajikUserJSON;
// Parse incoming data
const parsedUser = MajikUser.fromJSON(body);
// Validate before persisting
const validate = parsedUser.validate();
if (!validate.isValid) {
return error('Invalid user data', 400, 'INVALID_USER_DATA');
}
const supabase = createSupabaseAPIClient(env);
// Convert domain object → Supabase-friendly metadata
const userJSON = parsedUser.toSupabaseJSON();
const { data, error: sbError } =
await supabase.auth.admin.updateUserById(id, {
user_metadata: { ...userJSON },
});
if (sbError || !data?.user) {
console.error('Update error:', sbError);
return error(sbError?.message || 'Update failed', 400, 'UPDATE_FAILED');
}
// Return updated, normalized user
const newUserJSON = MajikUser
.fromSupabase(data.user)
.toJSON();
return success(
newUserJSON,
`Update for ${newUserJSON.email} saved successfully.`,
);
});
Why this pattern is recommended
- Incoming data is validated before persistence
- Supabase metadata stays clean and normalized
- No leaking Supabase-specific structures to clients
- MajikUser enforces consistency across all updates
Philosophy
Majik User is:
- ❌ Not an ORM
- ❌ Not an auth system
- ❌ Not a UI state manager
It is:
- A domain model
- A shared contract
- A single source of truth for user behavior
Threat Model – Majik User
Assets Protected
Majik User is responsible for protecting:
- User identity metadata (name, email, profile info)
- User-controlled profile content (bio, social links, pictures)
- Verification state (email, phone, identity flags)
- Public-facing user representations (
toPublicJSON())
Trust Boundaries
Majik User operates across multiple trust boundaries:
- External clients (browsers, mobile apps)
- APIs and serverless functions
- Authentication providers (e.g. Supabase Auth)
- Databases and caches
All data crossing into the Majik User domain is treated as untrusted.
In-Scope Threats
1. Cross-Site Scripting (XSS)
Threat
Attackers attempt to inject HTML or script content via user-controlled fields
(e.g. display names, bios, metadata).
Mitigations
- Plain-text enforcement for user-facing fields
- Rejection or normalization of unsafe markup
- Optional DOMPurify integration for external data ingestion
- Validation during initialization, mutation, and serialization
Residual Risk
- XSS is still possible if applications render user data unsafely
(e.g.
innerHTMLwithout escaping)
2. Unsafe URL Injection
Threat
Injection of malicious URI schemes such as javascript: or data: URLs.
Mitigations
- Protocol allowlisting (
https, controlleddatausage) - URL parsing and validation before persistence
3. Accidental Data Exposure
Threat
Sensitive fields being unintentionally exposed to public APIs or clients.
Mitigations
- Explicit separation of internal vs public serialization
toPublicJSON()exposes only whitelisted fields- Defensive defaults for getters
4. State Mutation & Integrity Bugs
Threat
Unexpected mutation of internal user state causing data corruption or bypasses.
Mitigations
- Readonly getters and defensive cloning
- Controlled setters with validation
- Immutable-like update patterns
Out-of-Scope Threats
Majik User does not attempt to protect against:
- SQL injection
- Authentication bypass
- Authorization logic flaws
- CSRF
- Server-side request forgery (SSRF)
- Business logic vulnerabilities
- Client-side misuse of rendered data
These must be handled by the surrounding application and infrastructure.
Assumptions
- Consumers follow secure rendering practices
- Frontend frameworks escape content by default
- CSP is implemented where appropriate
- The library is used as intended (domain layer, not UI sanitizer)
Violating these assumptions may reintroduce risk.
Security Posture Summary
Majik User reduces risk by:
- Shrinking the XSS attack surface
- Enforcing strict domain invariants
- Making unsafe states difficult to represent
It does not claim to eliminate vulnerabilities entirely.
Security is a shared responsibility.
Contributing
If you want to contribute or help extend support to more platforms, reach out via email. All contributions are welcome!
License
Apache-2.0 — free for personal and commercial use.
Author
Made with 💙 by @thezelijah
About the Developer
- Developer: Josef Elijah Fabian
- GitHub: https://github.com/jedlsf
- Project Repository: https://github.com/jedlsf/majik-user
Contact
- Business Email: [email protected]
- Official Website: https://www.thezelijah.world
