npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@majikah/majik-universal-id

v0.0.10

Published

The core universal identity model for the Majikah ecosystem, featuring hybrid PQC signatures (Ed25519 + ML-DSA), ML-KEM-768 private info encryption, and multi-stage Didit verification graduation.

Readme

Majik Universal ID

Developed by Zelijah GitHub Sponsors

Majik Universal ID is a cryptographically anchored identity layer for the Majikah ecosystem. Built on top of Majik Key and Majik Signature, it provides a tamper-proof, post-quantum-ready universal identity record that binds a user, a signing key, and a multi-stage verification state into a single, integrity-hashed document.

Every MajikUniversalID is permanently bound to exactly one MajikKey bundle. Private personal information is always encrypted at rest using ML-KEM-768 (FIPS-203). Identity signing uses the hybrid Ed25519 + ML-DSA-87 scheme from Majik Signature — both algorithms must verify for any signed content to be considered authentic.

Identities can be created and managed via the web application at https://id.majikah.solutions using your own Majik Keys.

npm npm downloads License TypeScript



Security Architecture

1. Hybrid Post-Quantum Identity Signing

Every MajikUniversalID is created with a hybrid Ed25519 + ML-DSA-87 signature over its core identity fields (id, user_id, timestamp, hash). This is the same dual-algorithm architecture used by Majik Signature — both algorithms must verify for the identity record to be considered authentic. A break in either algorithm alone is not sufficient to forge a valid identity.

2. Integrity Hash

A SHA3-512 integrity hash is computed over the identity's canonical fields at creation time:

hash = SHA3-512(id : user_id : timestamp : x25519_pk : ed_pk : ml_kem_pk : ml_dsa_pk)

All four public keys from the bound MajikKey are committed into this hash. Any modification to the stored identity — including key material substitution — will fail the hash check on deserialization.

3. Private Info Encryption

Private personal information (PrivatePersonalInfo) is never stored in plaintext. It is immediately encrypted with the bound key's ML-KEM-768 public key on creation via MajikEnvelope. The encrypted envelope is the only form ever serialized or persisted. Decrypted data (rehydrated) exists only in memory and is stripped from all serialization outputs.

4. Immutability After Verification

Once a MajikUniversalID reaches any verified tier (any tier above UNVERIFIED), the identity becomes immutable. Mutating operations (signContent, signFile, syncUserRef, updateDisplayName, updateSettings, grantConsent, revokeConsent) all throw MajikUniversalIDImmutableError on a verified identity. The identity can only be modified again after calling revokeVerification(), which itself is blocked for 30 days after verification (isVerificationLocked).

5. 1:1:1 Key Binding

A MajikUniversalID is permanently bound to exactly one MajikKey bundle at creation. The bound key's fingerprint, and all four of its public keys (X25519, Ed25519, ML-KEM-768, ML-DSA-87), are committed into the integrity hash. There is no key rotation — to use a different key, a new MajikUniversalID must be created.


Overview

What is a Majik Universal ID?

A MajikUniversalID is a structured identity record that anchors:

  • A MajikUser (the account holder)
  • A MajikKey bundle (the cryptographic signing and encryption identity)
  • A verification state derived from up to five Didit verification stages
  • Encrypted private personal info (legal name, date of birth, contact details, address, government IDs)
  • A public profile (display name, avatar, bio, social handles)
  • A consent log, settings, and a signature history

The identity is self-contained and portable — it serializes to JSON or base64, carries its own public keys, and can be verified without a key registry.

Verification is performed through Didit, a third-party KYC/identity verification provider. Webhook payloads from Didit are processed by the processWebhook() method, which maps raw stage results to typed structures and graduates the identity's IDTier.

Use Cases

  • Identity Anchoring: Bind a Majikah user account to a specific MajikKey for provenance and non-repudiation
  • KYC Graduation: Track a user's verification progress across five Didit stages and automatically derive their identity tier
  • Content Provenance: Sign and verify documents, files, and API payloads as a verified Majikah identity
  • Private Data Custody: Store encrypted personal info alongside the identity — decryptable only with the bound key
  • Consent Management: Record and revoke data-sharing consents with a structured, auditable consent log
  • Majikah Ecosystem: Integrate with Majik Message and other Majikah products for identity-bound communication

