Skip to content

IPFS-Meshkit/Meshkit-UCAN

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MeshKit-UCAN (acul71)

What is MeshKit-UCAN?

MeshKit is a developer-friendly toolkit for decentralized storage on mobile (React Native first) with UCANs. It wraps IPFS-style content addressing behind a small TypeScript API so app authors can store, retrieve, upload, and download without running their own infra — while optionally adding AES-GCM encryption, UCAN-based sharing, and a demo messaging layer backed by a Key Service.

Layer Role
@meshkit/core Headless SDK — storage, encryption, share, send/receive
@meshkit/react-native React hooks + provider for Expo / RN apps
Key Service (py/keyservice) HTTP registry for content keys, UCAN delegations, and inbox indices (POC)
Kubo (optional) Pins ciphertext to real IPFS CIDs when pinning is configured

MeshKit is not a replacement for Helia on web; it targets mobile DX under Manu's storage-first direction. See MANU_SPEC.md for product scope.


Encrypted chat demo (sender ↔ recipient)

With EXPO_PUBLIC_MESHKIT_ENCRYPT=true, the Expo example app is a WhatsApp-style chat: one screen, message bubbles, file attachments, identity header, and a Debug Coach panel after each send.

Sender (alicebob) — message encrypted, pinned on Kubo, inbox entry on Key Service. Debug Coach shows the full message CID and terminal steps for the recipient:

Sender session: alice messaging bob with Debug Coach showing message CID

Recipient (bobalice) — inbox polled every few seconds; reply appears in-thread. Debug Coach shows the outbound message CID:

Recipient session: bob replying to alice with Debug Coach

Under the hood:

  1. Ciphertext lives on Kubo (IPFS CID).
  2. Content encryption keys and UCAN delegations live on the Key Service.
  3. Inbox is a Key Service index per user — clients poll it; there are no push notifications in this POC.

Chat UI (encrypt mode)

Feature Behavior
Default user alice (sender); override with EXPO_PUBLIC_MESHKIT_USER_ID
To: picker Lists users from Key Service GET /v1/users (excludes self)
Sender label Each bubble shows who sent it (senderId)
Inbox poll Auto every 3 s (EXPO_PUBLIC_MESHKIT_INBOX_POLL_MS; 0 = manual ↻ only)
Unknown recipient In-chat error if peer never registered
Debug Coach Full message CID + copy-paste steps for second client

User registration (no pre-seed)

The Key Service starts empty. Each client POSTs /v1/users on first app open when encrypt: true. There is no built-in alice/bob seed data.

Recipient-first rule: the user you send to must have opened the app at least once so they exist on the Key Service (GET /v1/users/{id}). That is why the demo starts bob before alice — not because alice is pre-registered.

Messages to an offline peer still work: ciphertext stays on Kubo; keys and inbox entries stay on the Key Service until the recipient opens the app and inbox polling runs.


How to run the demo

Prerequisites

Tool Version
Node.js 22+ for Expo example (nvm use in rn/example-app)
uv Python Key Service
Python 3.10 – 3.13
Kubo (ipfs) Optional but required for real CIDs in encrypt demo

One-time setup:

cd acul71/rn/example-app
chmod +x setup.sh && ./setup.sh

Configure Kubo for LAN dev (bind API + gateway to 0.0.0.0, enable CORS) — see rn/example-app/README.md.

Three-terminal flow (web, same PC)

# Terminal 0 — Key Service (separate terminal: ipfs daemon on this PC too)
cd acul71/rn/example-app && npm run start:keyservice
# in another shell on the same PC: ipfs daemon

# Terminal 1 — recipient MUST open first (registers bob; script forces encrypt)
npm run start:kubo:as -- bob

# Terminal 2 — sender alice (encrypt required here — start:kubo does NOT default it)
EXPO_PUBLIC_MESHKIT_ENCRYPT=true npm run start:kubo

start:kubo:as already sets EXPO_PUBLIC_MESHKIT_ENCRYPT=true and EXPO_PUBLIC_MESHKIT_USER_ID. Only alice’s terminal needs the env var prefix on web.

