Skip to content
LogoLogo

Generated — do not edit. Source of truth: crates/terp-rs/tools/hash-market/docs/content-distribution.md. Edit there, then rebuild docs.

Content distribution (BUD + IPFS dual-index)

Status: Phase A content plane (opt-in). Default off — mint / VE support unchanged.

Invariants (frozen)

  1. Primary key: BUD sha256 of raw bytes — lowercase 64-char hex everywhere public.
  2. Secondary: IPFS CID (optional pin via Kubo).
  3. Announce: content.available (sha256, ipfs_cid, urls.bud / urls.ipfs / urls.s3).
  4. Default off: [distribution] enabled = false keeps mint/VE hosts simple.
  5. Single store: all blob bytes through TreeStore (/f/sha256) — sole BlobStore.
  6. Private notes ≠ public content plane: encrypted claim/bridge note envelopes live only under HeadstashStore (notes/hs_id/addr.json, auth + PIR). Do not put private note cleartext (or owner-addressable private bodies) on GET /content/*. Calendar off-chain cid/content/* is the template for public off-chain data only. See docs/headstash.md.

cid encoding (dao-calendar / Nostr off-chain)

PreferFormResolve
Yes (default)bare sha256 hexGET /content/sha256
Optionalbud:sha256 / sha256:sha256strip → same
Secondaryipfs:cid or ipfs://ciddual-index or gateway

Do not put bare IPFS CIDs in new MetadataExt.cid without ipfs: — bare 64-hex is reserved for BUD.

Helpers: hash_market::content::parse_cid / encode_for_chain / content_path / bind_offchain_event / resolve_local / resolve_http (calendar Phase B).

# Store off-chain body (authenticated BUD)
POST /blobs  → { "sha256": "<64 hex>", "url": "/blobs/..." }
 
# Chain holds MetadataExt.cid = "<64 hex>"
 
# Resolve later
GET /content/sha256       → bytes (local) or IPFS
GET /content/sha256/meta  → { sha256, ipfs_cid, urls }
GET /content/by-ipfs/cid  → via dual-index

Calendar off-chain bind (Rust)

use hash_market::{bind_offchain_event, resolve_local, KIND_TIME_BASED};
 
let (bind, meta) = bind_offchain_event(&store, body, KIND_TIME_BASED)?;
// meta.cid → MetadataExt.cid on create_event
let body = resolve_local(&store, &meta.cid)?;
// egress: hash_market::metadata_to_nip52_event(...) → relay publish

Config

[distribution]
enabled = true
ipfs_api = "http://127.0.0.1:5001"
ipfs_gateway_public = "https://ipfs.example/ipfs/"
pin_on_upload = true
pin_required = false
webhooks = ["http://collector:8080/hooks/content"]
webhook_bearer = "optional-shared-secret"
 
auth_mode = "both"                           # bearer | jwt-plane | both
ingest_bearer = "optional-shared-secret"
# jwt_secret via OLINE_SERVICE_JWT_SECRET preferred
jwt_audience = "hash-market-distribution"
jwt_require_role = "content.ingest"
 
default_labels = ["public"]
public_get = true
redirect_ipfs_on_miss = false

Plane JWT (oline)

export OLINE_SERVICE_JWT_SECRET='…'
oline auth jwt service-issue \
  --sub webhook-collector \
  --aud hash-market-distribution \
  --content-ingest

HTTP API

MethodPathAuthPurpose
GET/content/sha256public if public_getResolve: local BUD → IPFS
GET/content/by-ipfs/cidpublic if public_getcid → sha256 → resolve
GET/content/sha256/metapublicDual-index + urls
GET/contentpublicList registry
POST/content/registeringest authManual dual-index
POST/webhooks/s3ingest authMinIO / content.available
POST/blobsblossom authBUD upload; pin+announce if distribution on

Event shape (content.available)

{
  "type": "content.available",
  "sha256": "hex",
  "ipfs_cid": "bafy…",
  "size": 1234,
  "content_type": "application/octet-stream",
  "origin": "bud",
  "bucket": null,
  "key": null,
  "labels": ["public"],
  "urls": {
    "bud": "http://127.0.0.1:9090/blobs/sha256",
    "ipfs": "https://ipfs.example/ipfs/bafy…",
    "s3": null
  }
}

Oline / MinIO

  1. webhook-collector pins S3 → IPFS and POSTs content.available when HM_DISTRIBUTION_URL set.
  2. MinIO → collector may use static notify token (or Headscale-only network).
  3. Collector → hash-market: prefer HM_AUTH_MODE=jwt-plane.

MinioIpfsClient durable root

MinioIpfsClient::new().with_durable(MinioIpfsDurableConfig {
    local_dir: Some("/data".into()),
    distribution_url: Some("http://127.0.0.1:9090".into()),
    distribution_auth: Some("Bearer …".into()),
});

On root confirm: pin optional CID, write JSON under local_dir/hashmerchant/roots/…, register sha256 on content plane.

Smoke (L0)

cd crates/terp-rs
cargo test -p hash-market --lib content:: --features server
# dual_index_upload_meta_get_smoke + cid + auth

Non-disruption

SurfaceDefault
VE / mint treesUnchanged when distribution off
BUD /blobs/* authUnchanged
Public GETOnly /content/* when enabled

Layout

data_dir/content-registry/by-sha256/sha256.json
data_dir/trees/f/sha256

Phase B gate

Calendar Nostr e2e only after: dual-index smoke green, cid convention documented (this file), no second blob store.