Features

Security & Post-Quantum Readiness

  • Hybrid Identity Signature: Ed25519 + ML-DSA-87 over core identity fields — both must verify
  • Integrity-Hashed: SHA3-512 over all four bound public keys — tampering invalidates the hash
  • ML-KEM-768 Private Encryption: Private personal info is always encrypted at rest with a post-quantum KEM
  • Immutable After Verification: Verified identities cannot be mutated — enforced at the class boundary
  • 30-Day Revocation Lock: revokeVerification() is blocked for 30 days after verification to prevent abuse
  • No Private Key for Verification: Content and file verification uses public keys only

Identity & Verification

  • 5-Stage Didit Workflow: ID Verification → Liveness → Face Match → Phone Verification → IP Analysis
  • Automatic Tier Derivation: IDTier (UNVERIFIED → BASIC → VERIFIED → ENHANCED → TRUSTED) derived from completed stages
  • Webhook Processing: processWebhook() maps raw Didit V3 payloads to typed structures and computes user sync actions
  • AML Screening: PEP and sanctions screening from Didit AML nodes

Content Signing & Verification (via Majik Signature)

  • Sign arbitrary content: signContent() — bytes, strings, JSON payloads
  • Sign files with embedded signatures: signFile() — all formats supported by Majik Signature
  • Verify content: verifyContent(), verifyText() — both Ed25519 and ML-DSA-87 must pass
  • Verify files: verifyFile() — extracts and verifies embedded signatures
  • Signer binding enforced: All signing and verification checks the fingerprint against the bound key

Private Info

  • Encrypted at rest: PrivatePersonalInfo is always stored as an MajikEnvelope-encrypted payload
  • Session decryption: decryptPrivate(key) — decrypts and caches in memory for the session
  • Silent load decryption: fromJSON(json, { key }) — attempts decryption on load, swallows failures
  • Secure sharing: sharePrivate({ senderKey, recipients }) — re-encrypts for multiple recipients via MajikEnvelope group encryption, returns a scanner string
  • Never serialized: rehydrated is stripped from all toJSON() / toBase64() outputs

Developer Experience

  • First-Class TypeScript: Full type definitions for all interfaces, enums, and classes
  • Structured Errors: Typed error hierarchy — every public method throws a named subclass
  • JSON & Base64 Round-Trip: toJSON(), fromJSON(), toBase64(), fromBase64()
  • Public View Projection: toPublicView() — safe public projection with no private info
  • Validation: validate() — structural and integrity checks with detailed error and warning lists
  • Isomorphic: Works in Node.js 18+, modern browsers, Deno, and Bun

Installation

# Using npm
npm install @majikah/majik-universal-id

# Peer dependencies — must also be installed
npm install @majikah/majik-key
npm install @majikah/majik-signature
npm install @majikah/majik-envelope

No native bindings. Works in Node.js 18+, all modern browsers, Deno, and Bun.


Quick Start

import { MajikKey } from '@majikah/majik-key';
import { MajikUser } from '@thezelijah/majik-user';
import { MajikUniversalID } from '@majikah/majik-universal-id';

// ── Step 1: Create and unlock a MajikKey ─────────────────────────────────────
const mnemonic = MajikKey.generateMnemonic();
const key = await MajikKey.create(mnemonic, 'my-passphrase', 'My Identity Key');

// ── Step 2: Create a MajikUniversalID ─────────────────────────────────────────
const majikId = await MajikUniversalID.create(user, key, {
  account_id: 'acct_your_tenant_id',
});

console.log('ID:', majikId.id);
console.log('Tier:', majikId.tier);       // IDTier.UNVERIFIED
console.log('Hash:', majikId.hash);       // SHA3-512 integrity hash
console.log('Signer:', majikId.signingKey.fingerprint);

// ── Step 3: Access decrypted private info (available right after create()) ───
const info = majikId.privateInfo;
console.log('Legal name:', info.legal_first_name, info.legal_last_name);

// ── Step 4: Sign content ──────────────────────────────────────────────────────
const signature = await majikId.signContent('My signed document', key);