Then in the browser:

  1. Alice tab: tap To: and pick bob from registered users, then send.
  2. Bob tab: inbox loads automatically (poll every 3 s); messages show the sender name on each bubble.
  3. Bob replies: pick To: alice and send.

Android emulator variant (recipient first, same as web):

# Terminal 0 — Key Service (+ ipfs daemon on PC)
cd acul71/rn/example-app && npm run start:keyservice

# Terminal 1 — bob registers first
npm run start:kubo:android:as -- bob

# Terminal 2 — alice (default user; encrypt already on in start-kubo-android.sh)
npm run start:kubo:android

npm run start:kubo:android defaults EXPO_PUBLIC_MESHKIT_ENCRYPT=true, Kubo URLs on 10.0.2.2, and clears Metro cache (-c is built into the script). You do not need to prefix Terminal 2 with EXPO_PUBLIC_MESHKIT_ENCRYPT=true unless you want to be explicit.

Use $HOME/.buildozer/android/platform/android-sdk as ANDROID_HOME (not a random ~/Android/Sdk path). AOSP AVD Meshkit_AOSP_34 — see rn/example-app/README.md.

Hermes lacks crypto.subtle; @meshkit/core falls back to @noble/ciphers AES-GCM and requires react-native-get-random-values (imported first in the Expo app). After SDK changes: rebuild ts/, npm install in example-app, restart Expo with -c.

Default To: is never yourself: alice → bob, bob → alice.

Environment variables (encrypt + Kubo mode)

