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

  • schema MUST be nfu.descriptor.v1.

  • kind MUST be public or unlockable.

  • tokenId MUST be a decimal string (no hex).

  • standard MUST be erc721 or erc1155.

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:

  1. Validate mime_whitelisted === true, size_ok === true, and blocklist_match === false (and/or perform its own checks).

  2. Consult local/remote blocklists and any revocation lists; if flagged, the viewer MUST refuse key retrieval/decryption.

  3. Obtain Lit key only after safety checks pass, then decrypt and render by MIME.

2.2.5 Backward compatibility

  • v1.1 fields (storage for 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: contract is a valid EVM address; tokenId is decimal; standard ∈ {erc721,erc1155}.

  • Timestamps: createdAt is ISO-8601; authoritative time is the Descriptor’s Arweave block timestamp.

  • Hashes: hashes.sha256 MUST be the SHA-256 of the plaintext (not ciphertext).

  • Encrypted media (Verified build): storage.dataTxId MUST reference the ciphertext; crypto.alg MUST be AES-GCM; ivB64 MUST be present.

  • Safety: If safety_manifest is 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)

  1. Prepare content & metadata.

    • Compute required hashes (see §3.3).

    • If encrypted media (Verified build): encrypt client-side and produce a ciphertext blob + crypto metadata.

  2. 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).

  3. 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.

  4. Wait for confirmation.

    • Implementations SHOULD obtain at least 1 Arweave confirmation and record the block height/timestamp.

  5. Create & post NFU-Proof (L1).

    • Sign the EIP-712 NFU-Proof with the uploader’s EVM wallet and post it to Arweave (L1).

  6. 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"}
  ]
}