// ── Step 5: Verify content ────────────────────────────────────────────────────
const result = majikId.verifyContent('My signed document', signature);
console.log('Valid:', result.valid); // true

// ── Step 6: Serialize for storage ────────────────────────────────────────────
const json = majikId.toJSON();   // private info is stripped — never persisted
const b64  = majikId.toBase64(); // compact base64 representation

// ── Step 7: Restore from storage ─────────────────────────────────────────────
const restored = await MajikUniversalID.fromJSON(json, { key });
// If key is provided and unlocked, private info is silently decrypted on load.

API Reference

Factory Methods

MajikUniversalID.create(user, key, options)

Create a new MajikUniversalID from a MajikUser and an unlocked MajikKey. The key must be unlocked and must have all public key fields (Ed25519, ML-DSA-87, ML-KEM-768, X25519). Private personal info is encrypted immediately. The identity starts at IDTier.UNVERIFIED.

Parameters:

  • user: MajikUser — A valid, fully constructed MajikUser instance.
  • key: MajikKey — An unlocked MajikKey with all signing and encryption keys present.
  • options: CreateUniversalIDOptions
    • account_id: string — The Majikah tenant account ID. Required.
    • locale?: string — Preferred locale (e.g. "en-PH"). Defaults to "en-PH".
    • schema_version?: string — Schema version override. Defaults to the current SCHEMA_VERSION constant.

Returns: Promise<MajikUniversalID>

Throws: MajikUniversalIDKeyError if the key is locked or missing required fields. MajikUniversalIDValidationError if the user fails validation.


MajikUniversalID.fromJSON(json, options?)

Reconstruct a MajikUniversalID from its JSON representation. Validates structure and verifies the SHA3-512 integrity hash on load. Optionally attempts silent decryption of private info.

Parameters:

  • json: MajikUniversalIDJSON | string — The serialized identity JSON (object or string).
  • options?: FromJSONOptions
    • key?: MajikKey — An unlocked MajikKey to attempt private info decryption. All failures are silently swallowed — no exception is thrown on decryption failure.

Returns: Promise<MajikUniversalID>

Throws: MajikUniversalIDIntegrityError if the hash check fails. MajikUniversalIDDeserializationError if the JSON is malformed.


MajikUniversalID.fromBase64(b64, options?)

Reconstruct from a base64-serialized string. Accepts the same optional key for silent decryption.

Returns: Promise<MajikUniversalID>


Content Signing

Signing methods require isMutable === true (the identity must be at IDTier.UNVERIFIED). The provided key's fingerprint must match the bound signing_key.fingerprint.


majikId.signContent(content, key, options?)

Sign arbitrary content and return a MajikSignature. Uses the hybrid Ed25519 + ML-DSA-87 scheme from Majik Signature.

Parameters:

  • content: Uint8Array | string — Content to sign.
  • key: MajikKey — An unlocked MajikKey matching the bound fingerprint.
  • options?: SignOptions & { label?: string } — Optional content type, timestamp override, and label.

Returns: Promise<MajikSignature>

Throws: MajikUniversalIDImmutableError if the identity is verified. MajikUniversalIDSigningError on signing failure. MajikUniversalIDKeyNotFoundError if the fingerprint doesn't match.


majikId.signFile(file, key, options?)

Sign a file and embed the signature into it. Delegates to MajikSignature.signFile() — all file formats supported by Majik Signature are supported here.

Parameters:

  • file: Blob — The file to sign.
  • key: MajikKey — An unlocked MajikKey matching the bound fingerprint.
  • options? — Optional contentType, timestamp, mimeType, and label.

Returns: Promise<{ blob: Blob; signature: MajikSignature; handler: string; mimeType: string }>


Content Verification

Verification methods are read-only and have no isMutable requirement.


majikId.verifyContent(content, signature, context?)

Verify that content was signed by the key bound to this identity. Both Ed25519 and ML-DSA-87 must pass. The signer fingerprint embedded in the signature is checked against the bound key's fingerprint before cryptographic verification.