Variable Purpose
EXPO_PUBLIC_MESHKIT_ENCRYPT=true Enable share / send / receive (required on npm run start:kubo; defaulted by start:kubo:as and start:kubo:android)
EXPO_PUBLIC_MESHKIT_USER_ID Identity (alice, bob, …)
EXPO_PUBLIC_MESHKIT_PINNING Kubo API URL (e.g. http://192.168.x.x:5001)
EXPO_PUBLIC_MESHKIT_GATEWAY Gateway URL (e.g. http://192.168.x.x:8092)
EXPO_PUBLIC_MESHKIT_KEY_SERVICE Key Service URL (default http://127.0.0.1:8787)
EXPO_PUBLIC_MESHKIT_INBOX_POLL_MS Inbox poll interval (default 3000; 0 = manual only)
EXPO_PUBLIC_MESHKIT_DEBUG Show Debug Coach after send (default on in dev)

Copy rn/example-app/.env.example and restart Expo with -c after changes.

Node-only SDK demos (no UI)

cd acul71/ts
npm install && npm run build
npm run demo:storage       # memory storage
npm run demo:share         # UCAN share (in-memory Key Service)
npm run demo:messaging     # send / receive (in-memory)

demo:remote-keys needs the Python Key Service listening on :8787. Start it in another terminal first, then run the demo:

# Terminal A — Key Service (leave running)
cd acul71/rn/example-app && npm run start:keyservice
# or: cd acul71/py && uv run meshkit-keyservice --port 8787 --host 0.0.0.0

# Terminal B — remote-keys demo
cd acul71/ts && npm run demo:remote-keys

Without Terminal A you get ECONNREFUSED 127.0.0.1:8787 — that is expected, not an SDK bug.


Limitations (read before production use)

This repository is a Phase 1 POC, not production messaging or file sync.

Topic Limitation
Single shared backend Demo assumes one Key Service + one Kubo reachable by all clients. Each machine running its own 127.0.0.1 services = separate worlds.
Same LAN / dev PC Default scripts target two browser tabs or phone + PC on Wi‑Fi. Cross-city deployment needs explicit public URLs, TLS, and auth — not included.
No push / realtime Inbox is HTTP polling (3 s default). No WebSocket, FCM, or libp2p pubsub delivery.
Recipient must register first The peer you send to must have opened the app once (Key Service GET /v1/users/{id} → 404 otherwise). Key Service is not pre-seeded. Typos show an in-chat error.
Key Service in-memory Restarting the Python server clears users, keys, and inbox — restart all clients after a keyservice restart.
Inbox ≠ chat thread Inbox is keyed by signed-in user, not the To: field. Poll loads all senders; set To: only for outbound replies.
File notify hack Attachments send a demo JSON string via send() so the inbox renders a file bubble — not a typed production envelope.
UCAN POC tokens Delegations are simplified MeshKit UCANs verified locally; not full @ipld/dag-ucan production semantics.
Lit Protocol accessControl: 'lit' is a stub; real Lit integration is deferred (LIT_SPIKE.md).
Kubo RPC exposure Binding Kubo to 0.0.0.0 is for local dev only — never expose unauthenticated RPC to the internet.
Expo --tunnel Proxies Metro JS only; the phone still must reach Kubo/Key Service on your LAN IP.
Memory mode Without EXPO_PUBLIC_MESHKIT_PINNING, CIDs are in-app only — not on IPFS.

MeshKit API reference

Package: @meshkit/core (ts/). React Native: @meshkit/react-native (rn/) exposes the same surface via useMeshkit().

Initialization

import { Meshkit } from '@meshkit/core'

const mk = await Meshkit.init({
  preset: 'memory',              // or 'public-gateway'
  storage: 'memory',             // 'memory' | 'gateway' | 'helia' (Node 22+)
  gateway: 'https://ipfs.io',    // read / link base URL
  pinning: 'http://127.0.0.1:5001', // Kubo API — required for gateway writes
  encrypt: false,                // default; set true for share/messaging
  keyService: 'http://127.0.0.1:8787', // omit → in-memory Key Service (single process)
  accessControl: 'ucan',         // when encrypt: true
  identity: { userId: 'alice' }, // registers on Key Service at init
})

await mk.stop()

MeshkitInitOptions

Field Type Default Description
preset 'memory' | 'public-gateway' Shorthand; merged with explicit options
storage 'memory' | 'gateway' | 'helia' 'memory' Where bytes are stored
gateway string https://ipfs.io HTTP gateway for reads and link()
pinning string Kubo /api/v0 base; enables gateway writes
encrypt boolean false AES-GCM-256 envelope + Key Service
keyService string in-memory Shared HTTP Key Service URL for multi-user
accessControl 'ucan' | 'lit' 'ucan' if encrypt Delegation backend
identity MeshkitIdentity { userId, ethAddress?, publicKey? }
encryption 'aes-gcm-256' 'aes-gcm-256' Only supported algorithm today

MeshkitIdentity

Field Description
userId Logical user name (demo) or app id
ethAddress Optional 0x… — UCAN audience did:ethr:…
publicKey Optional base64 Ed25519 — audience did:key:…

On init with encrypt: true, the client POSTs /v1/users to register the identity.


Storage API (always available)

store(payload: unknown): Promise<{ cid: string }>

JSON-serializes payload, stores bytes, returns CID.

  • Plain: raw JSON bytes in storage provider.
  • Encrypted: AES-GCM envelope; content key registered on Key Service for owner.

retrieve(cid: string, options?: { ucan?: string }): Promise<unknown>

Fetches by CID and JSON-parses (plain) or decrypts (encrypted).

  • Owner: no proof needed when identity.userId matches key owner.
  • Delegate: pass { ucan } from share().

upload(data: Blob | Uint8Array, options?): Promise<{ cid: string, size: number }>

Binary upload with optional onProgress({ loaded, total, percent }).

  • Encrypted: wraps bytes in envelope before put; registers content key.

download(cid: string, options?: { ucan?: string }): Promise<Uint8Array>

Raw bytes; decrypts when encrypt: true.

link(cid: string): string

Returns {gateway}/ipfs/{cid}. Meaningful when storage: 'gateway' and content is pinned.

status(): Promise<MeshkitStatus>

interface MeshkitStatus {
  storage: string           // 'memory' | 'gateway' | 'helia'
  gateway: string
  pinning?: string
  encrypt: boolean
  accessControl?: 'ucan' | 'lit'
  gatewayReachable: boolean // pings pinning API or gateway HEAD
  shareableLinks: boolean   // gateway + pinning configured
}

stop(): Promise<void>

Releases storage provider resources.


Access control API (encrypt: true required)

share(cid: string, recipientId: string): Promise<{ ucan: string, link?: string }>

  1. Resolves recipient on Key Service (must exist for HTTP client).
  2. Creates UCAN delegation for meshkit/fetchKey/{cid}.
  3. Returns encoded UCAN; optional link when gateway mode.

Recipient can retrieve / download with { ucan } or, after delegation is indexed by recipient id, as inbox owner.

revoke(cid: string): Promise<void>

Owner deletes content key and delegations on Key Service.


Messaging API (encrypt: true required)

POC helper — not a standalone chat protocol. Messages are encrypted JSON blobs on IPFS plus inbox pointers on Key Service.

send(recipientId: string, text: string): Promise<{ id: string }>

  1. Builds { type: 'message', text, sender, recipient, sentAt }.
  2. storeEncrypted → message CID (id).
  3. share(cid, recipientId) — recipient can decrypt.
  4. appendInbox(recipientId, { id, sender, recipient, sentAt }).

Returns { id } where id is the message CID (shown in Debug Coach).

receive(id: string, options?: { ucan?: string }): Promise<ReceivedMessage>

Decrypts message CID; validates type === 'message'.

interface ReceivedMessage {
  id: string
  text: string
  sender: string
  recipient: string
  sentAt: string   // ISO-8601
}

listInbox(recipientId?: string, options?): Promise<InboxEntry[]>

Lists inbox refs for recipientId or current identity.userId.

interface InboxEntry {
  id: string       // message CID — pass to receive()
  sender: string
  recipient: string
  sentAt: string
}

Typical receive loop (as in the Expo app):

const inbox = await mk.listInbox()
for (const entry of inbox) {
  const msg = await mk.receive(entry.id)
  console.log(msg.sender, msg.text)
}

React Native integration

import { MeshkitProvider, useMeshkit } from '@meshkit/react-native'
import { meshkitOptions } from './config'

export default function App () {
  return (
    <MeshkitProvider options={meshkitOptions}>
      <ChatScreen />
    </MeshkitProvider>
  )
}

function ChatScreen () {
  const { send, listInbox, receive, loading, status } = useMeshkit()
  // …
}

Hooks: useMeshkit(), useStore(), useUpload(), useMessaging().


Key Service HTTP API (Python POC)

Base URL e.g. http://127.0.0.1:8787:

Method Path Purpose
GET /health Liveness
POST /v1/users Register identity
GET /v1/users List registered users (demo recipient picker)
GET /v1/users/{userId} Lookup user
POST /v1/keys Register content key for CID
GET /v1/keys/{cid} Fetch key (owner header or Bearer UCAN)
POST /v1/keys/{cid}/share Delegate to recipient
DELETE /v1/keys/{cid} Revoke
POST /v1/inbox/{recipientId} Append inbox entry
GET /v1/inbox/{recipientId} List inbox

Start: cd acul71/rn/example-app && npm run start:keyservice


Errors

Error Typical cause
EncryptionError share/send/receive without encrypt: true
Meshkit user not found: … Recipient never opened app
Key not found or revoked for CID Wrong CID or revoked share
Not authorized to fetch key Missing or invalid UCAN
CID … is not an encrypted message receive() on non-message CID

Repository layout

Path Description
ts/ @meshkit/core SDK
rn/ @meshkit/react-native + Expo example app
py/keyservice/ Python Key Service
test-vectors/ Cross-stack crypto vectors
MANU_SPEC.md Product spec
FEASIBILITY.md Milestones and analysis
run-poc.sh Run all tests and demos

Roadmap

Milestone Status
M0 Spec alignment Done
M1 Real storage Done
M2 @meshkit/core Done
M3 React Native SDK Done
M4 DX polish Done
M5 Lit Protocol Design doc (LIT_SPIKE.md)
M7 Share / UCAN / messaging Done (POC)

Further reading

About

Meshkit for py-libp2p

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors