the cronum standard v1
Public & Unlockable Attachments
cronum is a public, open standard. Anyone is free to implement their own uploader or viewer, or make proprietary changes to the standard for their own organizations needs. If you have changes that you would like to see incorporated into the public standard, please contact us.
Please note: This page is technical and is intended for developers who wish to build their own cronum uploader or viewer. The standard is still a draft at this time, while we’re working on the implementation of all the necessary supporting pieces.
Status: Draft v1.1 (proposed).
This update formalizes two reference implementations of the NFU Uploader:
-
NFU Uploader (Public): public attachments and text-only unlockables (no encrypted media).
-
NFU Uploader (Verified): adds encrypted media unlockables (images/audio/video/3D/PDF) for verified creators/collections only, enforced via an allowlist and a Safety Manifest.
The spec adds optional v1.1 fields for encrypted-media support and safety metadata; it remains backward-compatible with v1.0.
Scope: Defines how creators attach public files and unlockables to existing NFTs and how viewers verify authenticity (EIP-712 proof), time (Arweave block timestamp), eligibility (ownership at timestamp or allowlist), and safety policy (Safety Manifest + blocklists) prior to decryption and display.
-
The Public build implements public files and text-only unlockables.
-
The Verified build additionally implements encrypted media unlockables gated by Lit Protocol and restricted to wallets/collections in an allowlist (managed separately).
-
Both builds emit Descriptor JSON and NFU-Proof artifacts compatible with this standard.
1) Design goals
-
Two deployment tracks:
-
Public build: simple, broadly accessible; never enables encrypted media.
-
Verified build: enables encrypted media unlockables only for approved wallets/collections; requires allowlist + Safety Manifest checks.
-
-
Safety-by-design: A public Safety Manifest in the Descriptor records plaintext SHA-256 (and optional perceptual hashes) plus policy signals. Viewers MUST consult local/remote blocklists and MAY refuse key retrieval or decryption if flagged.
-
Clear access control: Encrypted media keys are released only when Lit access-control conditions are satisfied and the uploader/viewer passes policy checks (e.g., allowlist for upload, revocation lists for view).
-
No key custody: All cryptography is end-user controlled; decryption happens client-side.
-
Time & identity anchors:
-
Arweave is the clock (Descriptor Tx block timestamp).
-
Ethereum is identity (EIP-712 NFU-Proof binding fileHash + descriptorTxId + NFT tuple to the uploader).
-
-
Viewer eligibility rule: Render only if the uploader owned the NFT at the Arweave timestamp (or appears on the verified allowlist) and safety checks pass.
-
Deterministic audit trail: Post (a) data (for public files or encrypted media), (b) Descriptor JSON (L1), and (c) NFU-Proof (L1) so any party can independently verify the state.
-
Progressive compatibility: v1.1 fields are optional; older viewers ignore unknown fields without breaking baseline behavior.
2) Artifacts & Descriptor JSON (v1.1)
This section defines the canonical on-chain/off-chain artifacts and the Descriptor JSON structure used by both uploader builds:
-
NFU Uploader (Public) — supports public files and text-only unlockables (no encrypted media).
-
NFU Uploader (Verified) — additionally supports encrypted media unlockables for verified creators/collections.
2.1 Required artifacts
-
Data Tx (Arweave)
-
Public build: the raw public file.
-
Verified build (encrypted media): the ciphertext of the media (not the plaintext).
-
-
Descriptor JSON (Arweave, L1) — machine-readable description of the attachment.
-
Acts as the clock: viewers MUST use the Descriptor’s Arweave block timestamp for “time of upload.”
-
v1.1 adds fields for encrypted media and the Safety Manifest.
-
-
NFU-Proof (Arweave, L1) — EIP-712-signed proof binding the upload to the uploader and the NFT tuple.
-
MUST include:
fileHash(plaintext hash),arweaveTxId(Descriptor id),contract,tokenId,standard,erc1155Amount,uploader.
-
All three artifacts SHOULD be linked to each other via tags and/or URLs for easy discovery.
2.2 Descriptor JSON (v1.1)
2.2.1 Common fields (all builds)
{
"schema": "nfu.descriptor.v1",
"kind": "public | unlockable",
"nft": {
"chain": "ethereum|polygon|arbitrum|base|optimism",
"contract": "0x...",
"tokenId": "string-decimal",
"standard": "erc721|erc1155"
},
"file": {
"name": "filename.ext",
"mime": "mime/type",
"size": 12345,
"ext": "ext"
},
"createdAt": "ISO-8601",
"app": { "name": "nfu-uploader", "version": "x.y.z" }
}
Requirements
-
schemaMUST benfu.descriptor.v1. -
kindMUST bepublicorunlockable. -
tokenIdMUST be a decimal string (no hex). -
standardMUST beerc721orerc1155.
2.2.2 Public files (both builds)
Add a storage pointer to the raw file:
"storage": {
"driver": "arweave",
"dataTxId": "",
"dataUrl": "https://arweave.net/"
}
Viewer rule: Render directly (no decryption needed). Safety checks MAY still be applied to URLs and metadata.
2.2.3 Unlockables — Public build (text-only)
For text unlockables, encryption output is embedded in the Descriptor:
"lit": {
"method": "encryptToJson | local-aes-gcm",
"chain": "ethereum",
"uacc": [ /* Lit access-control conditions */ ],
"packed": { /* encryptToJson output OR local GCM bundle */ }
},
"hashes": { "sha256": "" }
Viewer rule: Before requesting keys, MAY consult Safety Manifest if present (optional in Public build for text).
2.2.4 Unlockables — Verified build (encrypted media)
Encrypted media uses a ciphertext Data Tx plus explicit crypto metadata:
"storage": {
"driver": "arweave",
"dataTxId": "",
"dataUrl": "https://arweave.net/"
},
"lit": {
"method": "local-aes-gcm | encryptToJson",
"chain": "ethereum",
"uacc": [ /* Lit access-control conditions */ ]
},
"crypto": {
"alg": "AES-GCM",
"ivB64": "",
"cipherType": "binary-b64"
},
"hashes": {
"sha256": ""
},
"safety_manifest": {
"hashes": {
"sha256_plaintext": "",
"phash": "",
"video_keyframe_phash": ["", "..."]
},
"checks": {
"mime_whitelisted": true,
"size_ok": true,
"blocklist_match": false
},
"uploader_attestation": {
"aup_version": "YYYY-MM-DD",
"wallet": "0xUploader…",
"sig_eip712": ""
}
}
Viewer MUST:
-
Validate
mime_whitelisted === true,size_ok === true, andblocklist_match === false(and/or perform its own checks). -
Consult local/remote blocklists and any revocation lists; if flagged, the viewer MUST refuse key retrieval/decryption.
-
Obtain Lit key only after safety checks pass, then decrypt and render by MIME.
2.2.5 Backward compatibility
-
v1.1 fields (
storagefor encrypted media,crypto,safety_manifest) are optional. -
Older viewers MUST ignore unknown fields without failing baseline behavior (public files and text unlockables remain compatible).
2.3 Validation (normative)
Implementations MUST enforce:
-
NFT identity:
contractis a valid EVM address;tokenIdis decimal;standard∈ {erc721,erc1155}. -
Timestamps:
createdAtis ISO-8601; authoritative time is the Descriptor’s Arweave block timestamp. -
Hashes:
hashes.sha256MUST be the SHA-256 of the plaintext (not ciphertext). -
Encrypted media (Verified build):
storage.dataTxIdMUST reference the ciphertext;crypto.algMUST beAES-GCM;ivB64MUST be present. -
Safety: If
safety_manifestis present, viewers MUST evaluate it and MAY augment with their own checks; if any mandatory check fails, decryption MUST NOT proceed.
3) Upload, confirmation & NFU-Proof
This section defines the canonical upload flow, the NFU-Proof (an EIP-712 signed receipt), and the linkage between the Data Tx, the Descriptor JSON (authoritative “clock”), and the Proof. It applies to both uploader builds:
-
Public build: public files + text-only unlockables.
-
Verified build: adds encrypted-media unlockables for verified creators/collections.
3.1 Overview (high level)
-
Prepare content & metadata.
-
Compute required hashes (see §3.3).
-
If encrypted media (Verified build): encrypt client-side and produce a ciphertext blob + crypto metadata.
-
-
Post Data (if applicable).
-
Public file → post raw file to Arweave as Data Tx.
-
Encrypted media → post ciphertext to Arweave as Data Tx.
-
Text-unlockable (Public build) → no Data Tx required (ciphertext or pack may be embedded in Descriptor).
-
-
Post Descriptor JSON (L1).
-
Always post a Descriptor as an L1 Arweave transaction.
-
Viewers MUST use the Descriptor’s Arweave block timestamp as the authoritative upload time.
-
-
Wait for confirmation.
-
Implementations SHOULD obtain at least 1 Arweave confirmation and record the block height/timestamp.
-
-
Create & post NFU-Proof (L1).
-
Sign the EIP-712 NFU-Proof with the uploader’s EVM wallet and post it to Arweave (L1).
-
-
Link artifacts.
-
Use tags/fields so Data ⇄ Descriptor ⇄ Proof are discoverable (see §3.5).
-
3.2 NFU-Proof (EIP-712) — normative
The NFU-Proof binds the plaintext content (or the exact public bytes) to the Descriptor and the NFT tuple, signed by the uploader’s wallet on the relevant EVM chain.
3.2.1 Domain (recommended)
{
"name": "NFU Upload",
"version": "1",
"chainId":
}
Implementations MAY change
name/version, but SHOULD keep a stable domain across releases.
3.2.2 Types (required)
{
"NFUReceipt": [
{"name":"fileHash","type":"bytes32"},
{"name":"arweaveTxId","type":"string"},
{"name":"contract","type":"address"},
{"name":"tokenId","type":"uint256"},
{"name":"standard","type":"string"},
{"name":"erc1155Amount","type":"uint256"},
{"name":"uploader","type":"address"}
]
}
4) Eligibility & Safety Evaluation
This section defines what a viewer MUST check before rendering any attachment and in what order. It applies to both uploader builds, with additional rules for the Verified (encrypted-media) build.
4.1 Goals (summary)
-
Prevent decryption/rendering when policy or safety checks fail.
-
Make results deterministic across viewers.
-
Keep privacy: never transmit plaintext to third parties during checks.
4.2 Inputs
A conforming viewer relies on:
-
Descriptor JSON (L1) — authoritative “clock” via its Arweave block timestamp (see §2).
-
NFU-Proof (L1) — EIP-712 receipt (see §3).
-
Allowlist Manifest — from NFU Guard (or equivalent), read-only JSON containing approved wallets/collections and a flag
enforceMediaAllowlist. -
Blocklists — local + remote (e.g., SHA-256, pHash) used for pre-decryption safety decisions.
-
Revocation List — JSON of
descriptorTxIdand/or hashes that official viewers MUST refuse to show.
The Allowlist/Blocklist/Revocations MAY be served by your own endpoint(s) and SHOULD support ETag/Cache-Control.
4.3 Upload-time checks (creator client) — SHOULD
Before any posting (especially for encrypted media), the creator client SHOULD:
-
Validate allowlist (verified build only; uploader wallet or collection is allowed).
-
Compute plaintext hashes (SHA-256; optional pHash / video keyframe pHashes).
-
Check blocklists → if any match, abort.
-
Build Safety Manifest and include it in the Descriptor draft (see §2).
These checks reduce the chance that permanent storage receives disallowed content.
4.4 View-time gate (mandatory order)
A viewer MUST evaluate in the following order before requesting/releasing any decryption key:
-
Descriptor integrity
-
Fetch Descriptor (L1) and confirm it exists.
-
Record its Arweave block timestamp
t_A(authoritative upload time).
-
-
Revocation check
-
If
descriptorTxId(or its hashes in Safety Manifest) appears in a revocation list, STOP (do not decrypt or render).
-
-
Kind & build rules
-
If
kind=public→ proceed to §4.8 (Proof/ownership) and render without decryption. -
If
kind=unlockable+ Public build (text-only) → continue. -
If
kind=unlockable+ Verified build (encrypted media) → continue and apply §4.6 (allowlist) and §4.7 (safety).
-
-
Safety Manifest (if present)
-
Validate shape; if malformed, viewer MAY refuse to continue for encrypted media.
-
If
checks.blocklist_match === trueorchecks.mime_whitelisted === falseorchecks.size_ok === false→ STOP.
-
-
External blocklists (local/remote)
-
Recompute/confirm Descriptor-provided hashes (when possible) and compare to configured blocklists.
-
On match → STOP.
-
-
Key-release gating (unlockables only)
-
Only if steps (1)–(5) pass, request Lit key per
lit.uacc. -
If policy requires “ownership-at-time” (below), a viewer MAY preflight that check before requesting the key to avoid unnecessary key traffic.
-
-
Decrypt (unlockables)
-
Decrypt client-side (never upload plaintext).
-
Compute keccak256/SHA-256 of plaintext for §4.8 verification.
-
-
Proof & eligibility (all kinds)
-
Verify NFU-Proof, historical ownership (or allowlist), then render.
-
4.5 Ownership-at-time rule (normative)
Definition: The uploader must have owned the NFT at t_A (the Descriptor’s Arweave block timestamp), or the uploader wallet/collection must appear on a configured allowlist (policy choice).
-
ERC-721: resolve
ownerOf(tokenId)over time; prove that the uploader EOA controlled the NFT att_A(by locating adjacent transfer events or using an indexer capable of point-in-time ownership). -
ERC-1155: prove
balanceOf(uploader, tokenId) > 0att_A.
Viewer MUST implement one of:
-
Strict mode (recommended): require uploader ownership at
t_A. If ambiguous, treat as not eligible. -
Allowlist override: if uploader (or the NFT’s collection) is on the allowlist, ownership-at-time MAY be waived (policy dependent).
4.6 Allowlist policy (verified build)
-
If
enforceMediaAllowlist=true, uploaders of encrypted media MUST be in the allowlist (by wallet) or the NFT’s contract MUST be in an approved collection allowlist. -
A viewer MUST check that the
uploaderrecovered from the NFU-Proof signature is allowlisted (directly or via collection rule) when policy is enabled.
4.7 Blocklists & Safety checks
-
Hashes to check:
-
hashes.sha256(Descriptor) /safety_manifest.hashes.sha256_plaintext(same value) -
Optional
safety_manifest.hashes.phash(images) -
Optional
safety_manifest.hashes.video_keyframe_phash[](videos)
-
-
Outcome: Any positive match in configured blocklists (local or remote) MUST result in no key request and no rendering.
-
Network hygiene (optional): Check URLs in Descriptors against anti-abuse URL feeds; a positive match SHOULD prevent rendering external links.
4.8 Proof & content verification (post-decryption where applicable)
For all attachments, the viewer MUST:
-
Verify NFU-Proof (see §3): recover signer; ensure the message fields match Descriptor/NFT tuple.
-
Recompute
fileHash:-
Public file: from raw bytes (Data Tx).
-
Unlockable: from plaintext after decryption.
Compare tofileHashin the EIP-712 message; on mismatch → STOP.
-
-
Eligibility: Apply §4.5 (ownership-at-time or allowlist).
-
Render if and only if all checks pass.
4.9 Decision outcomes (recommended)
Viewers SHOULD expose a consistent decision code for analytics and user messaging:
-
OK_RENDER— all checks passed. -
BLOCK_REVOCATION— revoked by policy list. -
BLOCK_BLOCKLIST— matched a forbidden hash/URL. -
BLOCK_ALLOWLIST— media not permitted for this uploader/collection. -
BLOCK_PROOF— invalid or mismatched NFU-Proof. -
BLOCK_ELIGIBILITY— uploader not owner att_A(and no allowlist override). -
BLOCK_DESCRIPTOR— malformed or missing required fields. -
BLOCK_DECRYPT— Lit key denied or crypto error.
4.10 Caching & freshness (recommended)
-
Allowlist/Blocklist/Revocations: respect
Cache-ControlandETag; refresh opportunistically in the background. -
Safety Manifest: trust only as a hint; recompute hashes when feasible (e.g., after decryption).
-
Fail-safe: on network errors fetching policy lists, default to conservative (do not request keys for encrypted media).
4.11 Privacy & logging (non-normative)
-
Never upload plaintext or user media to third-party services during view checks.
-
Log only high-level decisions (codes above) and artifact ids (descriptor/data/proof); avoid any PII beyond wallet addresses.
4.12 Minimal viewer sequence (pseudocode)
load(descriptorTxId) tA = arweave.blockTimestamp(descriptorTxId) if revoked(descriptorTxId) -> BLOCK_REVOCATION desc = fetchDescriptor(descriptorTxId) if desc.kind == "public": bytes = fetchData(desc.storage.dataTxId) if !verifyProof(bytes, desc, proof): BLOCK_PROOF if !eligibilityAtTime(proof.uploader, desc.nft, tA): BLOCK_ELIGIBILITY render(bytes); return OK_RENDER // unlockable: if verifiedBuildRequired(desc) && !allowlisted(proof.uploader, desc.nft): BLOCK_ALLOWLIST if safetyManifestSaysBlock(desc) || blocklistMatch(desc.safety_manifest.hashes): BLOCK_BLOCKLIST key = lit.getKey(desc.lit.uacc) // may include preflight eligibility pt = decrypt(bytesOrEmbedded, key, desc.crypto) if !verifyProof(pt, desc, proof): BLOCK_PROOF if !eligibilityAtTime(proof.uploader, desc.nft, tA): BLOCK_ELIGIBILITY render(pt); return OK_RENDER
5) Policy Manifests (Allowlist, Blocklists & Revocations)
This section standardizes the JSON manifests that viewers and uploaders consult for policy decisions. Implementations MAY serve these from any origin; paths shown are examples only.
5.1 Goals
-
Provide deterministic, cacheable inputs for eligibility and safety.
-
Keep payloads small and schemas stable so third-party viewers can comply.
-
Permit soft failures to default to conservative behavior.
5.2 Allowlist Manifest (wallets & collections)
Purpose: Indicate who may upload encrypted media (Verified build) and whether enforcement is active.
-
URL (example):
/wp-json/nfu-guard/v1/manifest -
Cache:
Cache-Control: public, max-age=300andETagMUST be supported. -
Schema (normative):
{
"schema": "nfu.guard.manifest.v1",
"version": 12,
"updatedAt": "2025-09-27T12:00:00Z",
"enforceMediaAllowlist": true,
"allow": [
{ "chain": "ethereum", "address": "0xabc…1234", "label": "Museum A" },
{ "chain": "polygon", "address": "0xdef…5678", "label": "Gallery B" }
],
"block": [
{ "algo": "sha256", "hash": "0123…abcd", "label": "Known bad" }
]
}
{
"schema": "nfu.blocklist.v1",
"version": 7,
"updatedAt": "2025-09-27T12:00:00Z",
"entries": [
{ "algo": "sha256", "hash": "ff3a…99e1", "label": "CSAM known hash" },
{ "algo": "phash", "hash": "a1b2c3d4e5f6a1b2", "label": "Matched image set" },
{ "algo": "video_keyframe_phash", "hash": "deadbeef…", "label": "Keyframe pHash" }
]
}
{
"schema": "nfu.revocations.v1",
"version": 3,
"updatedAt": "2025-09-27T12:00:00Z",
"revoke": [
{ "descriptorTxId": "abcd…", "reason": "Policy violation" },
{ "sha256_plaintext": "0123…abcd", "reason": "Law enforcement request" }
]
}
type Allowlist = {
enforceMediaAllowlist: boolean;
allow: { chain: string; address: string }[];
};
type BlockEntry = { algo: "sha256"|"phash"|"video_keyframe_phash"; hash: string };
type Revocations = { revoke: ({ descriptorTxId?: string; sha256_plaintext?: string })[] };
async function fetchAllowlist(): Promise { /* GET + ETag */ }
async function fetchBlocklists(): Promise { /* merge N feeds */ }
async function fetchRevocations(): Promise { /* GET + ETag */ }
6) Cryptography & Access Control (normative)
This section standardizes how encrypted media unlockables are produced (Verified build), how keys are controlled, and how viewers decrypt and verify them. Text-only unlockables (Public build) reuse a subset of these rules.
6.1 Algorithms & parameters
-
Symmetric cipher:
AES-256-GCM(256-bit key). -
IV / nonce: 96-bit random per encrypted object. Never reuse an IV with the same key.
-
Auth tag: 128-bit (GCM tag).
-
Key material: 32 random bytes (cryptographically secure RNG).
-
Encoding: raw bytes for crypto; public fields encoded as:
-
ivB64: Base64 (URL-safe not required). -
Ciphertext: stored as raw bytes in the Data Tx (recommended), or Base64 if embedded (text-only path).
-
-
Hashing (dual discipline):
-
keccak256(plaintext) → used in NFU-Proof.fileHash (EIP-712).
-
sha256(plaintext) → stored in
hashes.sha256and insafety_manifest.hashes.sha256_plaintext.
-
Rationale: AES-GCM provides confidentiality + integrity; dual hashes support both EVM tooling (keccak) and safety tooling (sha256).
6.2 Lit access control (UACC profiles)
Implementations MUST use Lit Protocol access control conditions (UACC) that are equivalent to:
-
ERC-721 (single owner):
[{
"conditionType":"evmBasic",
"standardContractType":"ERC721",
"chain":"",
"contractAddress":"",
"method":"ownerOf",
"parameters":[""],
"returnValueTest":{"comparator":"=","value":":userAddress"}
}]
[{
"conditionType":"evmBasic",
"standardContractType":"ERC1155",
"chain":"",
"contractAddress":"",
"method":"balanceOf",
"parameters":[":userAddress",""],
"returnValueTest":{"comparator":">","value":"0"}
}]
"storage": { "driver":"arweave", "dataTxId":"", "dataUrl":"https://arweave.net/" },
"lit": { "method":"local-aes-gcm|encryptToJson", "chain":"ethereum", "uacc":[ ... ] },
"crypto": { "alg":"AES-GCM", "ivB64":"<...>", "cipherType":"binary-b64" },
"hashes": { "sha256":"" },
"safety_manifest": { /* as defined in §2 */ }
key = random(32)
iv = random(12)
ct = AES_256_GCM_Encrypt(key, iv, plaintext)
saveEncryptionKey(uacc, chain, key, authSig)
post Data Tx := ct
post Descriptor := { storage, lit, crypto{ivB64}, hashes.sha256, safety_manifest }
wait ≥1 confirm
sign EIP-712 NFU-Proof with keccak256(plaintext)
post Proof (L1)
desc = fetch Descriptor (L1) prechecks := revocation + allowlist + safety + blocklists (see §4) if fail -> stop key = lit.getKey(uacc) ct = fetch ciphertext (Data Tx) OR packed from Descriptor pt = AES_256_GCM_Decrypt(key, ivB64, ct) verify NFU-Proof with keccak256(pt) check ownership-at-time or allowlist (policy) render
// inputs: descriptorTxId
const desc = await fetchDescriptorL1(descriptorTxId);
const tA = await getArweaveBlockTimestamp(descriptorTxId);
// 1) Revocations
if (revoked(desc, descriptorTxId)) return BLOCK_REVOCATION;
// 2) Kind rules
if (desc.kind === "public") {
const bytes = await fetchDataTx(desc.storage.dataTxId);
if (!verifyEip712Proof(bytes, desc, proof)) return BLOCK_PROOF;
if (!eligibilityAtTime(proof.uploader, desc.nft, tA)) return BLOCK_ELIGIBILITY;
return renderPublic(bytes);
}
// 3) Unlockables (text or media)
if (verifiedBuildRequired(desc) && !allowlisted(proof.uploader, desc.nft))
return BLOCK_ALLOWLIST;
// 4) Safety checks
if (safetyManifestBlocks(desc) || blocklistMatch(desc.safety_manifest?.hashes))
return BLOCK_BLOCKLIST;
// 5) Key + decrypt (never before safety/allowlist checks)
const key = await litGetKey(desc.lit.uacc, desc.lit.chain);
const ct = await fetchCipherOrPacked(desc);
const pt = await decryptAesGcm(key, desc.crypto.ivB64, ct);
// 6) Proof & eligibility
if (!verifyEip712Proof(pt, desc, proof)) return BLOCK_PROOF;
if (!eligibilityAtTime(proof.uploader, desc.nft, tA)) return BLOCK_ELIGIBILITY;
return renderUnlockable(pt, desc.file.mime);
{
"schema": "nfu.descriptor.v1",
"kind": "public",
"nft": { "chain":"ethereum", "contract":"0x…", "tokenId":"1234", "standard":"erc721" },
"file": { "name":"poster.png", "mime":"image/png", "size":123456, "ext":"png" },
"storage": { "driver":"arweave", "dataTxId":"", "dataUrl":"https://arweave.net/" },
"hashes": { "sha256":"" },
"createdAt": "2025-09-27T00:00:00Z",
"app": { "name":"nfu-uploader", "version":"x.y.z" }
}
{
"schema":"nfu.descriptor.v1",
"kind":"unlockable",
"nft":{ "chain":"ethereum","contract":"0x…","tokenId":"1234","standard":"erc721" },
"file":{ "name":"clip.mp4","mime":"video/mp4","size":73400320,"ext":"mp4" },
"storage":{ "driver":"arweave","dataTxId":"","dataUrl":"https://arweave.net/" },
"lit":{ "method":"local-aes-gcm","chain":"ethereum","uacc":[/* ERC721/1155 ACC */] },
"crypto":{ "alg":"AES-GCM","ivB64":"","cipherType":"binary-b64" },
"hashes":{ "sha256":"" },
"safety_manifest":{
"hashes":{ "sha256_plaintext":"","phash":"","video_keyframe_phash":[""] },
"checks":{ "mime_whitelisted":true,"size_ok":true,"blocklist_match":false },
"uploader_attestation":{ "aup_version":"2025-09-27","wallet":"0xUploader…","sig_eip712":"" }
},
"createdAt":"2025-09-27T00:00:00Z",
"app":{ "name":"nfu-uploader","version":"x.y.z" }
}
{
"domain": { "name":"NFU Upload","version":"1","chainId": },
"types": {
"NFUReceipt": [
{"name":"fileHash","type":"bytes32"},
{"name":"arweaveTxId","type":"string"},
{"name":"contract","type":"address"},
{"name":"tokenId","type":"uint256"},
{"name":"standard","type":"string"},
{"name":"erc1155Amount","type":"uint256"},
{"name":"uploader","type":"address"}
]
},
"primaryType":"NFUReceipt",
"message":{ /* per §3.2.3 */ }
}
async function fetchJson(url:string, etag?:string){
const res = await fetch(url, { headers: etag ? { "If-None-Match": etag } : {} });
if (res.status === 304) return { etag, json: null, notModified: true };
const newEtag = res.headers.get("ETag") || undefined;
return { etag: newEtag, json: await res.json() };
}
// Example endpoints
const GUARD = "/wp-json/nfu-guard/v1/manifest";
const REVOC = "/.well-known/nfu/revocations.json";
8) Conformance & Feature Registry
This section defines who implements the standard, what must be implemented at each level, and a simple feature registry so third-party sites and apps can advertise support.
8.1 Roles
-
Creator Client (Uploader): builds artifacts and posts them (Data Tx, Descriptor, NFU-Proof).
-
Viewer: evaluates eligibility/safety and renders content.
-
Policy Server: serves allowlist/blocklist/revocation manifests (e.g., NFU Guard).
-
Indexer (optional): caches/derives ownership-at-time data for faster viewer checks.
8.2 Uploader conformance profiles
-
Uploader (Public-Conformant)
MUST:-
Produce Descriptor (L1) per §2 with
schema:"nfu.descriptor.v1". -
For public files: post Data Tx (raw bytes) +
storage.dataTxId. -
For text unlockables: embed encrypted pack (
lit.packed) and includehashes.sha256of plaintext. -
Post NFU-Proof (L1) per §3 with
fileHash = keccak256(plaintext-or-public-bytes). -
Wait ≥1 Arweave confirmation on Descriptor before posting NFU-Proof.
SHOULD: -
Add basic
App-Name/App-Version/NFU-*tags (§7.5).
-
-
Uploader (Verified-Conformant)
MUST implement all Public requirements plus:-
Encrypted-media workflow per §2.2.4 / §6 (AES-256-GCM,
crypto.ivB64, ciphertext in Data Tx). -
Lit access control (UACC) per §6.2 and key save.
-
Safety Manifest populated and truthful (§2.2.4).
-
Preflight policy: fetch allowlist/blocklists/revocations and refuse upload on match (§4.3).
SHOULD: -
Enforce MIME/size whitelist (§6.6).
-
Include pHash / keyframe pHashes when applicable.
-
8.3 Viewer conformance profiles
-
Viewer (Baseline) — supports public files and text unlockables.
MUST:-
Use Descriptor L1 block timestamp as the clock (§2).
-
Verify NFU-Proof and recompute
fileHashfrom plaintext/public bytes (§3, §4.8). -
Enforce ownership-at-time or declared policy (§4.5).
SHOULD: -
Honor revocation list; render only after ≥1 Descriptor confirmation.
-
-
Viewer (Safe) — adds encrypted media & policy enforcement.
MUST (Baseline +):-
Apply revocation before anything (§4.4).
-
For encrypted media: require allowlist if policy says so (§4.6).
-
Apply blocklists before key requests; refuse on match (§4.7).
-
Request Lit key only after prechecks; decrypt client-side (§6.5).
-
Verify NFU-Proof using keccak256(plaintext) after decryption (§4.8).
SHOULD: -
Validate
Safety Manifestshape/claims; recompute hashes post-decrypt.
-
-
Viewer (Strict) — Safe + additional defenses.
SHOULD:-
Preflight ownership-at-time before key requests to avoid unnecessary key traffic.
-
Require pHash/keyframe pHash checks for images/video when present.
-
Fail closed on manifest/network errors for encrypted media (§4.10).
-
8.4 Required behaviors (matrix)
| Requirement | Public Uploader | Verified Uploader | Baseline Viewer | Safe Viewer | Strict Viewer |
|---|---|---|---|---|---|
| Descriptor v1 (§2) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Data Tx (public or ciphertext) | ✅* | ✅ | ✅ | ✅ | ✅ |
| Text unlockables | ✅ | ✅ | ✅ | ✅ | ✅ |
| Encrypted media | — | ✅ | — | ✅ | ✅ |
| NFU-Proof EIP-712 (§3) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ownership-at-time (§4.5) | — | — | ✅ | ✅ | ✅ |
| Allowlist enforcement (§4.6) | — | ✅ | — | ✅ | ✅ |
| Blocklists & revocations (§4.4/§4.7) | SHOULD | MUST | SHOULD | MUST | MUST |
| Safety Manifest (§2.2.4) | — | MUST | — | MUST-check | MUST-check |
* For text unlockables, Public Uploader may omit a Data Tx (cipher embedded).
8.5 Feature registry (advertising support)
Implementations MAY publish a tiny capability file so other apps know what to expect. Suggested location:/.well-known/cronum-capabilities.json
{
"schema": "cronum.capabilities.v1",
"product": "MyViewer",
"version": "2.3.1",
"roles": ["viewer"],
"features": {
"descriptor_v1": true,
"public_files": true,
"text_unlockables": true,
"encrypted_media": true,
"lit_uacc": ["erc721.ownerOf", "erc1155.balanceOf"],
"allowlist_required": true,
"blocklists": ["sha256","phash","video_keyframe_phash"],
"revocations": true,
"ownership_at_time": "strict"
},
"manifests": {
"allowlist": "https://example.org/wp-json/nfu-guard/v1/manifest",
"revocations": "https://example.org/.well-known/nfu/revocations.json"
}
}
8.6 Self-test checklist (interoperability)
-
Artifacts: Create known vectors (public file, text unlockable, encrypted image/video) and verify another implementation can:
(a) discover artifacts via tags; (b) confirm Descriptor; (c) verify NFU-Proof; (d) enforce policy; (e) render. -
Negative cases: Ensure viewer blocks on each code path:
BLOCK_REVOCATION,BLOCK_BLOCKLIST,BLOCK_ALLOWLIST,BLOCK_PROOF,BLOCK_ELIGIBILITY,BLOCK_DESCRIPTOR,BLOCK_DECRYPT. -
Timing: Validate “ownership-at-time” by testing uploads around a transfer boundary.
-
Privacy: Confirm no plaintext or keys leave the client during checks/decrypt.
8.7 Versioning & upgrades
-
This document is v1.1. New optional fields MUST NOT break existing viewers; unknown fields MUST be ignored.
-
Future versions MAY add: chunked/streaming encryption, attested viewers, or additional ACC profiles; implementers SHOULD advertise support via the capabilities file.
Appendix A: JSON Schemas
Below are canonical JSON Schemas (Draft 2020-12) for implementers. They’re split into (A) base types, (B) Descriptor v1.1 (plus profiles), (C) NFU-Proof envelope, (D) Policy Manifests, and (E) Capabilities.
Notes
• Use these schemas for validation only; viewers still MUST apply the normative rules in §§3–6.
• Arweave IDs are Base64URL; we allow a safe range in the regex.
• Ethereum addresses/signatures are hex-encoded.
A) Shared base types
{
"$id": "https://cronum.art/schemas/base.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Cronum Base Types",
"type": "object",
"properties": {},
"$defs": {
"chain": { "type": "string", "enum": ["ethereum","polygon","arbitrum","base","optimism"] },
"evmAddress": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
"evmSigHex65": { "type": "string", "pattern": "^0x[0-9a-fA-F]{130}$" },
"arweaveId": { "type": "string", "pattern": "^[A-Za-z0-9_-]{43,64}$" },
"mime": { "type": "string", "pattern": "^[\\w!#$&^_.+-]+\\/[\\w!#$&^_.+-]+$" },
"hex32": { "type": "string", "pattern": "^0x[0-9a-fA-F]{64}$" },
"sha256hex": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"pHashHex": { "type": "string", "pattern": "^[0-9a-f]{8,64}$" },
"isoDateTime": { "type": "string", "format": "date-time" },
"isoDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
"tokenIdDecimal": { "type": "string", "pattern": "^[0-9]+$" },
"standard": { "type": "string", "enum": ["erc721","erc1155"] }
}
}
B) Descriptor JSON v1.1
B.1 Descriptor (base)
{
"$id": "https://cronum.art/schemas/descriptor.v1.1.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NFU Descriptor v1.1",
"type": "object",
"required": ["schema","kind","nft","file","createdAt","app"],
"properties": {
"schema": { "const": "nfu.descriptor.v1" },
"kind": { "type": "string", "enum": ["public","unlockable"] },
"nft": {
"type": "object",
"required": ["chain","contract","tokenId","standard"],
"properties": {
"chain": { "$ref": "base.json#/$defs/chain" },
"contract": { "$ref": "base.json#/$defs/evmAddress" },
"tokenId": { "$ref": "base.json#/$defs/tokenIdDecimal" },
"standard": { "$ref": "base.json#/$defs/standard" }
},
"additionalProperties": false
},
"file": {
"type": "object",
"required": ["name","mime","size","ext"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"mime": { "$ref": "base.json#/$defs/mime" },
"size": { "type": "integer", "minimum": 0 },
"ext": { "type": "string" }
},
"additionalProperties": false
},
"storage": {
"type": "object",
"required": ["driver"],
"properties": {
"driver": { "type": "string", "enum": ["arweave"] },
"dataTxId": { "$ref": "base.json#/$defs/arweaveId" },
"dataUrl": { "type": "string", "format": "uri" }
},
"additionalProperties": false
},
"lit": {
"type": "object",
"required": ["chain","uacc"],
"properties": {
"method": { "type": "string", "enum": ["encryptToJson","local-aes-gcm"] },
"chain": { "$ref": "base.json#/$defs/chain" },
"uacc": { "type": "array", "minItems": 1, "items": { "type": "object" } },
"packed": { "type": "object" } /* text-only path */
},
"additionalProperties": true
},
"crypto": {
"type": "object",
"required": ["alg","ivB64","cipherType"],
"properties": {
"alg": { "type": "string", "const": "AES-GCM" },
"ivB64": { "type": "string", "pattern": "^[A-Za-z0-9+/=]+$" },
"cipherType": { "type": "string", "enum": ["binary-b64"] }
},
"additionalProperties": false
},
"hashes": {
"type": "object",
"required": ["sha256"],
"properties": {
"sha256": { "$ref": "base.json#/$defs/sha256hex" }
},
"additionalProperties": false
},
"safety_manifest": {
"$ref": "https://cronum.art/schemas/safety-manifest.v1.json"
},
"createdAt": { "$ref": "base.json#/$defs/isoDateTime" },
"app": {
"type": "object",
"required": ["name","version"],
"properties": {
"name": { "type": "string" },
"version": { "type": "string" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}
B.3 Profiles (validation helpers)
These optional schemas add constraints for each usage pattern.
Public file profile
{
"$id": "https://cronum.art/schemas/profile.public-file.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{ "$ref": "descriptor.v1.1.json" },
{
"properties": {
"kind": { "const": "public" },
"storage": { "required": ["dataTxId","dataUrl"] }
},
"required": ["storage"]
}
]
}
Unlockable — text-only (Public build)
{
"$id": "https://cronum.art/schemas/profile.unlockable-text.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{ "$ref": "descriptor.v1.1.json" },
{
"properties": {
"kind": { "const": "unlockable" },
"lit": { "required": ["packed"] }
},
"required": ["lit","hashes"],
"not": { "required": ["crypto"] }
}
]
}
Unlockable — encrypted media (Verified build)
{
"$id": "https://cronum.art/schemas/profile.unlockable-encrypted-media.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{ "$ref": "descriptor.v1.1.json" },
{
"properties": {
"kind": { "const": "unlockable" },
"storage": { "required": ["dataTxId","dataUrl"] },
"crypto": { "required": ["alg","ivB64","cipherType"] },
"safety_manifest": {}
},
"required": ["storage","crypto","lit","hashes","safety_manifest"]
}
]
}
C) NFU-Proof (envelope posted to Arweave)
{
"$id": "https://cronum.art/schemas/nfu-proof.v1.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NFU Proof Envelope v1",
"type": "object",
"required": ["nfu_proof"],
"properties": {
"nfu_proof": {
"type": "object",
"required": [
"v","chainId","contract","tokenId","standard",
"uploader","fileHash","arweave","signature","sigStandard"
],
"properties": {
"v": { "type": "string", "const": "1.0" },
"chainId": { "type": "integer", "minimum": 1 },
"contract": { "$ref": "base.json#/$defs/evmAddress" },
"tokenId": { "type": "string", "pattern": "^[0-9]+$" },
"standard": { "type": "string", "enum": ["ERC721","ERC1155"] },
"uploader": { "$ref": "base.json#/$defs/evmAddress" },
"fileHash": { "$ref": "base.json#/$defs/hex32" },
"arweave": {
"type": "object",
"required": ["txId"],
"properties": {
"txId": { "$ref": "base.json#/$defs/arweaveId" },
"blockHeight": { "type": ["integer","null"], "minimum": 0 },
"blockTimestamp": { "type": ["integer","null"], "minimum": 0 }
},
"additionalProperties": false
},
"signature": { "$ref": "base.json#/$defs/evmSigHex65" },
"sigStandard": { "type": "string", "const": "EIP-712" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}
D) Policy Manifests
D.1 Allowlist (NFU Guard manifest)
{
"$id": "https://cronum.art/schemas/nfu-guard.manifest.v1.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NFU Guard Manifest v1",
"type": "object",
"required": ["schema","version","updatedAt","enforceMediaAllowlist","allow","block"],
"properties": {
"schema": { "const": "nfu.guard.manifest.v1" },
"version": { "type": "integer", "minimum": 1 },
"updatedAt": { "$ref": "base.json#/$defs/isoDateTime" },
"enforceMediaAllowlist": { "type": "boolean" },
"allow": {
"type": "array",
"items": {
"type": "object",
"required": ["chain","address"],
"properties": {
"chain": { "$ref": "base.json#/$defs/chain" },
"address": { "$ref": "base.json#/$defs/evmAddress" },
"label": { "type": "string" }
},
"additionalProperties": false
}
},
"block": {
"type": "array",
"items": {
"type": "object",
"required": ["algo","hash"],
"properties": {
"algo": { "type": "string", "enum": ["sha256","phash","video_keyframe_phash"] },
"hash": { "type": "string" },
"label": { "type": "string" }
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
D.2 Blocklist feed
{
"$id": "https://cronum.art/schemas/nfu.blocklist.v1.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NFU Blocklist v1",
"type": "object",
"required": ["schema","version","updatedAt","entries"],
"properties": {
"schema": { "const": "nfu.blocklist.v1" },
"version": { "type": "integer", "minimum": 1 },
"updatedAt": { "$ref": "base.json#/$defs/isoDateTime" },
"entries": {
"type": "array",
"items": {
"type": "object",
"required": ["algo","hash"],
"properties": {
"algo": { "type": "string", "enum": ["sha256","phash","video_keyframe_phash"] },
"hash": { "type": "string" },
"label": { "type": "string" }
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
D.3 Revocations
{
"$id": "https://cronum.art/schemas/nfu.revocations.v1.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NFU Revocations v1",
"type": "object",
"required": ["schema","version","updatedAt","revoke"],
"properties": {
"schema": { "const": "nfu.revocations.v1" },
"version": { "type": "integer", "minimum": 1 },
"updatedAt": { "$ref": "base.json#/$defs/isoDateTime" },
"revoke": {
"type": "array",
"items": {
"type": "object",
"minProperties": 1,
"properties": {
"descriptorTxId": { "$ref": "base.json#/$defs/arweaveId" },
"sha256_plaintext": { "$ref": "base.json#/$defs/sha256hex" },
"reason": { "type": "string" }
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
E) Capabilities file (feature registry)
{
"$id": "https://cronum.art/schemas/cronum.capabilities.v1.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Cronum Capabilities v1",
"type": "object",
"required": ["schema","product","version","roles","features"],
"properties": {
"schema": { "const": "cronum.capabilities.v1" },
"product": { "type": "string" },
"version": { "type": "string" },
"roles": { "type": "array", "items": { "type": "string", "enum": ["viewer","uploader","policy","indexer"] } },
"features": {
"type": "object",
"properties": {
"descriptor_v1": { "type": "boolean" },
"public_files": { "type": "boolean" },
"text_unlockables": { "type": "boolean" },
"encrypted_media": { "type": "boolean" },
"lit_uacc": { "type": "array", "items": { "type": "string" } },
"allowlist_required": { "type": "boolean" },
"blocklists": { "type": "array", "items": { "type": "string", "enum": ["sha256","phash","video_keyframe_phash"] } },
"revocations": { "type": "boolean" },
"ownership_at_time": { "type": "string", "enum": ["off","basic","strict"] }
},
"additionalProperties": false
},
"manifests": {
"type": "object",
"properties": {
"allowlist": { "type": "string", "format": "uri" },
"revocations": { "type": "string", "format": "uri" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}
How to use
-
Host these at predictable URLs (e.g.,
/schemas/*.json) and reference via$idto enable$refresolution. -
Validators: Ajv (JS), jsonschema (Python), everit (Java) all support Draft 2020-12.
-
For Descriptor validation, apply the base schema then one of the profiles depending on the artifact type.