Parameters:

  • content: Uint8Array | string — The original signed content.
  • signature: MajikSignature | MajikSignatureJSON | string — The signature to verify (instance, JSON object, or base64 string).
  • context?: string — Optional context label for auditing.

Returns: ContentVerificationResult

{
  valid: boolean;
  signer_fingerprint: string;
  signer_registered: boolean;  // Whether fingerprint matched the bound key
  content_hash?: string;
  signed_at?: string;
  content_type?: string;
  reason?: string;             // Present when valid is false
}

majikId.verifyFile(file, context?)

Extract and verify a file's embedded MajikSignature against this identity's bound public keys.

Returns: Promise<FileVerificationResult> — extends ContentVerificationResult with an optional handler field (e.g. "PDF", "WAV", "MP4/MOV").


majikId.verifyText(text, signature, context?)

Convenience wrapper for verifyContent() for string content.


Private Info — Decrypt & Share


majikId.decryptPrivate(key)

Decrypt and rehydrate private personal info with the provided MajikKey. On success, isPrivateDecrypted becomes true and privateInfo is accessible. If already decrypted in this session, the cached value is returned immediately. Never throws — returns a result object.

Parameters:

  • key: MajikKey — The bound unlocked MajikKey. Fingerprint must match signing_key.fingerprint.

Returns: Promise<DecryptPrivateResult>

{
  success: boolean;
  data?: PrivatePersonalInfo;  // Present when success === true
  reason?: string;             // Present when success === false
}

majikId.sharePrivate(options)

Share private info with one or more recipients by re-encrypting it for their MajikKeys using MajikEnvelope group encryption. Returns a scanner string that each recipient can decrypt with their own MajikKey. The sender's key is always included as a recipient. Does not mutate the current instance.

Parameters:

  • options.senderKey: MajikKey — The unlocked bound MajikKey. Used to decrypt first.
  • options.recipients: MajikKey[] — One or more recipient keys. Only mlKemPublicKey is used — they do not need to be unlocked.

Returns: Promise<string> — A MajikEnvelope scanner string (~*$MJKMSG:<base64>).

Recipients can decrypt with:

const env = MajikEnvelope.fromScannerString(scannerString);
const json = await env.decrypt({ fingerprint, mlKemSecretKey });
const privateInfo: PrivatePersonalInfo = JSON.parse(json);

Didit Verification Lifecycle


majikId.processWebhook(payload, headers, secret)

Process an incoming Didit V3 webhook payload. Verifies the HMAC signature, maps the payload to typed DiditVerification structures, graduates IDTier, and returns actions for the caller to apply on the linked MajikUser.

Important: payload.vendor_data must equal MajikUniversalID.id. Set this value as vendor_data when creating the Didit session.

Parameters:

  • payload: DiditWebhookPayload — The raw Didit webhook body.
  • headers: DiditWebhookHeaders — The webhook request headers (must include x-timestamp and at least one signature header).
  • secret: string — The HMAC secret configured in the Didit dashboard.

Returns: Promise<WebhookProcessResult>

{
  success: boolean;
  session_id: string;
  session_status: string;
  previous_tier: IDTier;
  new_tier: IDTier;
  tier_changed: boolean;
  all_stages_passed: boolean;
  updated_stages: DiditStage[];
  user_sync_actions: UserSyncAction[];  // e.g. ["verifyPhone", "verifyIdentity"]
  extracted_personal_data?: { ... };   // Flattened Stage 1 personal data
}

majikId.revokeVerification(reason)

Revoke all Didit verification and reset the identity to IDTier.UNVERIFIED. Throws MajikUniversalIDVerificationLockedError if the identity was verified within the last 30 days. Safe no-op if already UNVERIFIED.

Parameters:

  • reason: string — Required justification stored in the audit trail.

majikId.requireReverification(reason?)

Flag the identity for re-verification (admin use). Does not unlock the identity or reset its tier.


majikId.isVerificationPassed(stage)

Check whether a specific DiditStage has been passed.

if (majikId.isVerificationPassed(DiditStage.LIVENESS)) {
  // liveness check was passed
}

majikId.getPassedVerifications()

Returns the list of DiditStage values that have been passed, in the order they were completed.

Returns: DiditStage[]


