Skip to content

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.

License

Notifications You must be signed in to change notification settings

jedlsf/majik-user

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Majik User

Developed by Zelijah GitHub Sponsors

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.

npm npm downloads npm bundle size License TypeScript



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 isFullyVerified status

Settings & Restrictions

  • Notification preferences
  • System-level restrictions
  • Temporary or permanent account restriction
  • Restriction expiration checks

Serialization & Interop

  • toJSON() for database persistence
  • fromJSON() for hydration
  • toPublicJSON() for safe public exposure
  • toSupabaseJSON() and fromSupabase() 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-user

Using 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(
  "business@thezelijah.world",
  "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(
  "business@thezelijah.world",
  "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 = "business@majikah.solutions";
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>(
  "business@thezelijah.world",
  "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 innerHTML usage)
  • 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:

  1. Let Supabase create and authenticate the user
  2. Convert the Supabase user → MajikUser
  3. 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. innerHTML without escaping)

2. Unsafe URL Injection

Threat
Injection of malicious URI schemes such as javascript: or data: URLs.

Mitigations

  • Protocol allowlisting (https, controlled data usage)
  • 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


Contact

About

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.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Packages

No packages published