# RFC-1 — CivicIPFS Addon Specification **Status:** Draft **Version:** 0.4 **Changes from 0.3:** Added Steward/Witness role model (section 3.1). Added internal/external relationship declaration, signed into pin requests and receipts (section 6.4). Added four-list content filtering — node and channel allow/deny lists, sourced from Hubzilla's existing `message_filter_incl`/`message_filter_excl` mechanism (section 6.5). Channel panel UI updated to surface role declaration and filter lists (section 9.2). Capacity-class conformance clarified as a directory/whitelist concern (RFC-4), not an addon-level gate — addon makes no quota decisions of its own. **Changes from 0.2:** Authentication contract in section 6.3 corrected and fully specified from source. Channel Ed25519 keypair (`channel_eprvkey`/`channel_epubkey`) confirmed as the signing key for addon requests. RSA-4096 primary key documented as separate. `CryptoContract` added to CoreBoundary. **Verified against:** Hubzilla 11.2.1 / Zot 6.0 **Author:** CivicIPFS Project **Repository:** TBD **RFC-2 (Orchestrator Specification):** TBD — separate document **RFC-4 (Directory and Conformance):** TBD — separate document, defines what makes a node CivicIPFS-compliant --- ## 1. Purpose and Scope This document specifies the CivicIPFS Hubzilla addon. It covers: - What the addon does inside Hubzilla - The CoreBoundary contracts — every point of contact with Hubzilla core, named and versioned - The Subscription API — the interface between the addon and an orchestrator service - The UI surface presented to the Hubzilla user - The addon file structure and namespace design - What is explicitly out of scope for this document This RFC does not specify the orchestrator service, the inter-orchestrator protocol, the challenge-proof mechanism, the reputation propagation model, or the conformance/directory criteria that determine whether a node is recognised as part of the CivicIPFS network. Those are specified in RFC-2, RFC-3, and RFC-4 respectively. --- ## 2. Design Principles These principles are binding on all implementation decisions. When a decision conflicts with a principle, the principle wins and the decision is reconsidered. ### 2.1 Isolation from core The addon is 100% isolated from Hubzilla core at the code level. It communicates with core exclusively through the hook system documented in `include/plugin.php`. It does not call core functions directly except through named, documented, versioned contracts in the `CivicIpfs\CoreBoundary` namespace. ### 2.2 Adaptive boundary isolation Every dependency on Hubzilla core behaviour is: - Named as a constant or documented interface in `CivicIpfs\CoreBoundary` - Attributed to a specific source file and line number in core - Versioned against the Hubzilla release it was verified against - Checked at addon load time against the running core version, with a logged warning if they differ When Hubzilla core evolves, the starting point for any compatibility audit is `CivicIpfs\CoreBoundary`. Nothing else needs to be searched. ### 2.3 Readability and maintainability over cleverness Every class has one responsibility. Every method does one thing. Every dependency is injected, not reached for globally. No magic. No dynamic dispatch that cannot be followed by reading the code linearly. A developer who has never seen this codebase should be able to understand any single file without reading any other file first. ### 2.4 The orchestrator is external The addon does not contain IPFS logic. It does not contain pin management logic. It does not contain challenge-proof logic. It does not contain reputation logic. It does not contain capacity conformance logic — the addon never decides whether a node's capacity class is acceptable; that is a directory-time concern specified in RFC-4. All of that lives in the orchestrator or the conformance layer above it. The addon's job is to be the clean, well-documented bridge between Hubzilla identity and an orchestrator subscription. ### 2.5 No private content All content addressed through CivicIPFS is public. The addon provides no encryption layer, no key distribution, no access control on content. Content-addressed storage is inherently public — a CID resolves to the same content for any requester. This is a deliberate design boundary, not a deferred feature. ### 2.6 The evidentiary purpose is primary CivicIPFS exists to make public records permanently retrievable and verifiable. Every design decision that trades verifiability for convenience is wrong. Every design decision that trades durability for speed is wrong. The use cases are public health records, policy documents, civic archives, and scientific data — content whose integrity matters more than its delivery performance. ### 2.7 The addon makes no economic or conformance judgments The addon declares facts — who is publishing, what their relationship to the content is, what they will and will not accept pinned on their behalf — and signs those declarations. It does not decide whether a price is fair, whether a node's capacity claim is honest, or whether a Steward and Witness relationship is properly separated. Those are orchestrator-side (RFC-2), network-side (RFC-3), or directory-side (RFC-4) concerns. The addon's role is honest declaration, not enforcement. --- ## 3. What the Addon Does The addon provides a Hubzilla channel with the ability to: 1. **Browse orchestrators** — view a list of available orchestrator subscriptions, with their advertised service levels, prices, pinning durations, capacity-class disclosure, and reputation scores as reported by the orchestrator 2. **Subscribe to an orchestrator** — configure the addon to use a specific orchestrator endpoint, authenticated by the channel's Zot6 identity 3. **Declare a role for a pin** — see section 3.1. Every pin request declares whether the channel is acting as the content's Steward and whether the relationship is internal or external 4. **Pin content** — submit a CID for pinning, selecting service level, price tier, duration, and role/relationship from the subscribed orchestrator's offer 5. **View pin state** — see the current status of all pinned CIDs: active, expired, challenged, confirmed, declined 6. **View receipts** — retrieve signed pin receipts issued by the orchestrator as verifiable commitments, including the recorded relationship (section 6.4) 7. **View reputation data** — see the orchestrator's current reputation score as reported by the network 8. **Configure content filters** — surface and edit the channel's allow/deny lists used to inform Witness orchestrators what this channel is willing to have pinned on its behalf (section 6.5) 9. **Unpin content** — request removal of a pin commitment The addon does not: - Run an IPFS node - Manage pin storage directly - Issue or respond to challenges - Propagate reputation scores - Handle payment — payment is between the user and the orchestrator operator, outside this system - Store or serve content - Decide whether an orchestrator is conformant — that is a directory/whitelist function (RFC-4) the addon consumes but does not perform ### 3.1 Steward and Witness — the two roles a channel can declare A Hubzilla channel using CivicIPFS acts in one of two roles with respect to any given piece of content. These roles are declared by the channel, signed into the relevant API request, and are informational facts the orchestrator and network record — the addon does not enforce role separation; that enforcement, where it exists, is a network/directory-level conformance property (RFC-4). **Steward** — the originating publisher. A Steward publishes content in clear text and stands behind its truth and authorship. A Steward may publish: - **Internally** — the operator publishes their own content and bears its own cost, with no external paying relationship - **Externally** — the operator publishes content on behalf of someone else, for profit, as a paid clear-text publishing service **Witness** — the pinning party. A Witness commits storage to preserve content it did not originate, and is not presumed to endorse the content's truth or position — it is providing infrastructure, not making an editorial judgment. A Witness may pin: - **Internally** — the operator pins their own Steward output, for free, as self-redundancy - **Externally** — the operator pins someone else's Steward content, for profit, as the core paid pinning service A single Hubzilla channel may hold either role, or both roles for different content, but a Steward and a Witness relationship for the *same* content are expected to be separate nodes — this is a conformance property checked at the directory level (RFC-4), not something the addon code verifies. The Witness's internal/external relationship, not the Steward's, is what is recorded against per-node capacity accounting at the orchestrator (see RFC-2 section 13.4 as revised). The Steward's internal/external relationship is recorded for civic-record completeness in the receipt (section 6.4) but carries no capacity implication. --- ## 4. Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Hubzilla Core │ │ (identity, permissions, hooks, config, UI shell) │ └──────────────────────────────────────────────────────────────────┘ │ hooks only │ CoreBoundary contracts ┌──────────────────▼─────────────────────────────────────────────┐ │ CivicIPFS Addon │ │ │ │ ┌──────────────────┐ ┌────────────────────┐ ┌────────────┐ │ │ │ CoreBoundary │ │Subscription │ │ UI Layer │ │ │ │ namespace │ │ API Client │ │ (widgets) │ │ │ └──────────────────┘ └─────────┬──────────┘ └────────────┘ │ └───────────────────────────────────┼──────────────────────────────┘ │ Subscription API │ (HTTP/JSON, versioned) ┌──────────────────▼─────────────────────────────────────────────┐ │ Orchestrator Service │ │ (specified in RFC-2) │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Kubo IPFS Node │ │ │ └────────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────────┘ ``` The addon operator does not run an orchestrator. They point the addon at an orchestrator run by someone else (or themselves if they choose to run both). The orchestrator subscription is independent of the Hubzilla installation. Whether that orchestrator is recognised as a conformant CivicIPFS node is a question the directory layer (RFC-4) answers, not this addon. --- ## 5. CoreBoundary Contracts These are every point of contact between the addon and Hubzilla core. No other contact exists. Each entry records the source file, the line number verified, and the Hubzilla version at time of verification. ### 5.1 Entry Points Hubzilla loads addons via `include/plugin.php`. The following bare global functions are the only global namespace entries the addon makes. Each delegates immediately to a namespaced class. ```php // addon/civicipfs/civicipfs.php function civicipfs_install() { CivicIpfs\Addon::install(); } function civicipfs_uninstall(){ CivicIpfs\Addon::uninstall(); } function civicipfs_load() { CivicIpfs\Addon::load(); } function civicipfs_unload() { CivicIpfs\Addon::unload(); } ``` **Verified:** `include/plugin.php` — `load_plugin()`, `install_plugin()`, `uninstall_plugin()`, `unload_plugin()` functions. Hubzilla 11.2.1. ### 5.2 Hook Contracts Defined in `CivicIpfs\CoreBoundary\HookContract`. ``` HOOK_ACTIVITY_RECEIVED = 'activity_received' Source: Zotlabs/Lib/Libzot.php line 1979 Data: ['item_id' => int, 'item' => array, 'sender' => array, 'channel' => array] Use: Detect when a channel receives a CID reference embedded in an activity, trigger local pin offer HOOK_CHANNEL_PROTOCOLS = 'channel_protocols' Source: Zotlabs/Lib/Libzot.php line 2929 Data: ['channel_id' => int, 'protocols' => array] Use: Advertise that a channel supports civicipfs protocol to peers during capability exchange HOOK_ZOTINFO = 'zotinfo' Source: Zotlabs/Lib/Libzot.php line 2999 Data: the zotinfo response array (mutable) Use: Inject civicipfs capability advertisement into the channel's zotinfo response HOOK_POST_LOCAL = 'post_local' Source: Zotlabs/Lib/Libzot.php line 1933 Data: item array (mutable) Use: Detect local posts containing CID references, offer to pin referenced content automatically HOOK_IDENTITY_EXPORT = 'identity_basic_export' Source: include/channel.php line 1164 Data: ['channel_id' => int, 'sections' => array, 'data' => array (mutable)] Use: Pack channel JSON store into export bundle by writing into $addon['data']['civicipfs'] HOOK_IMPORT_CHANNEL = 'import_channel' Source: Zotlabs/Module/Import.php line 523 Data: ['channel' => array, 'data' => array] Use: Unpack civicipfs JSON store from import bundle and write JSON files for the new channel instance ``` **Note:** There is no hook in `Libzot::import()` that fires for unknown Zot6 packet types. Unknown types are silently discarded after the type check at lines 1243 and 1323. Inter-orchestrator coordination therefore does not use Zot6 packet delivery — it uses the orchestrator-to-orchestrator protocol specified in RFC-2. This is a verified architectural boundary, not a deferred decision. ### 5.3 Config Contracts Defined in `CivicIpfs\CoreBoundary\ConfigContract`. ``` REALM_FAMILY = 'system' REALM_KEY = 'directory_realm' REALM_DEFAULT = 'RED_GLOBAL' Source: boot.php line 106 (DIRECTORY_REALM constant) boot.php lines 2441-2447 (get_directory_realm()) Use: Read at load time to verify realm membership. Addon emits a logged warning if value is 'RED_GLOBAL' and federation mode is requested. REALM_CIVIC = 'CIVICUS_IPFS' The canonical realm name for the CivicIPFS network. An operator sets system.directory_realm to this value to join the civic network. This constant is the single authoritative definition of the realm name in the addon codebase. It must not appear as a literal string anywhere else. REALM_TOKEN_FAMILY = 'system' REALM_TOKEN_KEY = 'realm_token' Source: Zotlabs/Lib/Libzotdir.php (sync_directories) Use: Read when verifying civic realm membership. Not set or distributed by this addon — set by the hub administrator out of band. ADDON_FAMILY = 'civicipfs' ADDON_ORCHESTRATOR = 'orchestrator_url' ADDON_MODE = 'mode' // 'local' | 'federated' ADDON_VERSION = 'contract_version' Use: Addon-specific configuration. Written by civicipfs admin panel, read at runtime. ADDON_FILTER_INCL_KEY = 'message_filter_incl' ADDON_FILTER_EXCL_KEY = 'message_filter_excl' ADDON_FILTER_FAMILY = 'system' Source: Zotlabs/Module/Settings/Channel.php lines 39-40, 137-138 Use: Read per-channel (pconfig, uid-scoped) and read site-wide (config, the equivalent Admin/Site.php fields) to build the four-list filter set described in section 6.5. The addon does not write these values — they are owned by Hubzilla's existing channel and site settings UI. The addon only reads them and forwards parsed rules to the orchestrator. ``` ### 5.4 Version Gate Defined in `CivicIpfs\CoreBoundary\VersionGate`. At `civicipfs_load()`, the addon runs two checks. **Core version check.** It reads `STD_VERSION` from the running Hubzilla instance and compares it against `VERIFIED_CORE_VERSION = '11.2.1'`. If they differ: ``` [civicipfs] Core version mismatch. Verified against: 11.2.1 Running: {actual version} Review CivicIpfs\CoreBoundary contracts before assuming compatibility. ``` **Realm check.** It reads `get_directory_realm()` and compares it against `ConfigContract::REALM_CIVIC` (`'CIVICUS_IPFS'`). If the realm is `RED_GLOBAL` and federation mode is enabled: ``` [civicipfs] Realm mismatch. Federation requires realm: CIVICUS_IPFS Current realm: RED_GLOBAL Federation features are disabled until realm is changed. Local pinning remains active. ``` Both are warnings, not errors. The addon continues to load. Local pinning functions regardless of realm. Federation features are silently gated until the realm matches `ConfigContract::REALM_CIVIC`. The realm name `CIVICUS_IPFS` never appears as a literal string anywhere in the addon except in `ConfigContract`. **Verified:** `boot.php` — `STD_VERSION` constant. Hubzilla 11.2.1. ### 5.5 CryptoContract Defined in `CivicIpfs\CoreBoundary\CryptoContract`. Documents every cryptographic fact the addon depends on about Hubzilla's key and signing infrastructure. ``` CHANNEL_KEY_TYPE_PRIMARY = 'RSA-4096' Source: include/channel.php line 241 include/crypto.php new_keypair() Note: Used for Zot6 protocol signing via Libzot::sign(). Format: PEM. Signature: 'sha256.' . base64url($sig). NOT used for addon Subscription API requests. CHANNEL_KEY_TYPE_SECONDARY = 'Ed25519' Source: include/channel.php lines 243-245 Note: Generated via sodium_crypto_sign_keypair(). DB fields: channel_eprvkey (secret key, base64 no-padding), channel_epubkey (public key, base64 no-padding). Zotinfo field: ed25519_key (Multibase-encoded xchan_epubkey). Used for addon Subscription API request signing. SIGN_FUNCTION = 'sodium_crypto_sign_detached' Source: PHP sodium extension (core since PHP 7.2) Input: string $message, string $secretkey (raw bytes, 64 bytes) Output: raw signature bytes (64 bytes), base64_encode for header VERIFY_FUNCTION = 'golang.org/x/crypto/ed25519.Verify' Used by: orchestrator to verify incoming addon requests Input: publicKey (32 bytes), message, signature (64 bytes) ZOTINFO_PUBKEY_FIELD = 'ed25519_key' Source: Zotlabs/Lib/Libzot.php line 2906 Format: Multibase-encoded Ed25519 public key Endpoint: GET {hub_url}/.well-known/zot-info?channel_address={addr} SIGNING_ALGORITHM_DECLARED = 'rsa-sha256' Source: Zotlabs/Lib/Libzot.php line 2907 Note: This is the declared algorithm for Zot6 protocol signing. The addon uses Ed25519 (secondary key) for its own requests, which is separate from the Zot6 protocol signing algorithm. ``` --- ## 6. Subscription API The Subscription API is the interface between the addon and the orchestrator. It is the only channel through which the addon communicates with the outside world. ### 6.1 Principles - HTTP/JSON over HTTPS - All requests carry a Zot6 channel signature in the `X-CivicIPFS-Sig` header, allowing the orchestrator to verify the requesting identity without any separate authentication mechanism - The API is versioned — the URL path carries the version: `/api/v1/` - The addon sends the API version it expects in every request header: `X-CivicIPFS-Version: 1` - If the orchestrator returns a version mismatch error, the addon surfaces this to the user and logs it — it does not silently degrade ### 6.2 Endpoints (addon-facing) These are the calls the addon makes to the orchestrator. The orchestrator specification in RFC-2 defines the implementation side. ``` GET /api/v1/info Returns orchestrator identity, version, service offers, reputation summary, current capacity-use figures (informational, not a gate — see RFC-2 section 13.4 as revised), and capacity-class disclosure (see section 6.6). GET /api/v1/offers Returns the full list of service tiers with prices, pinning durations, and SLA commitments. POST /api/v1/pin Body: { cid, service_tier, duration_days, relationship, content_tags } Returns: signed pin receipt GET /api/v1/pin/{cid} Returns current pin state for a CID: { cid, status, pinned_at, expires_at, receipt } status includes: active | expired | challenged | confirmed | failed | declined DELETE /api/v1/pin/{cid} Request unpin. Returns acknowledgement. GET /api/v1/pins Returns all pins for the authenticated channel. GET /api/v1/reputation Returns the orchestrator's current reputation score and spot-check history as reported by the network. POST /api/v1/register Body includes the four filter lists (section 6.5) in addition to the registration fields specified in RFC-2 section 8.2. ``` ### 6.3 Authentication Every request from the addon carries: ``` X-CivicIPFS-Channel: {channel_hash} X-CivicIPFS-Sig: {Ed25519 signature of canonical request body} X-CivicIPFS-Version: 1 ``` **Key type — verified from source:** Hubzilla channels have two keypairs, both created at channel creation (`include/channel.php` lines 241–245): 1. **RSA-4096** (`channel_prvkey` / `channel_pubkey`) — the primary Zot6 identity key. Used by `Libzot::sign()` via `openssl_sign()` with SHA-256. Signature format: `'sha256.' . base64url_encode($sig)`. Advertised as `signing_algorithm: 'rsa-sha256'` in Zotinfo. 2. **Ed25519** (`channel_eprvkey` / `channel_epubkey`) — generated via `sodium_crypto_sign_keypair()`. Stored as base64, no padding, standard variant. Advertised in Zotinfo as `ed25519_key` (Multibase-encoded via `xchan_epubkey`). The addon signs requests using the channel's **Ed25519 private key**: ```php // CivicIpfs\Subscription\Request::sign() $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $prvkey = base64_decode($channel['channel_eprvkey']); $sig = sodium_crypto_sign_detached($body, $prvkey); $headers['X-CivicIPFS-Sig'] = base64_encode($sig); ``` Ed25519 is chosen over RSA because: - The orchestrator's own identity key is Ed25519 — one verification path for all signatures the orchestrator checks - `sodium_crypto_sign_detached()` is available in PHP 7.2+ as a core function — no library dependency - Signatures are 64 bytes, compact in HTTP headers The orchestrator retrieves the channel's `ed25519_key` from the Hubzilla Zotinfo endpoint (`/.well-known/zot-info`) at registration and caches it. Verification uses `golang.org/x/crypto/ed25519`. A channel that migrates to a new Hubzilla instance retains its orchestrator relationship automatically — `channel_hash`, `channel_eprvkey`, and `channel_epubkey` are all preserved through Hubzilla's nomadic migration (verified: they travel in the channel export via the `identity_basic_export` hook). ### 6.4 The relationship field — internal vs. external Every `POST /api/v1/pin` request carries a `relationship` field, declared by the requesting channel: ```json { "cid": "bafybeig...", "service_tier": "standard", "duration_days": 365, "relationship": "internal", "content_tags": ["public-health", "policy-document"] } ``` `relationship` is one of `internal` or `external`. It describes the economic relationship the *requesting* channel — Steward or Witness, depending on which endpoint role applies — has to the content being pinned, per the definitions in section 3.1. This field is: - **Self-declared.** The orchestrator does not verify the truthfulness of the claim. Truthfulness is a Steward/Witness accountability matter, the same accountability bucket as the requirement that Stewards publish in clear text (section 2.5). - **Signed.** It is part of the canonically-ordered JSON body that the Ed25519 signature in `X-CivicIPFS-Sig` covers. A relationship claim cannot be altered after the fact without invalidating the signature. - **Recorded in the receipt, not used for capacity accounting at the addon level.** The orchestrator records the Witness-side relationship against its own per-node capacity-class disclosure (RFC-2 section 13.4 as revised; RFC-4). The addon makes no capacity decisions — it only declares and signs the fact. A CID may accumulate multiple, independent pin receipts from multiple Stewards and multiple Witnesses, each with its own `relationship` value — this is consistent with IPFS content addressing (one CID, many independent custodial references) and is not treated as a conflict or duplication anywhere in the addon. ### 6.5 Content filter lists — four lists, sourced from existing Hubzilla mechanisms CivicIPFS does not invent a content taxonomy. It reuses Hubzilla's existing message filter mechanism (`Zotlabs/Module/Settings/Channel.php` lines 39-40, 137-138, and the equivalent site-wide fields in `Admin/Site.php`), which already supports `#tag`, `$category`, `/regex/`, `lang=xx`, and `lang!=xx` syntax, stored per-channel in `pconfig` (`cat='system'`, `k='message_filter_incl'`/`'message_filter_excl'`) and site-wide in `config`. The addon reads four lists and forwards them to the orchestrator at registration and at pin time: 1. **Node-level allow list** — the hub administrator's site-wide `message_filter_incl` 2. **Node-level deny list** — the hub administrator's site-wide `message_filter_excl` 3. **Channel-level allow list** — the channel owner's `message_filter_incl` 4. **Channel-level deny list** — the channel owner's `message_filter_excl` These four lists are not new addon-specific settings — they are the same fields the channel owner and hub administrator already configure in `Settings → Channel` and `Admin → Site` for unrelated reasons (filtering imported posts). CivicIPFS reuses them for a second purpose: informing a Witness operator's accept/decline policy. The addon does not interpret or evaluate these lists itself — it reads them and includes them, verbatim and parsed into structured rule arrays, in the request body. Evaluation against a specific pin request is an orchestrator-side or directory-side (RFC-4) concern. ```json { "channel_hash": "...", "hub_url": "https://hubzilla.example.org", "hub_realm": "CIVICUS_IPFS", "filters": { "node_allow": ["#civic-record", "$public-health"], "node_deny": ["#activism", "/political.*campaign/"], "channel_allow": [], "channel_deny": ["lang!=en"] } } ``` ### 6.6 Capacity-class disclosure The addon does not validate, cap, or gate based on an orchestrator's storage capacity. Whether a node is recognised as a conformant CivicIPFS Witness — including the capacity-class discipline discussed in RFC-4 — is determined out-of-band by the directory/whitelist process, not by the addon at request time. The addon does, however, surface whatever capacity-class disclosure the orchestrator publishes (via `GET /api/v1/info`, see RFC-2) to the channel owner in the orchestrator browser UI (section 9.3), so the channel owner can make an informed choice between offers — for example, a residential/home-lab-class Witness versus a datacenter-class Witness — without the addon making that judgment on the owner's behalf. --- ## 7. File Structure ``` addon/civicipfs/ ├── civicipfs.php ← entry point, 4 bare functions only ├── Addon.php ← CivicIpfs\Addon │ install/uninstall/load/unload logic ├── CoreBoundary/ │ ├── HookContract.php ← CivicIpfs\CoreBoundary\HookContract │ ├── ConfigContract.php ← CivicIpfs\CoreBoundary\ConfigContract │ └── VersionGate.php ← CivicIpfs\CoreBoundary\VersionGate ├── Subscription/ │ ├── Client.php ← CivicIpfs\Subscription\Client │ │ HTTP client for orchestrator API │ ├── Request.php ← CivicIpfs\Subscription\Request │ │ builds and signs API requests, │ │ including relationship and filters │ ├── Response.php ← CivicIpfs\Subscription\Response │ │ parses and validates responses │ └── OrchestratorOffer.php ← CivicIpfs\Subscription\OrchestratorOffer │ value object for a service offer, │ including capacity-class disclosure ├── Pin/ │ ├── PinRequest.php ← CivicIpfs\Pin\PinRequest │ │ includes relationship, content_tags │ ├── PinReceipt.php ← CivicIpfs\Pin\PinReceipt │ │ includes relationship field │ └── PinState.php ← CivicIpfs\Pin\PinState ├── Filter/ │ └── FilterListReader.php ← CivicIpfs\Filter\FilterListReader │ reads the four pconfig/config lists │ described in section 6.5 ├── Hook/ │ ├── ActivityReceived.php ← CivicIpfs\Hook\ActivityReceived │ ├── ChannelProtocols.php ← CivicIpfs\Hook\ChannelProtocols │ ├── ZotInfo.php ← CivicIpfs\Hook\ZotInfo │ ├── PostLocal.php ← CivicIpfs\Hook\PostLocal │ ├── IdentityExport.php ← CivicIpfs\Hook\IdentityExport │ │ packs JSON store into export bundle │ └── ImportChannel.php ← CivicIpfs\Hook\ImportChannel │ unpacks JSON store from import bundle ├── Store/ │ ├── ChannelStore.php ← CivicIpfs\Store\ChannelStore │ │ read/write per-channel JSON files │ └── Schema.php ← CivicIpfs\Store\Schema │ JSON schema version constants │ and migration between schema versions ├── Ui/ │ ├── AdminPanel.php ← CivicIpfs\Ui\AdminPanel │ ├── ChannelPanel.php ← CivicIpfs\Ui\ChannelPanel │ │ includes role declaration and │ │ filter list display (section 9.2) │ └── OrchestratorBrowser.php ← CivicIpfs\Ui\OrchestratorBrowser └── views/ ├── admin.tpl ├── channel.tpl └── browser.tpl ``` --- ## 8. Local State Store The addon does not create tables in Hubzilla's MariaDB database for its own JSON pin/subscription state — see section 6.5 for the one place the addon *reads* (but never writes) existing MariaDB-resident `pconfig`/`config` fields. All addon-originated state is stored in per-channel JSON files in a dedicated directory. ``` data/civicipfs/ {channel_hash}.subscriptions.json {channel_hash}.pins.json {channel_hash}.version_log.json ``` ### 8.1 Subscriptions file — `{channel_hash}.subscriptions.json` ```json { "schema": 1, "channel_hash": "...", "subscriptions": [ { "orchestrator_url": "https://...", "subscribed_at": 1700000000, "last_seen_at": 1700000000, "reputation_score": 0.97, "active": true } ] } ``` ### 8.2 Pins file — `{channel_hash}.pins.json` ```json { "schema": 1, "channel_hash": "...", "pins": [ { "cid": "bafybeig...", "orchestrator_url": "https://...", "service_tier": "standard", "relationship": "internal", "content_tags": ["public-health"], "pinned_at": 1700000000, "expires_at": 1731536000, "status": "active", "receipt": { } } ] } ``` Status values: `active` | `expired` | `challenged` | `confirmed` | `failed` | `declined` The `declined` status records a pin request the orchestrator chose not to accept, per its own filter/policy evaluation against the lists in section 6.5 — this is surfaced to the channel owner rather than silently dropped. The `receipt` field stores the signed receipt JSON received from the orchestrator verbatim, including the `relationship` field (section 6.4). ### 8.3 Authoritative record The orchestrator holds the authoritative record. The local JSON store is a cache and display layer. If lost during nomadic migration, the addon rebuilds it from the orchestrator using the channel's Zot6 identity. ### 8.4 Nomadic migration The addon registers against two hooks to include its JSON state in the channel export/import bundle. Both are verified from source. **Export hook — verified: `include/channel.php` line 1164** ``` call_hooks('identity_basic_export', $addon) Data (by reference): 'channel_id' => int 'sections' => array — section names being exported 'data' => array — mutable, addon writes into $addon['data']['civicipfs'] ``` The addon's `CivicIpfs\Hook\IdentityExport` handler adds a `civicipfs` key to `$addon['data']` containing the serialised contents of both JSON files for the channel being exported. **Import hook — verified: `Zotlabs/Module/Import.php` line 523** ``` call_hooks('import_channel', $addon) Data (by reference): 'channel' => array — the channel record just imported 'data' => array — the full import data including $data['civicipfs'] ``` The addon's `CivicIpfs\Hook\ImportChannel` handler reads `$addon['data']['civicipfs']` and writes the JSON files to the new instance's data directory under the channel's hash. These two hooks are symmetrical and purpose-built for exactly this use case. No core modification is required. The nomadic migration gap is fully closed. --- ## 9. UI Surface ### 9.1 Admin panel Accessible to hub administrators. Configures: - Default orchestrator URL for this hub (users may override per-channel) - Federation mode: `local` (addon active, no civic realm features) or `federated` (requires realm `CIVICUS_IPFS` — gated by VersionGate realm check) - Version gate warning display The admin panel does not configure the node-level filter lists (section 6.5) — those remain owned by Hubzilla's existing `Admin → Site` settings page. The addon admin panel may link to that page for convenience but does not duplicate the fields. ### 9.2 Channel panel Accessible to the channel owner. Shows: - Current orchestrator subscription and its reputation score - List of active pins with status, expiry, relationship, content tags, and receipt availability - Controls to pin new content (CID input, tier/price/duration selection, role declaration per section 3.1, relationship selection per section 6.4, content tag entry) - Controls to unpin - A read-only display of the channel's own four filter lists (section 6.5), with a link to `Settings → Channel` where the channel owner already edits the `message_filter_incl`/`excl` fields that populate them The channel panel does not provide its own filter-editing UI — it reflects the existing Hubzilla settings, consistent with the principle that CivicIPFS reuses rather than duplicates Hubzilla mechanisms (section 6.5). ### 9.3 Orchestrator browser Lists orchestrators that have been manually configured by the hub administrator or the channel owner. There is no automatic discovery mechanism and there will never be one. Automatic discovery would require a registry under someone's control, reintroducing a central point of failure and a governance dependency that the entire architecture is designed to avoid. The relationship between a Hubzilla addon user and an orchestrator operator is a deliberate, human choice — equivalent to the choice between a static IP address and DHCP. The addon provides the configuration field. The operator fills it in. That is the complete discovery mechanism, by permanent design. For each listed orchestrator, the browser surfaces the capacity-class disclosure and offer details published at `GET /api/v1/info` and `GET /api/v1/offers` (RFC-2), including whether the node identifies itself as residential/home-lab class or datacenter class, so the channel owner can make an informed choice consistent with section 6.6. The browser does not filter, rank, or recommend orchestrators based on this disclosure — it presents it for the human to judge. The admin panel allows the hub administrator to pre-configure a default orchestrator URL for all channels on the hub. Individual channel owners may override this with their own orchestrator subscription. --- ## 10. Deferred Items The following are explicitly deferred. They are recorded here so they are not forgotten and so their absence from this RFC is a documented decision, not an oversight. **D1 — Private content.** No encryption layer, no access control on content. All content addressed through CivicIPFS is public. This is a permanent design boundary for version 1, not a deferred feature. **D2 — Directory bootstrapping.** How the civic realm directory is operated. Deferred until the network has enough nodes to warrant it. Orchestrator discovery is manual by permanent design and is not part of this deferral. Conformance criteria for what makes a node listable are specified in RFC-4; the operational mechanics of running that directory remain deferred. **D3 — Attestation and timestamping.** Anchoring CIDs to an external manipulation-resistant clock via Duniter or alternatives. Deferred — this requires a separate architectural decision and will be specified in a dedicated RFC. **D5 — Payment integration.** Payment is between the user and the orchestrator operator, outside this system entirely. Not deferred — permanently out of scope. **D6 — Steward/Witness separation enforcement.** The addon declares roles (section 3.1) but does not enforce that a Steward and a Witness for the same content are operated by separate nodes. Enforcement, if any, is a directory/conformance concern specified in RFC-4. **D7 — Filter list evaluation.** The addon forwards the four filter lists (section 6.5) but does not evaluate a pin request against them. Evaluation logic belongs to the orchestrator (RFC-2) and, for conformance purposes, to the directory layer (RFC-4). --- ## 11. Open Questions All architectural questions are resolved. The following note remains for completeness. **Q2 — Nomadic identity and pin state. RESOLVED.** See section 8.4. Both the export hook (`identity_basic_export`, `include/channel.php` line 1164) and the import hook (`import_channel`, `Zotlabs/Module/Import.php` line 523) are verified from source. Migration is fully handled within the addon. --- ## 12. What RFC-2 Specifies For completeness, the following are out of scope for RFC-1 and are specified in RFC-2: - The orchestrator service architecture - The Kubo IPFS node integration - The inter-orchestrator protocol - The `ipfs_pin`, `ipfs_announce`, `ipfs_query`, `ipfs_challenge`, `ipfs_proof`, `ipfs_reputation` message types and their transport - The challenge-proof mechanism - The reputation propagation model - The subscription management API (orchestrator side) - The service offer advertisement mechanism, including capacity-class disclosure structure - Quota and capacity handling at the orchestrator (informational, not a runtime gate — see RFC-2 section 13.4 as revised) ## 13. What RFC-4 Specifies For completeness, the following are out of scope for RFC-1 and are specified in RFC-4: - The domain whitelist — which Hubzilla hubs an orchestrator accepts registrations from, beyond realm matching - Capacity-class conformance — what makes a node's storage commitment "CivicIPFS-class" (e.g. the 64GB discipline), checked at listing/directory time rather than enforced by addon or orchestrator request-time logic - Steward/Witness separation as a conformance criterion - Capacity and reliability disclosure requirements for listing - The directory/registry mechanism itself, to the extent one exists, and its governance --- *End of RFC-1 draft 0.4*