Serialization

majikId.toJSON()

Export the full identity as a plain MajikUniversalIDJSON object. The rehydrated field is always stripped — decrypted private info is never serialized.

majikId.toBase64()

Serialize the identity to a compact base64 string. Equivalent to objectToBase64(toJSON()).

majikId.toPublicView()

Returns a public-safe projection (MajikIDPublicView) — no private info, no signature records. Includes the public profile, tier, status, display name, and public signing key material. Also includes verification_stages, a Record<DiditStage, boolean> derived from completed_stages, where all five stage keys are always present.

majikId.validate()

Run structural and integrity checks. Returns a UniversalIDValidationResult with is_valid, errors, and warnings arrays.


Getters

Identity

| Getter | Type | Description | |---|---|---| | id | string | UUIDv7 identity record ID | | userId | string | The bound MajikUser ID | | accountId | string | The Majikah tenant account ID | | publicKey | Base64 | Bound X25519 public key, base64 (32 bytes) | | hash | SHA3_512Hash | Integrity hash over id, user_id, timestamp, and all four public keys | | timestamp | ISODateTime | ISO 8601 creation timestamp | | lastUpdate | ISODateTime | ISO 8601 timestamp of last mutation |

Verification State

| Getter | Type | Description | |---|---|---| | tier | IDTier | Current verification tier | | status | IDStatus | Current status | | isVerified | boolean | true if tier is not UNVERIFIED | | isTrusted | boolean | true if tier is TRUSTED | | isMutable | boolean | true if tier is UNVERIFIED | | verifiedAt | ISODateTime \| undefined | Timestamp of last verified tier | | isVerificationLocked | boolean | true if verified within the last 30 days | | verificationLockDaysRemaining | number | Days until the 30-day lock expires | | isRestricted | boolean | true if the identity is currently restricted | | verificationSummary | MajikIDVerificationSummary | Full verification summary including stage states |

Key & Data

| Getter | Type | Description | |---|---|---| | signingKey | Readonly<MajikKeyPublicBundle> | The bound MajikKey public bundle | | userRef | Readonly<MajikUserRef> | Cached MajikUser reference fields | | settings | Readonly<MajikIDSettings> | Identity settings | | metadata | Readonly<MajikIDMetadata> | Full metadata (private field has rehydrated stripped) |

Private Info

| Getter | Type | Description | |---|---|---| | isPrivateDecrypted | boolean | Whether private info has been decrypted this session | | privateInfo | Readonly<PrivatePersonalInfo> | Decrypted private info — throws MajikUniversalIDPrivateInfoLockedError if not yet decrypted |


ID Tiers

Tiers are automatically derived from the set of Didit stages that have been passed. A tier can only increase through webhook processing and can only decrease via revokeVerification().

| Tier | Stages Required | Description | |---|---|---| | UNVERIFIED | None | No verification stages passed. Identity is mutable. | | BASIC | Stage 4 (Phone) only | Phone OTP verified. | | VERIFIED | Stage 1 (ID) | Government ID document verified. | | ENHANCED | Stages 1 + 2 + 3 | ID + Liveness + Face Match. | | TRUSTED | All 5 stages | Full verification including Phone and IP Analysis. Identity is immutable. |


Verification Stages

Didit verification is a 5-stage workflow. Each stage maps to a DiditStage enum value:

| Stage | Enum | Description | |---|---|---| | 1 | DiditStage.ID_VERIFICATION | Government ID document scan and OCR | | 2 | DiditStage.LIVENESS | Biometric liveness check | | 3 | DiditStage.FACE_MATCH | Face match against ID document | | 4 | DiditStage.PHONE_VERIFICATION | Phone OTP verification (SMS, call, or WhatsApp) | | 5 | DiditStage.IP_ANALYSIS | IP address risk analysis |

AML (Anti-Money Laundering) screening is a supplemental check that runs alongside the main workflow.


Immutability & Locking

Mutating operations on a MajikUniversalID follow two rules:

Rule 1 — Tier gate. Any operation that modifies the identity (signing, updating display name, syncing user ref, managing consents, updating settings) requires isMutable === true. A verified identity (tier !== UNVERIFIED) is immutable. Attempting to mutate a verified identity throws MajikUniversalIDImmutableError.

