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.
- Majik Universal ID
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.
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.
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.
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).
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.
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.
- 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
- 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
- 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
- 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
- Encrypted at rest:
PrivatePersonalInfois always stored as anMajikEnvelope-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 viaMajikEnvelopegroup encryption, returns a scanner string - Never serialized:
rehydratedis stripped from alltoJSON()/toBase64()outputs
- 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
# 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-envelopeNo native bindings. Works in Node.js 18+, all modern browsers, Deno, and Bun.
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.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 constructedMajikUserinstance.key: MajikKey— An unlockedMajikKeywith all signing and encryption keys present.options: CreateUniversalIDOptionsaccount_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 currentSCHEMA_VERSIONconstant.
Returns: Promise<MajikUniversalID>
Throws: MajikUniversalIDKeyError if the key is locked or missing required fields. MajikUniversalIDValidationError if the user fails validation.
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?: FromJSONOptionskey?: MajikKey— An unlockedMajikKeyto 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.
Reconstruct from a base64-serialized string. Accepts the same optional key for silent decryption.
Returns: Promise<MajikUniversalID>
Signing methods require isMutable === true (the identity must be at IDTier.UNVERIFIED). The provided key's fingerprint must match the bound signing_key.fingerprint.
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 unlockedMajikKeymatching 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.
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 unlockedMajikKeymatching the bound fingerprint.options?— OptionalcontentType,timestamp,mimeType, andlabel.
Returns: Promise<{ blob: Blob; signature: MajikSignature; handler: string; mimeType: string }>
Verification methods are read-only and have no isMutable requirement.
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
}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").
Convenience wrapper for verifyContent() for string content.
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 unlockedMajikKey. Fingerprint must matchsigning_key.fingerprint.
Returns: Promise<DecryptPrivateResult>
{
success: boolean;
data?: PrivatePersonalInfo; // Present when success === true
reason?: string; // Present when success === false
}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 boundMajikKey. Used to decrypt first.options.recipients: MajikKey[]— One or more recipient keys. OnlymlKemPublicKeyis 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);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_datamust equalMajikUniversalID.id. Set this value asvendor_datawhen creating the Didit session.
Parameters:
payload: DiditWebhookPayload— The raw Didit webhook body.headers: DiditWebhookHeaders— The webhook request headers (must includex-timestampand 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
}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.
Flag the identity for re-verification (admin use). Does not unlock the identity or reset its tier.
Check whether a specific DiditStage has been passed.
if (majikId.isVerificationPassed(DiditStage.LIVENESS)) {
// liveness check was passed
}Returns the list of DiditStage values that have been passed, in the order they were completed.
Returns: DiditStage[]
Export the full identity as a plain MajikUniversalIDJSON object. The rehydrated field is always stripped — decrypted private info is never serialized.
Serialize the identity to a compact base64 string. Equivalent to objectToBase64(toJSON()).
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.
Run structural and integrity checks. Returns a UniversalIDValidationResult with is_valid, errors, and warnings arrays.
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 |
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. |
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.
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.
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 });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);
}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); // falseimport { 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);
}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() });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);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);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
// }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');
}
}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';- 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
MajikKeyfingerprint - 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
- Key management: Lock the
MajikKeyimmediately 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
signerIdand a real-world identity through your own means vendor_datarouting: Always setMajikUniversalID.idasvendor_datawhen creating a Didit session. Webhooks with mismatchedvendor_dataare rejected byprocessWebhook()extractPublicKeys()caution: Public keys embedded in a Majik Signature envelope are self-reported by the signer. Always cross-checksignerIdagainstmajikId.signingKey.fingerprintbefore trusting extracted keys- Byte-for-byte consistency: The same bytes must be passed to both
sign()andverify(). For JSON payloads, use the sameJSON.stringify()output on both sides - Key upgrade: Legacy
MajikKeyaccounts without signing keys must be re-imported viaimportFromMnemonicBackup(). Check withkey.hasSigningKeysbefore calling any signing method
❌ 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
✅ 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
Hybrid post-quantum content signing — the signing engine used by signContent() and signFile().
Secure messaging platform using Majik Keys and Majik Signatures for identity-bound communication.
Seed phrase account library — required peer dependency for signing and encryption.
Post-quantum group encryption — used to encrypt and share private personal info.
If you want to contribute or help extend support, reach out via email. All contributions are welcome!
Apache-2.0 — free for personal and commercial use.
Made with 💙 by @thezelijah
Developer: Josef Elijah Fabian
GitHub: https://github.com/jedlsf
Project Repository: https://github.com/Majikah/majik-universal-id
- Business Email: business@thezelijah.world
- Official Website: https://www.thezelijah.world
- ID Web App: https://id.majikah.solutions