3.2.3 Message semantics (required)

  • fileHashkeccak256(bytes) of the plaintext (see §3.3).

    • Public file: keccak256 of the exact file bytes posted as Data Tx.

    • Text unlockable: keccak256 of the plaintext string.

    • Encrypted media: keccak256 of the plaintext media bytes (before encryption).

  • arweaveTxIdDescriptor JSON transaction id (L1).

  • contract — NFT contract address (EVM).

  • tokenId — decimal string value cast to uint256.

  • standard"ERC721" or "ERC1155" (uppercase).

  • erc1155Amount1 for ERC-1155, 0 for ERC-721`.

  • uploader — EOA address that signs the proof.

3.2.4 Verification (viewer MUST)

  • Recover signer from the EIP-712 signature; compare to uploader.

  • Confirm arweaveTxId exists and is the Descriptor used for this view.

  • Recompute fileHash:

    • Public file: from the fetched Data bytes.

    • Unlockable (text/media): after Lit key release and client-side decryption, from the plaintext.

  • Validate contract/tokenId/standard/erc1155Amount against the Descriptor.

  • Optionally check historical ownership at the Descriptor’s block timestamp (uploader owned the NFT at that instant) or apply allowlist policy (§4).

3.3 Hash discipline (normative)

  • EIP-712 fileHash MUST be keccak256 of plaintext as defined above.

  • Descriptor hashes.sha256 (and Safety Manifest hashes.sha256_plaintext) MUST be SHA-256 of the same plaintext.

  • Rationale: dual hashing eases ecosystem interoperability (EVM tooling for keccak256; safety/trust tooling for SHA-256).

3.4 Per-build workflows (normative)

3.4.1 Public build

  • Public file

    1. Post Data Tx (raw bytes).

    2. Post Descriptor (L1) with storage.dataTxId.

    3. Wait ≥1 confirmation.

    4. Compute fileHash = keccak256(raw_bytes); sign & post NFU-Proof (L1).

  • Text unlockable

    1. Produce encrypted pack (Lit or local AES-GCM).

    2. Post Descriptor (L1) embedding the pack (lit.packed) and hashes.sha256.

    3. Wait ≥1 confirmation.

    4. Compute fileHash = keccak256(plaintext_string); sign & post NFU-Proof (L1).

3.4.2 Verified build (encrypted media)

  1. Encrypt media client-side (AES-GCM); save symmetric key with Lit (UACC).

  2. Post Data Tx with ciphertext.

  3. Post Descriptor (L1) with storage.dataTxId, crypto (alg, iv), lit (uacc), hashes.sha256, and safety_manifest.

  4. Wait ≥1 confirmation.

  5. Compute fileHash = keccak256(plaintext_media_bytes); sign & post NFU-Proof (L1).

Safety gate: Implementations SHOULD run allowlist & blocklist checks before any posting (client-side) and MUST include/validate safety_manifest as specified in §2.

3.5 Arweave tags & linkage (recommended)

To make artifacts self-discoverable:

  • Data Tx (public or ciphertext):

    • App-Name: NFU-Data

    • NFU-Chain, NFU-Contract, NFU-TokenId, NFU-Standard

    • Content-Type: <mime> (public only)

    • NFU-Ext: <ext> (optional)

  • Descriptor (L1):

    • App-Name: NFU-Uploader

    • App-Version: <x.y.z>

    • NFU-Schema: nfu.descriptor.v1

    • NFU-Kind: public|unlockable

    • NFU-Chain, NFU-Contract, NFU-TokenId, NFU-Standard

    • NFU-Ext, NFU-CreatedAt

    • NFU-DataTxId: <txid> (if a Data Tx exists)

  • NFU-Proof (L1):

    • App-Name: NFU-Proof

    • App-Version: <x.y.z>

    • NFU-DescriptorTxId: <descriptor_txid>

    • NFU-Chain, NFU-Contract, NFU-TokenId, NFU-Standard

Implementations MAY also include canonical URLs in bodies for convenience:

  • https://arweave.net/<descriptor_txid>

  • https://arweave.net/<data_txid>

  • https://arweave.net/<proof_txid>

3.6 Confirmation requirements (recommended)

  • Descriptor (L1) SHOULD be confirmed (≥1) before posting NFU-Proof.

  • Viewers SHOULD treat unconfirmed Descriptors as pending and avoid rendering unlockables until confirmed.

  • If a bundled Data Tx is used, viewers MAY resolve parent L1 confirmation via GraphQL to establish inclusion time; the Descriptor’s block timestamp remains the authoritative clock.

3.7 Error handling & security notes (non-normative)

  • If any safety/eligibility check fails (allowlist, blocklist, revocation, or ownership-at-time), viewers MUST refuse key retrieval/decryption.

  • The NFU-Proof binds the plaintext to an immutable Descriptor id; altering the Descriptor requires a new proof.

  • Keep signer prompts clear (domain separation, human-readable summary) to avoid wallet-spoofing UX attacks.


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 descriptorTxId and/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:

  1. Validate allowlist (verified build only; uploader wallet or collection is allowed).

  2. Compute plaintext hashes (SHA-256; optional pHash / video keyframe pHashes).

  3. Check blocklists → if any match, abort.

  4. 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:

  1. Descriptor integrity

    • Fetch Descriptor (L1) and confirm it exists.

    • Record its Arweave block timestamp t_A (authoritative upload time).

  2. Revocation check

    • If descriptorTxId (or its hashes in Safety Manifest) appears in a revocation list, STOP (do not decrypt or render).

  3. 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).

  4. Safety Manifest (if present)

    • Validate shape; if malformed, viewer MAY refuse to continue for encrypted media.

    • If checks.blocklist_match === true or checks.mime_whitelisted === false or checks.size_ok === falseSTOP.

  5. External blocklists (local/remote)

    • Recompute/confirm Descriptor-provided hashes (when possible) and compare to configured blocklists.

    • On match → STOP.

  6. 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.

  7. Decrypt (unlockables)

    • Decrypt client-side (never upload plaintext).

    • Compute keccak256/SHA-256 of plaintext for §4.8 verification.

  8. 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 at t_A (by locating adjacent transfer events or using an indexer capable of point-in-time ownership).

  • ERC-1155: prove balanceOf(uploader, tokenId) > 0 at t_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 uploader recovered 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:

  1. Verify NFU-Proof (see §3): recover signer; ensure the message fields match Descriptor/NFT tuple.

  2. Recompute fileHash:

    • Public file: from raw bytes (Data Tx).

    • Unlockable: from plaintext after decryption.
      Compare to fileHash in the EIP-712 message; on mismatch → STOP.

  3. Eligibility: Apply §4.5 (ownership-at-time or allowlist).

  4. 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 at t_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-Control and ETag; 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=300 and ETag MUST 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" }
  ]
}
 

Viewer MUST:

  • Treat enforceMediaAllowlist=true as mandatory for encrypted media uploads (Verified build).

  • For viewing, recover the uploader from the NFU-Proof and require presence in allow or apply a configured collection-allowlist rule.

5.3 Hash Blocklist Feed(s)

Purpose: Provide lists of forbidden content hashes used for pre-decryption checks.

  • Multiple feeds allowed (local or remote). Implementations SHOULD support:

    • sha256 (plaintext content hash)

    • phash (image perceptual hash; hex)

    • video_keyframe_phash (array of pHashes)

  • Schema (normative):

 {
  "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" }
  ]
}

Viewer MUST:

  • Compare available Descriptor/Manifest hashes to all configured feeds before requesting keys.

  • On any match, refuse key retrieval and rendering.

5.4 Revocation List

Purpose: Emergency brake for known-bad descriptors or artifacts that should not be rendered, even if not in a hash feed.

  • Schema (normative):

 {
  "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" }
  ]
}

Viewer MUST: If descriptorTxId or any listed hash matches, do not request keys and do not render.

5.5 Caching, Integrity & Transport

  • Caching: All manifests SHOULD send Cache-Control and ETag. Viewers SHOULD honor them and perform conditional requests.

  • Integrity (optional): Servers MAY publish a detached signature (e.g., manifest.sig) with a public key. Viewers MAY verify signatures where required.

  • CORS: Public read-only manifests SHOULD set Access-Control-Allow-Origin: *.

5.6 Fallback Behavior

When a manifest endpoint is unreachable or invalid:

  • Allowlist: If enforceMediaAllowlist is unknown, viewers SHOULD default to deny for encrypted media uploads and refuse to render encrypted media.

  • Blocklists/Revocations: If unavailable, viewers SHOULD default to conservative behavior (do not request keys for encrypted media).

5.7 Minimal Client Interface (informative)

Implementations SHOULD expose a small client that:

 
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 */ }
 
Viewers and uploaders can share this library to ensure identical decisions.

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.sha256 and in safety_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"}
}]
  

  • ERC-1155 (balances):

  [{
  "conditionType":"evmBasic",
  "standardContractType":"ERC1155",
  "chain":"",
  "contractAddress":"",
  "method":"balanceOf",
  "parameters":[":userAddress",""],
  "returnValueTest":{"comparator":">","value":"0"}
}]
  

chain MUST be one of ethereum | polygon | arbitrum | base | optimism (lowercase). The Verified build MUST use these or stricter conditions.

6.3 Key creation, save & retrieval

  • Key creation (creator, client-side):

    1. Generate 32-byte random symmetric key.

    2. Generate 12-byte random IV.

    3. Encrypt plaintext with AES-GCM(key, iv) → ciphertext + tag.

  • Key save (creator → Lit):

    • Save symmetricKey with uacc + chain + creator’s authSig.

    • The Descriptor MUST NOT include any key material; it MUST include lit.uacc and crypto.ivB64.

  • Key retrieval (viewer → Lit):

    • Only after §4 eligibility/safety prechecks pass, request the symmetric key using the same uacc.

    • If Lit denies access or policy fails, the viewer MUST NOT attempt decryption.

6.4 Descriptor requirements for encrypted media (recap)

The Descriptor MUST include:

  
"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 */ }
   

For text-only unlockables (Public build), ciphertext MAY be embedded instead of using storage.dataTxId.

6.5 Encryption & decryption procedures

Creator (Verified build):

   
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)
    
Viewer (unlockable):
    
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
     

6.6 MIME whitelist & size limits (Verified build)

Implementations SHOULD restrict encrypted media to a conservative, reviewable set:

  • Images: image/png, image/jpeg, image/webp, image/gif (animated ok).

  • Video: video/mp4 (H.264/AAC), video/webm.

  • Audio: audio/mpeg, audio/wav, audio/ogg.

  • Documents/3D: application/pdf, model/gltf-binary (.glb).

A concrete max object size SHOULD be enforced (implementation-defined). If chunked/streaming encryption is not implemented, keep limits modest to avoid memory pressure.

6.7 Error handling (normative)

  • IV reuse detection: If the same ivB64 is seen with identical dataTxId but different hashes.sha256, viewers MUST treat as invalid.

  • Auth tag failure: On AES-GCM tag verify failure, treat as BLOCK_DECRYPT.

  • Descriptor mismatch: If crypto.algAES-GCM or required fields missing, treat as BLOCK_DESCRIPTOR.

  • Policy precedence: Any revocation or blocklist match preempts key requests.

6.8 Privacy notes (informative)

  • Decrypt in memory; do not persist plaintext to disk by default.

  • Do not transmit plaintext or symmetric keys to third-party services.

  • If thumbnails/previews are generated, ensure they inherit the same display rules and are never cached publicly.

6.9 Forward-compatibility

  • Fields unknown to v1.1 MUST be ignored by conforming viewers.

  • Future versions may add segmented encryption for large media (e.g., per-chunk AES-GCM with independent IVs); viewers MAY advertise support via feature flags but MUST continue to support v1.1 single-segment objects.


7) Implementation Guidance & Examples

This section turns the spec into pragmatic steps and reference snippets for both uploader builds and viewers. It also includes recommended Arweave tags and test vectors.

7.1 Uploader (Public build) — reference flow

Public file

  1. Read file bytes → compute sha256(plaintext) and keccak256(plaintext).

  2. Post Data Tx (raw bytes) with tags in §7.5.

  3. Post Descriptor (L1) with storage.dataTxId, hashes.sha256, etc.

  4. Wait ≥1 Arweave confirmation.

  5. Sign NFU-Proof (EIP-712) where fileHash = keccak256(raw bytes); post proof (L1).

Text unlockable

  1. Produce an encrypted pack (encryptToJson or local AES-GCM).

  2. Build Descriptor (L1) embedding the pack in lit.packed and include hashes.sha256.

  3. Wait ≥1 confirmation.

  4. Sign NFU-Proof with fileHash = keccak256(plaintext string); post proof (L1).

The Public build never uploads encrypted media; only public files or text unlockables.


7.2 Uploader (Verified build) — reference flow (encrypted media)

  1. Preflight (client-side, before any posting)

    • Fetch Allowlist/Blocklists/Revocations (§5).

    • If enforceMediaAllowlist=true, ensure uploader wallet or collection is allowed.

    • Compute sha256(plaintext), optional pHash/video keyframe pHashes; abort if any blocklist matches.

  2. Encrypt & post

    • Generate random 32-byte key + 12-byte IV; AES-256-GCM encrypt media → ciphertext.

    • Save the symmetric key to Lit with uacc (ERC-721 ownerOf / ERC-1155 balanceOf).

    • Post Data Tx with ciphertext (bundled ok).

  3. Descriptor & proof

    • Post Descriptor (L1) with:

      • storage.dataTxId, lit (uacc, chain, method), crypto (AES-GCM, ivB64), hashes.sha256, and safety_manifest.

    • Wait ≥1 confirmation.

    • Sign NFU-Proof with fileHash = keccak256(plaintext media bytes); post proof (L1).


7.3 Viewer — minimal, deterministic sequence

     
// 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);

UI states to expose (suggested):

  • Verifying policy…Checking allowlist/blocklists…Requesting key…Decrypting…Verified & rendering

  • Error badges: Revoked, Blocked (hash), Not verified, Ownership at time failed, Decrypt error.


7.4 Descriptor snippets (copy-paste)

Public file

      
{
  "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" }
}
 

Encrypted media (Verified build)

       
{
  "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" }
}
  

7.5 Arweave tags (recommended)

Data Tx (public/ciphertext):

  • App-Name: NFU-Data

  • NFU-Chain, NFU-Contract, NFU-TokenId, NFU-Standard

  • Content-Type: <mime> (public only)

  • NFU-Ext: <ext>

Descriptor (L1):

  • App-Name: NFU-Uploader

  • App-Version: <x.y.z>

  • NFU-Schema: nfu.descriptor.v1

  • NFU-Kind: public|unlockable

  • NFU-Chain, NFU-Contract, NFU-TokenId, NFU-Standard

  • NFU-Ext, NFU-CreatedAt

  • NFU-DataTxId: <txid> (if Data Tx exists)

NFU-Proof (L1):

  • App-Name: NFU-Proof

  • App-Version: <x.y.z>

  • NFU-DescriptorTxId: <descriptor_txid>

  • NFU-Chain, NFU-Contract, NFU-TokenId, NFU-Standard


7.6 EIP-712 (canonical types & message recap)

        
{
  "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 */ }
}
   

Validation: After decrypting (if applicable), recompute keccak256(plaintext) and compare to fileHash. Mismatch → BLOCK_PROOF.


7.7 MIME whitelist & size ceilings (reference)

  • Images: image/png, image/jpeg, image/webp, image/gif

  • Video: video/mp4 (H.264/AAC), video/webm

  • Audio: audio/mpeg, audio/wav, audio/ogg

  • Docs/3D: application/pdf, model/gltf-binary

  • Ceilings: implementation-defined (set conservatively; e.g., 100–350 MB). If you don’t implement chunked streaming decryption, keep limits lower.


7.8 Manifest integration (fetch & cache)

        
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";
   
  • Cache ETag locally and refresh opportunistically.

  • On manifest fetch failure for encrypted media, default to conservative: do not request keys.


7.9 Test vectors (non-normative, for sanity checks)

Use these to verify your hashing and proof wiring:

  • Empty plaintext

    • sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

    • keccak256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470

  • AES-GCM parameter sanity

    • Key length = 32 bytes (256-bit).

    • IV length = 12 bytes (96-bit).

    • Tag length = 16 bytes (128-bit).

    • Reusing IV with the same key is forbidden (treat as invalid).


7.10 Conformance checklist (summary)

A conforming Uploader:

  • Computes sha256(plaintext) (and optional pHashes).

  • Public build: posts raw file → Descriptor → NFU-Proof.

  • Verified build: allowlist + blocklist prechecks; encrypts media; saves key with Lit; posts ciphertext Data → Descriptor (+ Safety Manifest) → NFU-Proof.

A conforming Viewer:

  • Uses Descriptor’s Arweave block timestamp as the clock.

  • Checks Revocations first.

  • Enforces allowlist for encrypted media (if enabled).

  • Applies blocklists (hashes) before key requests.

  • Requests Lit key only after safety passes.

  • Verifies NFU-Proof with keccak256(plaintext).

  • Enforces ownership-at-time or allowlist override.

  • Renders by MIME; never uploads plaintext elsewhere.


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 include hashes.sha256 of 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 fileHash from 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 Manifest shape/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 $id to enable $ref resolution.

  • 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.