Rule 2 — Revocation lock. Once an identity becomes verified, revokeVerification() is blocked for 30 days from verifiedAt. Attempting revocation within this window throws MajikUniversalIDVerificationLockedError with the number of remaining days.

// Check before attempting revocation
if (!majikId.isVerificationLocked) {
  majikId.revokeVerification('User requested account reset');
}

// Check remaining lock time
console.log('Days remaining:', majikId.verificationLockDaysRemaining);

Note: processWebhook() and applyDiditResult() are not subject to the mutable check — they are the graduation path and must be able to run regardless of the current tier.


Usage Examples

Example 1: Create and Serialize an Identity

import { MajikKey } from '@majikah/majik-key';
import { MajikUniversalID } from '@majikah/majik-universal-id';

const mnemonic = MajikKey.generateMnemonic();
const key = await MajikKey.create(mnemonic, 'passphrase', 'My Key');

const majikId = await MajikUniversalID.create(user, key, {
  account_id: 'acct_my_tenant',
});

console.log('Created ID:', majikId.id);
console.log('Tier:', majikId.tier); // IDTier.UNVERIFIED

// Serialize — rehydrated private info is stripped automatically
const json = majikId.toJSON();
await db.identities.insert({ id: majikId.id, data: json });

Example 2: Load and Decrypt Private Info

import { MajikKey } from '@majikah/majik-key';
import { MajikUniversalID } from '@majikah/majik-universal-id';

// Option A: Provide the key on load — silent decryption attempt
const key = await MajikKey.fromJSON(storedKey);
await key.unlock('passphrase');

const majikId = await MajikUniversalID.fromJSON(storedJson, { key });

if (majikId.isPrivateDecrypted) {
  const info = majikId.privateInfo;
  console.log('Name:', info.legal_first_name, info.legal_last_name);
}

// Option B: Explicit decryption after load
const majikId2 = await MajikUniversalID.fromJSON(storedJson);
const result = await majikId2.decryptPrivate(key);

if (result.success) {
  console.log('DOB:', result.data!.date_of_birth);
} else {
  console.log('Decryption failed:', result.reason);
}

Example 3: Sign and Verify Content

import { MajikUniversalID } from '@majikah/majik-universal-id';

// Sign a text document (identity must be UNVERIFIED / mutable)
const signature = await majikId.signContent(
  'This is my signed statement.',
  key,
  { contentType: 'text/plain' },
);

// Verify — both Ed25519 and ML-DSA-87 must pass
const result = majikId.verifyContent('This is my signed statement.', signature);
console.log('Valid:', result.valid);         // true
console.log('Signed at:', result.signed_at);

// Tamper detection
const tamperResult = majikId.verifyContent('This is modified.', signature);
console.log('Tampered rejected:', tamperResult.valid); // false

Example 4: Sign and Verify a File

import { MajikUniversalID } from '@majikah/majik-universal-id';

// Sign a file and embed the signature
const { blob: signedPdf, handler } = await majikId.signFile(pdfBlob, key);
console.log('Handler:', handler); // "PDF"

// Verify the embedded signature later
const result = await majikId.verifyFile(signedPdf);
if (result.valid) {
  console.log('Authentic. Signed by:', result.signer_fingerprint);
  console.log('Handler used:', result.handler);
} else {
  console.log('Invalid:', result.reason);
}

Example 5: Process a Didit Webhook

import { MajikUniversalID, DiditStage } from '@majikah/majik-universal-id';

// In your webhook handler (e.g. Cloudflare Worker or Express route)
const result = await majikId.processWebhook(payload, headers, webhookSecret);

console.log('Previous tier:', result.previous_tier);
console.log('New tier:', result.new_tier);
console.log('Tier changed:', result.tier_changed);
console.log('Stages updated:', result.updated_stages);
console.log('User sync actions:', result.user_sync_actions);
// e.g. ["verifyPhone", "verifyIdentity"]

