@majikah/sdk
v1.1.0
Published
Official TypeScript client SDK for the Majikah ecosystem, providing unified access to Majik Universal ID (MUID) identity verification, Time Stamping Authority (TSA) issuing, cryptographically signed link claims (SLink), and on-chain document notarization
Maintainers
Readme
Majikah SDK
The official TypeScript SDK for the Majikah ecosystem.
@majikah/sdk provides a unified developer interface for Majikah's public services:
- Majik Universal ID (MUID) — public identity lookup and signature verification
- Time Stamping Authority (TSA) — trusted timestamps for digital signatures
- Signed Links (SLink) — signed claims associated with URLs and content hashes
- File Notarization — payment-aware on-chain anchoring of sealed files
The SDK is designed to work with Majikah's local cryptographic libraries, including @majikah/majik-key, @majikah/majik-signature, and @majikah/majik-slink.
Cryptographic operations that require private keys are performed locally. Private signing keys are not uploaded to the Majikah API by the SDK.
Why the Majikah SDK?
The SDK combines Majikah's public services with local cryptographic operations so applications can build complete signing, verification, timestamping, SLink, and notarization workflows without implementing the API protocol themselves.
For example, a single application can:
Create or receive a signed file
│
├── Verify the signature against a MUID
│
├── Request a trusted timestamp
│
├── Publish or verify a signed URL claim
│
└── Seal and notarize the file on-chainThe SDK also keeps service-specific functionality separated:
majikah.muid
majikah.tsa
majikah.slink
majikah.notaryThis allows you to use only the part of the ecosystem your application actually needs.
Features
MUID
Verify signatures against public Majik Universal ID identities.
- Look up public MUID profiles
- Retrieve the authenticated MUID
- Verify existing embedded signatures
- Verify detached signature envelopes
- Select a specific signer in multi-signature files
- Verify against a specific MUID by ID or username
TSA
Request trusted timestamps for digital signatures.
- Issue a TSA timestamp directly from a signature
- Timestamp an already-signed embedded file
- Timestamp a detached signature envelope
- Sign and timestamp a file in one operation
- Check TSA quota
- Automatically reuse the signature's TSA request payload
SLink
Create, register, search, and cryptographically verify signed link claims.
- Register existing SLinks
- Create, sign, and register an SLink in one operation
- Search claims by URL
- Search claims by content hash
- Retrieve SLinks by ID
- Delete SLinks
- Verify SLink signatures locally
- Resolve public signing keys through MUID or a custom key registry
- Cursor-based pagination for owned SLinks
File Notarization
Anchor sealed file hashes on-chain through a payment-aware workflow.
- Check or initiate payment for a sealed file
- Return the sealed artifact while payment is pending
- Register a paid seal hash
- Monitor on-chain confirmation
- Poll until a terminal state
- Embed the resulting chain anchor back into the file
- Seal an already-signed file and begin notarization
- Run the complete single-signer
sign → seal → initiateworkflow
Installation
Install the SDK with npm:
npm install @majikah/sdkThe SDK is designed to work with the Majikah cryptographic packages used by the specific workflows in your application.
For example:
npm install @majikah/sdk @majikah/majik-key @majikah/majik-signature @majikah/majik-slinkUse the packages required by your application rather than installing the entire Majikah ecosystem unnecessarily.
Get an API Key
To use the Majikah public API, you first need to create and activate your developer account.
- Go to developers.majikah.solutions.
- Complete the developer onboarding process:
- Create your Majikah account.
- Create your Majik Universal ID (MUID).
- Verify your MUID by completing the required KYC verification.
- Submit the Early Access application through the developer portal.
- Wait for your application to be reviewed and approved by the Majikah team.
- Once approved, you will receive an email confirmation and gain access to API key generation in the developer portal.
- Generate your API key and keep it secure. You will use this key when initializing
MajikahSDKClient.
Early Access: Public API access is currently subject to developer approval. An API key cannot be generated until your Early Access application has been approved.
Requirements
The SDK expects a modern JavaScript runtime with:
fetchURLAbortControlleratob/btoa
These APIs are available natively in modern browsers and current Node.js, Deno, Bun, and edge runtimes.
A custom fetch implementation can also be supplied through the client options when integrating with a specialized runtime or transport layer.
Authentication
Create a MajikahSDKClient with your public API key:
import { MajikahSDKClient } from "@majikah/sdk";
const majikah = new MajikahSDKClient({
apiKey: process.env.MAJIKAH_API_KEY!,
});The SDK automatically sends the API key using the expected authentication header.
Do not hard-code API keys into browser source code or commit them to your repository.
For server-side applications, use environment variables or another secure secret-management mechanism.
Client Configuration
The client supports a number of optional transport and API settings.
const majikah = new MajikahSDKClient({
apiKey: process.env.MAJIKAH_API_KEY!,
baseUrl: "https://api-public.majikah.solutions",
version: 1,
timeoutMs: 15_000,
headers: {
"X-Custom-Header": "example",
},
retry: {
maxAttempts: 3,
initialDelayMs: 300,
jitterFactor: 0.4,
capDelayMs: 5_000,
},
});Configuration options
| Option | Description | Default |
| ----------- | ------------------------------------------- | -------------------------------------- |
| apiKey | API key used to authenticate requests | Required |
| baseUrl | Public Majikah API base URL | https://api-public.majikah.solutions |
| version | Global API version or per-service versions | 1 |
| timeoutMs | Request timeout in milliseconds | 15000 |
| headers | Additional request headers | {} |
| fetch | Custom fetch implementation | Runtime fetch |
| retry | Retry configuration for retry-safe requests | See below |
Per-service API versions
A single version can be applied to every service:
const majikah = new MajikahSDKClient({
apiKey,
version: 1,
});Individual services can also be versioned independently:
const majikah = new MajikahSDKClient({
apiKey,
version: {
tsa: 1,
notary: 1,
slink: 1,
muid: 2,
},
});Services not explicitly overridden inherit the fallback version.
Retry behavior
The SDK automatically retries retry-safe requests using exponential backoff with jitter.
GET requests are retryable by default.
POST requests are only retried when explicitly marked as idempotent internally.
This distinction prevents the SDK from accidentally repeating operations that could create duplicate side effects.
Default retry settings:
{
maxAttempts: 3,
initialDelayMs: 300,
jitterFactor: 0.4,
capDelayMs: 5_000,
}Rate-limit responses can also provide retry timing information that the SDK uses when mapping the API response.
Service Modules
1. MUID — Identity and Signature Verification
MUID provides public identity information and lets applications verify digital signatures against a MUID.
Look up a MUID
const profile = await majikah.muid.lookup("alice");
console.log(profile);The lookup accepts either a MUID identifier or username.
Get the current MUID
const me = await majikah.muid.me();
console.log(me);Verify an existing signed file
const result = await majikah.muid.verifyFile(signedFile, {
muid: "alice",
});
if (result.valid) {
console.log("Signature is valid.");
}When the file contains multiple signatures, identify which signer should be verified:
const result = await majikah.muid.verifyFile(signedFile, {
expectedSignerId: signerFingerprint,
muid: "alice",
});When muid is omitted, verification uses the MUID associated with the caller's API credentials where supported by the API.
Verify a detached envelope
const result = await majikah.muid.verifyFileDetached(envelope, {
muid: "alice",
});Detached verification operates on the signature envelope rather than an embedded signature inside the original file.
2. TSA — Trusted Timestamping
The TSA service provides trusted timestamps for Majik Signatures.
Timestamp an existing signature
For a file that has already been signed:
const { blob, signature } = await majikah.tsa.timestampFile(
signedFile,
);No MajikKey is required for this operation because the method does not create a new signature.
For a multi-signature file:
const { blob, signature } = await majikah.tsa.timestampFile(
signedFile,
{
expectedSignerId: signerFingerprint,
},
);Timestamp a detached envelope
const { envelope, mjksig } =
await majikah.tsa.timestampDetached(envelope);The returned mjksig is the serialized detached signature envelope.
Sign and timestamp in one operation
const result = await majikah.tsa.stampFile(
file,
myUnlockedMajikKey,
{
contentType: "application/pdf",
},
);
console.log(result.blob);This performs:
sign
↓
request TSA timestamp
↓
attach TSA
↓
embed signatureSign and return a detached timestamped envelope
const result = await majikah.tsa.stampFileDetached(
file,
myUnlockedMajikKey,
{
contentType: "application/pdf",
},
);
const { envelope, mjksig } = result;This is useful when the signature needs to be distributed separately from the original file.
TSA quota
const quota = await majikah.tsa.quota();
console.log(quota);3. SLink — Signed Link Claims
SLink associates cryptographically signed claims with URLs or content hashes.
A useful distinction is:
SLink lookup establishes that a claim exists. Cryptographic verification establishes that the claim's signature is valid.
The API does not need to store the signer's public keys for local verification.
Register an existing SLink
const stored = await majikah.slink.create(slink);A MajikSLink instance or serialized JSON can be supplied.
Create, sign, and register an SLink
const stored = await majikah.slink.registerUrl(
"https://example.com",
aliceKey,
userId,
muid,
);A bare domain is also supported by the underlying SLink workflow:
const stored = await majikah.slink.registerUrl(
"example.com",
aliceKey,
userId,
muid,
);Search by URL
const results = await majikah.slink.verifyUrl(
"example.com",
);
console.log(results.matches);The SDK automatically normalizes a bare domain to HTTPS before sending the request.
Search by content hash
const results = await majikah.slink.verifyByHash(hash);
console.log(results.matches);Local cryptographic verification
To verify every search result cryptographically, use verifyUrlWithProof().
The simplest MUID-backed approach is:
import { createMuidPublicKeyResolver } from "@majikah/sdk";
const results = await majikah.slink.verifyUrlWithProof(
"thezelijah.world",
createMuidPublicKeyResolver(majikah.muid),
);
const verified = results.filter(({ result }) => result.valid);createMuidPublicKeyResolver():
- Resolves each MUID through
MUIDClient.lookup() - Reads the public signing keys
- Converts their Base64 representation to the SDK's binary key format
- Supplies the keys to the local signature verifier
Custom public key sources
Applications can supply their own trusted key registry:
const results = await majikah.slink.verifyUrlWithProof(
"example.com",
async (muid, signerId) => {
const keys = await myKeyRegistry.get(muid);
return {
signerId,
edPublicKey: keys.edPublicKey,
mlDsaPublicKey: keys.mlDsaPublicKey,
};
},
);This makes SLink verification independent of a specific public-key storage backend.
Verify already-fetched matches
If you already called verifyUrl() or verifyByHash(), you can verify the returned matches without performing another lookup:
const search = await majikah.slink.verifyUrl("example.com");
const verified = await majikah.slink.verifyMatches(
search.matches,
createMuidPublicKeyResolver(majikah.muid),
);List your SLinks
SLink ownership listings use cursor-based pagination:
const page = await majikah.slink.me({
limit: 50,
});
console.log(page.items);Fetch the next page using the returned opaque cursor:
if (page.has_more && page.next_cursor) {
const nextPage = await majikah.slink.me({
cursor: page.next_cursor,
limit: 50,
});
}The cursor should be treated as opaque data. Do not decode or modify it.
4. Notary — File Notarization
Notary provides payment-aware on-chain anchoring for sealed files.
A notarization is based on the file's seal hash, rather than uploading the entire file to the notarization service.
The sealed artifact remains with the application. The Majikah notarization service uses the seal hash to associate payment and on-chain anchoring with that artifact.
Do not re-seal or otherwise modify the artifact before finalization, as the sealHash must continue to correspond to the sealed content.
The high-level lifecycle is:
flowchart TD
A[Signed file] --> B[Create seal]
B --> C[sealedBlob + sealHash]
C --> D[Initiate notarization]
D --> E{Already anchored?}
E -->|Yes| F[Return notarized blob]
E -->|No| G[Create payment checkout]
G --> H[Return sealedBlob + sealHash + checkout]
H --> I[User completes payment]
I --> J[Register sealHash on-chain]
J --> K[Wait for chain confirmation]
K --> L[Embed chain anchor into sealed blob]
L --> M[Return notarized blob]Start notarization for an already-sealed file
const result =
await majikah.notary.initiateNotarization(sealedFile);The result tells you what happens next.
Payment required
const result =
await majikah.notary.initiateNotarization(sealedFile);
if (result.status === "payment_required") {
console.log(result.sealHash);
console.log(result.checkout.checkout_url);
console.log(result.sealedBlob);
}The checkout URL can be presented as a QR code or another payment interface.
The sealedBlob returned with the payment-required result is the exact sealed artifact associated with the returned sealHash. Retain both values while payment is being completed.
After payment is completed, finalize the notarization using the returned sealedBlob and sealHash:
if (result.status === "payment_required") {
const finalized =
await majikah.notary.finalizeNotarization(
result.sealedBlob,
result.sealHash,
{
poll: {
intervalMs: 2000,
timeoutMs: 130_000,
},
},
);
console.log(finalized.anchor.id);
console.log(finalized.blob);
}The sealed file is not uploaded to the notarization service as part of this flow. The service uses the seal hash to associate the payment and on-chain notarization with the sealed artifact.
Already anchored
If the file was already notarized:
if (result.status === "anchored") {
console.log("file is already notarized.");
console.log(result.anchor);
console.log(result.blob);
}The existing anchor is returned and the anchor information can be embedded into the file.
Already paid
If payment was previously completed but notarization has not yet been finalized:
const result =
await majikah.notary.initiateNotarization(sealedFile);
if (result.status === "ready_to_finalize") {
const finalized =
await majikah.notary.finalizeNotarization(
result.sealedBlob,
result.sealHash,
);
console.log(finalized.anchor);
console.log(finalized.blob);
}The result contains both the sealHash and the exact sealedBlob associated with it, so the caller does not need to reconstruct or re-seal the document.
Register manually
For applications that want lower-level lifecycle control:
const anchor = await majikah.notary.register(sealHash);Registration may initially return a pending anchor.
Poll for confirmation
const anchor = await majikah.notary.pollUntilTerminal(
anchorId,
{
intervalMs: 2000,
timeoutMs: 130_000,
},
);
switch (anchor.status) {
case "confirmed":
console.log("Anchor confirmed.");
break;
case "finalized":
console.log("Anchor finalized.");
break;
case "failed":
console.error("Anchor failed.");
break;
}A terminal failed status is returned as a normal result. It is not automatically converted into an exception because the failure state itself contains useful application-level information.
Complete single-signer flow
For a document that needs to be signed, sealed, paid, and notarized:
const result =
await majikah.notary.signSealAndInitiateNotarization(
file,
issuerKey,
{
contentType: "application/pdf",
},
);
if (result.status === "payment_required") {
// Present the checkout to the user.
showQrCode(result.checkout.checkout_url);
// After payment has been completed:
const finalized =
await majikah.notary.finalizeNotarization(
result.sealedBlob,
result.sealHash,
);
console.log(finalized.anchor);
console.log(finalized.blob);
}
if (result.status === "ready_to_finalize") {
const finalized =
await majikah.notary.finalizeNotarization(
result.sealedBlob,
result.sealHash,
);
console.log(finalized.anchor);
console.log(finalized.blob);
}
if (result.status === "anchored") {
console.log(result.blob);
console.log(result.anchor);
}This demonstrates the complete discriminated-union workflow without requiring developers to understand the lower-level payment() and register() methods first.
For multi-signature files, complete the signing process first, then seal and initiate notarization:
const result =
await majikah.notary.sealAndInitiateNotarization(
fullySignedFile,
issuerKey,
);This keeps the seal operation as the explicit transition from a mutable signing envelope to the notarization-ready file state.
Detached Signatures
Majik Signature supports detached signatures through the .mjksig format.
The SDK can operate directly on detached envelopes without requiring signatures to be embedded into the original file.
For example:
const result =
await majikah.tsa.stampFileDetached(
file,
key,
);
await saveFile(
result.mjksig,
"file.mjksig",
);Detached envelopes are useful for:
- Out-of-band signature distribution
- Independent verification
- Signature storage separate from the original file
- Workflows where modifying the original file is undesirable
The .mjksig media type is registered with IANA as:
application/vnd.majikah.mjksigLocal Cryptography and Key Handling
The SDK intentionally separates API operations from local cryptographic operations.
For workflows involving private keys:
Your application
│
├── MajikKey
│ │
│ └── private key operations stay local
│
└── Majikah API
│
├── identity lookup
├── TSA
├── SLink registry
└── notarization servicesA MajikKey is supplied to operations that require signing or sealing.
The SDK does not need to upload the private signing key to the API to perform those operations.
Applications should still follow normal key-management practices and protect unlocked keys within their own execution environment.
Verification Model
Majikah verification is intentionally layered.
For example, SLink verification consists of two different questions:
Does a claim exist?
const result = await majikah.slink.verifyUrl(
"example.com",
);This performs a service-side lookup.
Is the signature cryptographically valid?
const result =
await majikah.slink.verifyUrlWithProof(
"example.com",
createMuidPublicKeyResolver(majikah.muid),
);This performs local cryptographic verification using trusted public keys.
This distinction is important when building security-sensitive applications: a registry match should not automatically be treated as cryptographic proof.
File and Signature Compatibility
The SDK is built around the Majik Signature ecosystem and accepts the FileLike abstractions supported by @majikah/majik-signature.
Depending on the workflow, the SDK can operate on:
- Regular file or blob-like data
- Embedded Majik Signatures
- Detached
.mjksigenvelopes - Sealed signature envelopes
- files that receive a notarization chain anchor
The exact file-handling capabilities are provided by the corresponding Majikah cryptographic libraries rather than duplicated inside this SDK.
Error Handling
The SDK maps API and transport failures into SDK-specific error types.
For example:
try {
await majikah.tsa.issueForSignature(signature);
} catch (error) {
console.error(error);
}Applications should distinguish between:
- Validation errors caused by invalid local input
- API errors returned by the Majikah service
- Rate-limit responses
- Request timeouts
- Terminal workflow states such as a failed notarization
Not every negative application outcome is represented as an exception. For example, a notarization anchor with status === "failed" is a valid terminal result and can be handled through normal control flow.
TypeScript
The SDK is written in TypeScript and provides typed service methods, request options, response models, and workflow results.
For example:
const page = await majikah.slink.me({
limit: 25,
});
page.items;
// MajikSLinkJSON[]
page.next_cursor;
// string | nullThe SDK's public types are designed to make service behavior discoverable directly through IDE autocomplete and TypeScript type information.
Runtime Support
The SDK is designed for modern full-stack and edge environments, including:
- Browser applications
- Node.js
- Deno
- Bun
- Cloudflare Workers
- Other runtimes implementing the standard web APIs used by the SDK
A custom fetch implementation may be supplied when required:
const majikah = new MajikahSDKClient({
apiKey,
fetch: customFetch,
});This allows applications to integrate the SDK into environments with specialized HTTP transports or execution constraints.
API Design
The SDK deliberately separates lower-level service operations from higher-level convenience workflows.
For example:
majikah.tsa.issue(...)
majikah.tsa.timestampFile(...)
majikah.tsa.stampFile(...)and:
majikah.notary.payment(...)
majikah.notary.register(...)
majikah.notary.status(...)
majikah.notary.initiateNotarization(...)
majikah.notary.finalizeNotarization(...)This allows developers to choose between:
- Fine-grained control for backend workflows and infrastructure integrations
- Convenience methods for common application flows
The same principle applies to SLink verification and MUID identity operations.
Security Considerations
Applications integrating the SDK should:
- Keep API keys private on trusted server-side systems where appropriate
- Never expose privileged API credentials in client-side source code
- Protect unlocked
MajikKeyinstances - Use trusted sources when resolving public signing keys
- Treat API lookup results and cryptographic verification results as distinct security signals
- Validate application-level authorization before allowing destructive operations
- Handle rate limits and timeouts appropriately
- Preserve detached signature and seal data exactly as generated
The SDK provides cryptographic primitives and service integration, but your application's authorization, key custody, access control, and trust policies remain the responsibility of the application.
Contributing
Contributions, bug reports, documentation improvements, and ecosystem integrations are welcome.
Before submitting a change:
- Keep the public API backward-compatible unless a breaking change is intentional.
- Preserve strong TypeScript typing.
- Add tests for both successful and failure paths.
- Document new public APIs with JSDoc.
- Avoid silently changing cryptographic behavior or serialization formats.
Please open an issue or pull request in the project repository for proposed changes.
License
Apache-2.0
See LICENSE for the full license text.
Maintainer
Developed by Josef Elijah Fabian (Zelijah) and Majikah Solutions OPC.
- Website: https://majikah.solutions
- Developer: https://github.com/jedlsf
- GitHub: https://github.com/Majikah
- Organization: Majikah Solutions OPC
Links
- Majikah: https://majikah.solutions
- SDK package: https://www.npmjs.com/package/@majikah/sdk
- GitHub organization: https://github.com/Majikah
- Majik Signature: https://github.com/Majikah/majik-signature
- Business contact: [email protected]
Quick Reference
import {
MajikahSDKClient,
createMuidPublicKeyResolver,
} from "@majikah/sdk";
const majikah = new MajikahSDKClient({
apiKey: process.env.MAJIKAH_API_KEY!,
});
// MUID
const profile = await majikah.muid.lookup("alice");
// TSA
const stamped = await majikah.tsa.stampFile(
file,
signingKey,
);
// SLink
const slinks = await majikah.slink.verifyUrl(
"example.com",
);
// SLink + local cryptographic proof
const verified = await majikah.slink.verifyUrlWithProof(
"example.com",
createMuidPublicKeyResolver(majikah.muid),
);
// Notary
const notarization =
await majikah.notary.initiateNotarization(
sealedFile,
);Majikah SDK — connect your applications to identity, signatures, timestamps, signed claims, and file notarization.
