Implementation Guide
Universal Manifest (UM) is a specification. You can implement it in any language. The TypeScript helper and um-typescript repository are reference implementations, not normative dependencies.
1. Introduction
Section titled “1. Introduction”This guide takes you from first parse to conformance-ready implementation.
Prerequisites:
- JSON parser + RFC 3339 timestamp parsing.
- JCS + Ed25519 libraries if targeting v0.2.
- Access to spec and conformance artifacts.
Normative references:
/spec/v01//spec/v02//conformance/v01//conformance/v02/
2. Choose Your Conformance Level
Section titled “2. Choose Your Conformance Level”Text fallback:
- Consume only ->
v0.1-baselineconsumer. - Produce manifests -> include
v0.1-baselineissuer behavior. - Need tamper protection ->
v0.2-baseline. - Need revocation checks ->
v0.2-extended.
Conformance levels:
v0.1-baseline: required fields, TTL, unknown-field tolerance.v0.2-baseline: v0.1 baseline plus JCS + Ed25519 profile verification/signing.v0.2-extended: v0.2 baseline plus revocation-aware policy checks.
3. Step 1: Parse and Validate a Manifest
Section titled “3. Step 1: Parse and Validate a Manifest”Required fields:
@context@id@type(includesum:Manifest)manifestVersionsubjectissuedAtexpiresAt
Required behavior:
- Parse as JSON object.
- Reject missing required fields.
- Parse and validate timestamps.
- Reject for use when
now > expiresAt. - Safely ignore unknown fields.
TypeScript example:
function validateForUse(manifest: Record<string, unknown>, now = new Date()): void { const required = ['@context', '@id', '@type', 'manifestVersion', 'subject', 'issuedAt', 'expiresAt'] for (const key of required) { if (!(key in manifest)) throw new Error(`missing ${key}`) }
const t = manifest['@type'] const hasManifestType = typeof t === 'string' ? t === 'um:Manifest' : Array.isArray(t) && t.includes('um:Manifest') if (!hasManifestType) throw new Error('invalid @type')
const issued = new Date(String(manifest['issuedAt'])) const expires = new Date(String(manifest['expiresAt'])) if (Number.isNaN(issued.getTime()) || Number.isNaN(expires.getTime())) throw new Error('invalid timestamp') if (issued > expires || now > expires) throw new Error('manifest is not valid for use')}Python pseudocode:
required = ["@context", "@id", "@type", "manifestVersion", "subject", "issuedAt", "expiresAt"]for key in required: if key not in manifest: raise ValueError(f"missing {key}")
if now() > parse_rfc3339(manifest["expiresAt"]): raise ValueError("expired")Go pseudocode:
// Parse required fields and enforce TTL; ignore unknown fields by default.4. Step 2: Create a Manifest (Issuer)
Section titled “4. Step 2: Create a Manifest (Issuer)”Issuer essentials:
- Set unique
@id(recommendedurn:uuid:<uuidv4>). - Set stable
subjectURI. - Set
issuedAt/expiresAtwith explicit TTL. - Set exact
manifestVersion.
Example (v0.1):
{ "@context": "https://universalmanifest.net/ns/universal-manifest/v0.1/schema.jsonld", "@id": "urn:uuid:af58f76e-a8f8-4b3a-bf2f-c8b8bb76a8de", "@type": "um:Manifest", "manifestVersion": "0.1", "subject": "did:key:z6Mki...", "issuedAt": "2026-03-02T00:00:00Z", "expiresAt": "2026-03-02T12:00:00Z", "shards": []}5. Step 3: Sign a Manifest (v0.2)
Section titled “5. Step 3: Sign a Manifest (v0.2)”Profile requirements:
signature.algorithm = "Ed25519"signature.canonicalization = "JCS-RFC8785"
Signing flow:
- Remove
signaturefrom payload. - Canonicalize with JCS.
- Sign canonical bytes with Ed25519.
- Attach
signatureobject.
TypeScript sketch:
const unsigned = { ...manifest }delete unsigned.signatureconst canonical = canonicalize(unsigned)const sig = await sign(new TextEncoder().encode(canonical), privateKey)6. Step 4: Verify a Manifest (v0.2)
Section titled “6. Step 4: Verify a Manifest (v0.2)”Verification flow:
- Run baseline checks.
- Validate profile pair (
Ed25519,JCS-RFC8785). - Resolve/read public key.
- Recompute canonical signing bytes.
- Verify signature; reject on failure.
7. Step 5: Run the Conformance Suite
Section titled “7. Step 5: Run the Conformance Suite”Get runner and execute:
git clone https://github.com/grigb/universal-manifest.gitcd universal-manifest/conformance/runnernpm cinode ./cli.mjs --mode command --adapter-command "python3 /path/to/adapter.py" --report ./conformance-report.jsonAdapter response contract:
{ "result": "accept", "reason": "validated"}8. Common Pitfalls and FAQ
Section titled “8. Common Pitfalls and FAQ”Pitfalls:
- Rejecting unknown fields.
- Skipping TTL checks.
- Signing non-canonical JSON.
- Accepting unsupported signature profiles.
FAQ:
- What JSON-LD processing do I need?
- None for baseline conformance.
- Can I use a different signature algorithm for v0.2 conformance?
- No. v0.2 baseline requires Ed25519 + JCS.
- How do I handle unknown fields?
- Ignore safely.
- What minimum manifest size should I support?
- At least 1 MB.
- How do I test implementation correctness?
- Run conformance fixtures and compare with
expected.json.
- Can I extend the manifest with custom fields?
- Yes.
- What about privacy/GDPR?
- Use opaque IDs and minimal disclosure.
- How do I resolve a UMID?
GET https://myum.net/{UMID}.
- What if the resolver is down?
- Use cached manifests only while still within TTL.
9. Submitting Your Conformance Report
Section titled “9. Submitting Your Conformance Report”Submission package:
conformance-report.json.- Implementation metadata (name/version/language).
- Runner command and adapter reference.
- CI/local evidence links.