// Apply user_sync_actions to the linked MajikUser
for (const action of result.user_sync_actions) {
  if (action === 'verifyIdentity') await user.setIdentityVerified(true);
  if (action === 'verifyPhone')    await user.setPhoneVerified(true);
  if (action === 'restrict')       await user.restrict();
}

// Persist the updated identity
await db.identities.update({ id: majikId.id, data: majikId.toJSON() });

Example 6: Check Verification Stages

import { MajikUniversalID, DiditStage } from '@majikah/majik-universal-id';

// Check individual stages
const phonePassed = majikId.isVerificationPassed(DiditStage.PHONE_VERIFICATION);
const livenessPassed = majikId.isVerificationPassed(DiditStage.LIVENESS);

// Get all passed stages in order
const passed = majikId.getPassedVerifications();
console.log('Passed stages:', passed);

// Full verification summary
const summary = majikId.verificationSummary;
console.log('AML clear:', summary.aml_clear);
console.log('Biometric status:', summary.biometric_status);
console.log('IP risk level:', summary.ip_risk_level);

Example 7: Share Private Info With Another Recipient

import { MajikUniversalID } from '@majikah/majik-universal-id';

// recipientKey does NOT need to be unlocked — only mlKemPublicKey is used
const scannerString = await majikId.sharePrivate({
  senderKey: myUnlockedKey,
  recipients: [recipientKey],
});

// The recipient decrypts with their own MajikKey
const env = MajikEnvelope.fromScannerString(scannerString);
const json = await env.decrypt({
  fingerprint: recipientKey.fingerprint,
  mlKemSecretKey: recipientKey.mlKemSecretKey,
});
const privateInfo = JSON.parse(json);

Example 8: Public View Projection

import { MajikUniversalID } from '@majikah/majik-universal-id';

// Safe to return from a public API — contains no private info
const publicView = majikId.toPublicView();

console.log('Display name:', publicView.display_name);
console.log('Tier:', publicView.tier);
console.log('Signing key fingerprint:', publicView.signing_key.fingerprint);
console.log('Verification stages:', publicView.verification_stages);
// {
//   id_verification: true,
//   liveness: true,
//   face_match: false,
//   phone_verification: false,
//   ip_analysis: false
// }

Example 9: Revoke Verification

import {
  MajikUniversalID,
  MajikUniversalIDVerificationLockedError,
} from '@majikah/majik-universal-id';

try {
  majikId.revokeVerification('User requested account reset');
  console.log('Revoked. Tier:', majikId.tier); // IDTier.UNVERIFIED
} catch (err) {
  if (err instanceof MajikUniversalIDVerificationLockedError) {
    console.log('Cannot revoke yet:', err.days_remaining, 'days remaining');
  }
}

Error Reference

All public methods throw typed subclasses of MajikUniversalIDError. Use the exported type guards for precise error handling.

| Error Class | Code | Description | |---|---|---| | MajikUniversalIDValidationError | VALIDATION_ERROR | Invalid or missing inputs to factory methods | | MajikUniversalIDDeserializationError | DESERIALIZATION_ERROR | Malformed JSON or base64 on load | | MajikUniversalIDIntegrityError | INTEGRITY_ERROR | SHA3-512 hash mismatch on deserialization | | MajikUniversalIDKeyError | KEY_ERROR | Missing required public key fields on a MajikKey | | MajikUniversalIDKeyNotFoundError | KEY_NOT_FOUND | Provided key fingerprint doesn't match the bound key | | MajikUniversalIDSigningError | SIGNING_ERROR | Signing operation failed | | MajikUniversalIDVerificationError | VERIFICATION_ERROR | Structural error during content/file verification | | MajikUniversalIDWebhookSignatureError | WEBHOOK_SIGNATURE_INVALID | Webhook HMAC verification failed | | MajikUniversalIDWebhookPayloadError | WEBHOOK_PAYLOAD_INVALID | Webhook vendor_data mismatch or invalid structure | | MajikUniversalIDImmutableError | IDENTITY_IMMUTABLE | Mutating operation attempted on a verified identity | | MajikUniversalIDVerificationLockedError | VERIFICATION_LOCKED | revokeVerification() called within the 30-day lock | | MajikUniversalIDPrivateInfoLockedError | PRIVATE_INFO_LOCKED | privateInfo accessed before decryptPrivate() | | MajikUniversalIDPrivateInfoEncryptionError | PRIVATE_INFO_ENCRYPTION_ERROR | Encryption of private info failed | | MajikUniversalIDRestrictedError | IDENTITY_RESTRICTED | Operation blocked — identity is currently restricted | | MajikUniversalIDTierRequiredError | TIER_REQUIRED | Operation requires a higher IDTier |

Type guards:

import {
  isUniversalIDError,
  isValidationError,
  isWebhookError,
  isImmutableError,
  isLockedError,
  isPrivateInfoLockedError,
} from '@majikah/majik-universal-id';

Security Considerations

What is Guaranteed

  • Integrity: The SHA3-512 hash commits all four bound public keys — key material substitution is detectable on load
  • Content integrity: Any byte change to signed content invalidates both the Ed25519 and ML-DSA-87 signatures
  • Private info confidentiality: Private personal info is never stored or serialized in plaintext — only the ML-KEM-768 encrypted envelope is persisted
  • Signer binding: Signatures are cryptographically bound to a specific MajikKey fingerprint
  • Immutability enforcement: Verified identities cannot be mutated — enforced at every public mutating method
  • Hybrid downgrade resistance: Both classical and post-quantum algorithms must be broken simultaneously to forge a signature

What is Your Responsibility

  • Key management: Lock the MajikKey immediately after signing — key.lock() purges secret keys from memory
  • Signer identity verification: The library proves content was signed by a specific key. It does not prove who owns that key in the real world. Maintain the mapping between signerId and a real-world identity through your own means
  • vendor_data routing: Always set MajikUniversalID.id as vendor_data when creating a Didit session. Webhooks with mismatched vendor_data are rejected by processWebhook()
  • extractPublicKeys() caution: Public keys embedded in a Majik Signature envelope are self-reported by the signer. Always cross-check signerId against majikId.signingKey.fingerprint before trusting extracted keys
  • Byte-for-byte consistency: The same bytes must be passed to both sign() and verify(). For JSON payloads, use the same JSON.stringify() output on both sides
  • Key upgrade: Legacy MajikKey accounts without signing keys must be re-imported via importFromMnemonicBackup(). Check with key.hasSigningKeys before calling any signing method

What NOT to Do

DON'T access privateInfo without first calling decryptPrivate(key) or providing the key to fromJSON()
DON'T trust verifyContent() without also checking result.signer_registered === true and verifying result.signer_fingerprint against a known trusted source
DON'T pass a locked key to signContent() or signFile() — call key.unlock(passphrase) first
DON'T call processWebhook() without first checking that payload.vendor_data === majikId.id
DON'T modify file bytes (re-encode, compress, transcode) between signFile() and verifyFile()
DON'T rely on the contentType field in a signature for security decisions — it is advisory only

What TO Do

DO lock the key immediately after any signing operation
DO validate the integrity of a restored identity using majikId.validate() after loading from storage
DO use isSigned() from Majik Signature as a fast guard before calling verifyFile()
DO check result.signer_fingerprint === majikId.signingKey.fingerprint in verifyContent() results
DO store identities using toJSON() — private info is guaranteed stripped
DO use toPublicView() for any data returned from public-facing API routes


Related Projects

Majik Signature

Hybrid post-quantum content signing — the signing engine used by signContent() and signFile().

Read Docs · Microsoft Store

Majik Message Microsoft App Store

Majik Message

Secure messaging platform using Majik Keys and Majik Signatures for identity-bound communication.

Read Docs · Microsoft Store

Majik Message Microsoft App Store

Majik Key

Seed phrase account library — required peer dependency for signing and encryption.

Read More Information

Majik Envelope

Post-quantum group encryption — used to encrypt and share private personal info.

Read More Information


Contributing

If you want to contribute or help extend support, reach out via email. All contributions are welcome!


License

Apache-2.0 — free for personal and commercial use.


Author

Made with 💙 by @thezelijah

Developer: Josef Elijah Fabian
GitHub: https://github.com/jedlsf
Project Repository: https://github.com/Majikah/majik-universal-id


Contact