Disclaimer: This is alpha software intended for testing in controlled environments. It is expected to be unstable.
Ciprnode zero (Cn0) is a first and referencial ciprnode implementation, the fundamental building block of the Cosmic Index of Public Resources. This is a proof-of-concept designed to test the viability of the Cipr, a decentralized and distributed web index that some people would appreciate.
This software enables domain owners to be indexed in the Cipr by publicly hosting a copy of the directory, thereby being indexed on it.
A deployed ciprnode also offers a search-engine-like front-end ―its ciprface― with two-level search: first level is in the index itself and second level is deep inside the content of any cipred resource.
A local SQLite database (data/ciprdup.db) that stores the node's copy of the distributed index. The database is optimized for both high-volume reads and full-text search.
- An external-content FTS5 virtual table (
ciprdup_fts) is kept in sync with the mainciprduptable via automatic SQLite triggers (on insert, update, and delete). The virtual table indexes six fields:za,title,description,keywords,offering, andseeking. - BM25 ranking is the basis for all text search relevance scores.
- Configurable BM25 weights per field are applied according to the active search mode:
- Default:
za=32, title=16, description=8, keywords=1, offering=1, seeking=1 - Offering-prioritized:
za=32, title=16, description=8, keywords=1, offering=32, seeking=1 - Seeking-prioritized:
za=32, title=16, description=8, keywords=1, offering=1, seeking=32
- Default:
- A
languagesreference table is seeded on startup from a bundled JSON dataset of over 180 ISO 639-1 language codes and their names in both English and the language's own script. - A custom SQLite scalar function
is_within_radius(lat, lon, centerLat, centerLon, minKm, maxKm)is registered at runtime to enable geographic proximity filtering using the Haversine formula.
Each entry (za) stores: title, description, keywords, offering, seeking, ol (offensiveness level 0–3), latitude, longitude (WGS84 × 10,000,000 as integers), timestamp (Unix seconds), and primary_lang (ISO 639-1 code).
Raw query strings from users or peers are sanitized in repo.js before being passed to SQLite FTS5, in the following order:
- Balance unterminated double-quote pairs (odd count → append closing
"). - Strip invalid
*placements (prefix wildcards, mid-word wildcards; only trailingword*is valid). - Strip invalid
^placements (only leading^wordis valid). - Remove leading binary operators (
AND,OR). - Remove trailing binary operators (
AND,OR,NOT). - Balance parentheses (remove unmatched
)on the first pass, then unmatched(on the second). - Sanitize malformed
NEAR()expressions (empty, unterminated, single-term), falling back to the first word token. - Strip malformed column filters (bare
:, empty{}:, filter with no following term). - Return
nullif nothing meaningful remains (query is skipped entirely).
A strict Semantic RESTful implementation with full HATEOAS via HAL+JSON (application/hal+json). All responses support content negotiation against application/hal+json, application/json, text/plain, text/html, and application/xhtml+xml.
| Method | Path | Status codes | Description |
|---|---|---|---|
HEAD |
/ |
200 OK |
Verifies node presence. Returns X-Cipr-Count header with total entry count. |
GET |
/ |
200 OK |
Lists all entries (paginated). Returns HAL+JSON or HTML depending on Accept. |
PUT |
/{za}/ |
202 Accepted, 400, 403, 409, 413 |
Upserts a cipred resource after passing the full Insertion Validation Sequence. |
DELETE |
/{za}/ |
202 Accepted |
Requests deletion, accepted or ignored depending on validation result. |
QUERY |
/ |
200 OK |
Full-text search with filters and pagination over the ciprdup. |
GET |
/{za}/ |
200 OK, 404 |
Retrieves all fields for a specific entry in HAL+JSON or HTML. |
GET |
/{za}/{field}/ |
200 OK, 404 |
Retrieves a single field. Supports text/plain and hal+json. |
HEAD |
/ri/ |
200 OK, 204 No Content |
Checks if a resindex (ISE) is configured (200) or absent (204). |
QUERY |
/ri/ |
200 OK |
Queries all configured ISE providers; aggregates and returns results. |
GET |
/languages/ |
200 OK |
Returns a JSON array of language matches for autocomplete. Same-origin only (Sec-Fetch-Site guard). |
OPTIONS |
/ri/ |
200 OK |
Returns CORS headers for cross-origin ISE availability checks. |
- All HAL+JSON responses include
_linkswith at minimumselfand eithercollectionorup. GET /andQUERY /responses include full pagination links:first,prev,next,last(absent when not applicable).GET /{za}/{field}/responses include links to all sibling fields plus anuplink to the parent entry.- An ALPS profile is served at
/profiles/cipr.jsonand referenced via aLink: <...>; rel="profile"response header.
Incoming PUT /{za}/ requests go through the following validation chain. Any failure short-circuits with the appropriate HTTP error:
- Body size limit: 8 KB strict limit per payload (
400/413on violation). - Consistency check:
zain URL path must matchbody.za(400on mismatch). - Self-update protection: PUTs for the node's own
zaare silently accepted without any change to local data (202). - Currentness Validation:
body.timestampmust be within the last 24 hours and not more than 5 minutes in the future (400on failure). This prevents stale or replayed entries and limits the window for timestamp-spoofing attacks. - Field length limits: Defense-in-depth validation against hash-complexity DoS:
za ≤ 255,title ≤ 64,description ≤ 256,keywords ≤ 512,offering ≤ 128,seeking ≤ 128,primary_lang ≤ 2(413on violation). - DNS TXT Verification (Triple Validation): The
ciprHashcomputed from the PUT body must match the_cipr.{za}TXT record, verified against 3 randomly selected DoH resolvers from the configured pool (403on failure). - HTTP Reachability Verification: A
HEAD https://ciprnode.{za}/request must return200 OK, with 6 retries at 3-second intervals. 5xx responses are treated as transient and retried; 4xx (except 429) are treated as permanent failures (403on failure). - Reliability Validation: A random FTS expression is generated, run locally as a baseline QUERY, then sent as a
QUERYtohttps://ciprnode.{za}/. The results are compared with Jaccard set similarity using an auto-scaled threshold (30% for <=5 results, 45% for <=20, 60% for larger sets). Network errors during this step are non-fatal and fail open. - Insert/Update: Entry is upserted into the ciprdup.
Incoming DELETE /{za}/ requests do not unconditionally delete. Instead:
- If the entry is not found locally, the DELETE is silently accepted (
202). - If
zaequals the node's ownza, the self-deletion is ignored (202). - The node re-validates the entry using the full Deletion Validation Sequence (Ownership + Availability + Reliability):
- If the node passes all three checks, the DELETE is rejected: the entry is protected and retained.
- If the node fails DNS/HTTP validation, the DELETE is accepted locally.
- If the node passes DNS/HTTP but fails Reliability Validation (content mismatch), the DELETE is accepted locally.
- If the Reliability check encounters a network error, the DELETE is rejected (fail open: transient network issues must not cause data loss).
This logic is what allows malicious or incorrect DELETE signals to be absorbed without causing data loss across the network.
The scheduler runs continuously in the background, managing seven distinct periodic tasks:
Fires every expected_propagation_time milliseconds. In each cycle:
- Selects
N = calculateNodesPerPulse(total, expectedPropagationTime)random entries from the ciprdup (excluding self).Nis calculated as⌈totalEntries^(1/steps)⌉wheresteps = propagationTime / 1000. - Entry list is deduplicated by
zabefore processing to prevent concurrent tasks from racing on the same entry. - Up to 5 entries are audited concurrently (controlled by
PULSE_CONCURRENCY_LIMIT = 5) via DNS TXT + HTTP HEAD verification. - Valid entries: The consecutive failure counter (
fail_count) is reset to 0. APUTis sent (fire-and-forget) toNrandomly selected peer nodes. The PUT payload always carries a freshenedtimestamp: Date.now(): without this refresh, entries would fail Currentness Validation on the receiving side after the node has been running for more than 24 hours. - Invalid or unreachable entries: The entry's
fail_countis incremented. Only whenfail_countreaches 3 consecutive failures (the grace period,MAX_CONSECUTIVE_FAILURES) is the entry deleted. Before deletion, the full Deletion Validation Sequence is run: if the Reliability check passes despite the DNS+HTTP failure, the entry is retained andfail_countis reset (the node is alive and serving correct results, so the DNS+HTTP failure was transient). If the entry is deleted, aDELETEis sent toNrandomly selected peer nodes.
Fires on the same interval as the audit task. In each cycle:
- A random FTS expression is generated from
test_wordsand any recently captured user search terms. - The expression is executed locally as a baseline
QUERY. - Up to
Npeer nodes whosetimestampis older than 1 hour are selected randomly. - Up to 5 peers are queried concurrently via
QUERY https://ciprnode.{za}/. - Each peer's result set is compared to the local baseline using Jaccard set similarity with an auto-scaled threshold: 30% for <=5 results, 45% for <=20, 60% for larger sets.
- Content mismatch (QUERY succeeded but results diverge): the peer is evicted locally and a
DELETEis propagated toNpeers. - Network error (QUERY failed: timeout, connection refused, etc.): fail open - the entry is retained. Transient network issues must not cause data loss.
Fires every 3 × expected_propagation_time milliseconds:
- Validates the local node's own configuration using the same strict schema checks applied at startup.
- On success: broadcasts a
PUTfor the local entry toNrandom peers. - On failure: retries 3 times with 1-second delays. After 3 failed retries, a critical console alert is displayed and a
DELETEfor the node's ownzais sent toNrandom peers (self-destruct signal).
Fires every 4 hours. Re-broadcasts the local entry to all known peers via PUT. This ensures that even if the local entry was deleted from remote indexes (due to transient failures), it gets re-added. This is a key recovery mechanism against isolation.
Fires every 6 hours. Re-syncs from the configured bootstrap_nodes, bypassing the "DB already populated" check. This fetches and verifies entries from bootstrap nodes, breaking the isolation loop that can occur when all peers have evicted each other. Delegates to sync.js's reconnectToBootstraps().
Fires every 2 hours. Re-checks tombstoned entries (entries previously deleted due to verification failures, stored in a ciprdup_tombstones table). For each tombstoned entry:
- Attempts to fetch the entry data from the remote node (
GET /{za}/). - If the node responds, runs the full DNS TXT + HTTP HEAD verification.
- If verification passes, the entry is re-added to the ciprdup, the tombstone is removed, and a
PUTis propagated to peers.
This provides a secondary recovery mechanism beyond bootstrap reconnection, allowing nodes that temporarily went offline to be automatically re-discovered and re-indexed when they come back.
User queries submitted through the ciprface are captured by captureSearchTerms() and stored in a bounded in-memory cache (max 1024 terms, FIFO eviction). These terms are mixed into randomly generated FTS expressions used for Reliability Validation, progressively adapting the audit queries to real-world search vocabulary.
A server-rendered web interface served at https://ciprnode.{za}/. It acts as both a search engine front-end and the discoverable face of the ciprnode.
18 languages are bundled and fully translated:
| Code | Language |
|---|---|
ar |
Arabic |
bm |
Bambara |
bn |
Bengali |
de |
German |
en |
English |
es |
Spanish |
fa |
Farsi |
fr |
French |
hi |
Hindi |
id |
Indonesian |
it |
Italian |
ja |
Japanese |
pt |
Portuguese |
ru |
Russian |
sw |
Swahili |
uk |
Ukrainian |
ur |
Urdu |
zh |
Chinese |
Language is resolved in priority order: cipr_lang cookie → Accept-Language header → en fallback. A language switcher widget is rendered in the UI with a tooltip showing the full English name of the currently active language.
- FTS boolean operators:
AND(or implicit space),OR,NOT. - Phrase search:
"exact phrase"using double quotes. - Prefix search:
word*matches any word starting withword. - Initial token:
^wordmatches only ifwordis the very first token in the field. - Column filters:
title:linuxor{title description}:linux. - Proximity:
NEAR(term1 term2, distance). - Grouping:
(term1 OR term2) AND term3. - All inputs are sanitized server-side before reaching SQLite.
Three modes selectable from the UI, each adjusting the BM25 weight profile:
- Default: balanced weighting across all fields.
- Offering:
offeringfield is weighted at 32 (same asza). - Seeking:
seekingfield is weighted at 32 (same asza).
All filters can be combined freely with the text query:
| Filter | Parameter | Type |
|---|---|---|
| Offensiveness level | ol |
Multi-value checkbox (0–3) |
| Primary language | primary_lang |
Autocomplete text field (ISO 639-1) |
| Geographic proximity | geo[latitude], geo[longitude], geo[min], geo[max] |
Decimal degrees + km or mi radius range |
| Timestamp before | timestamp[before] |
Unix timestamp |
| Timestamp after | timestamp[after] |
Unix timestamp |
| Sort order | sort_by |
asc (default), desc, random |
| Pagination | pages[num], pages[size] |
Page number and page size |
When the node has ISE providers configured and a target ciprdup entry has a resindex (HEAD /ri/ returns 200), the ciprface displays intra-search controls that allow querying the resource's own internal search engine. Results are lazy-loaded per entry.
- A service worker (
sw.js) caches all static assets for offline use. - A web app manifest (
manifest.webmanifest) enables installation on mobile and desktop. - The service worker version is updated by changing the cache key in
sw.js; acontrollerchangelistener in the client JS triggers a reload when a new version is detected (skipping reload on first install).
The ciprface uses htmx v4.0.0-beta6 (public/js/htmx.js) to drive dynamic search interactions without writing custom JavaScript.
The CiprAPI uses the QUERY HTTP method (defined in draft-ietf-httpbis-safe-method-with-body) for search operations, as required by the Semantic RESTful API design. htmx v4-beta6 supports QUERY natively via the hx-method attribute: when hx-method="QUERY" is set on a form, htmx sends the form data as application/x-www-form-urlencoded in the request body (not as URL params, because QUERY is not in the GET|DELETE regex that controls URL param serialization), which matches the CiprAPI spec for QUERY requests. The server-side handler (src/api/controllers/search.js) merges URL params (e.g., pages[num], pages[size]) with body params (e.g., q, ol, geo_latitude).
The search form (src/templates/partials/search-form.eta) uses hx-method="QUERY" with hx-action="/", hx-target="#search-results-wrapper", hx-swap="innerHTML", and hx-push-url="true" to enable bookmarkable search results. Pagination links and the Explore button use standard hx-get attributes.
The QUERY method is supported by fetch() in modern browsers (Chrome 130+, Firefox 134+, Safari 18+). htmx v4 uses fetch() (not XMLHttpRequest), making it compatible.
The ciprface <head> includes:
- Standard
<title>and<meta name="description">. - Open Graph (
og:title,og:description,og:url,og:locale). - Dublin Core (
DC.Title,DC.Description,DC.Subject,DC.Creator,DC.Publisher,DC.Rights,DC.Identifier,DC.Coverage,DC.Language). <meta name="geo.position">,<meta name="geo.region">.<meta name="author">,<link rel="author">,<link rel="license">.<meta name="unavailable_after">(if configured).<link rel="profile">pointing to the ALPS profile.<link rel="canonical">.robots.txtthat allows full unrestricted crawling.
The QUERY /ri/ endpoint delegates searches to pluggable ISE providers defined in ciprnode.toml.
- Built-in adapter: Pagefind: a static site search provider. Pagefind uses its own index served from the target website; the ciprnode proxies the query and parses the response.
- Multiple providers: Multiple
[[ise_provider]]blocks can be configured; results from all of them are aggregated into a single response. - Extensible: New ISE adapters can be created by copying
integrations/ise/ise-template.example.js. - Cross-origin ping:
HEAD /ri/andOPTIONS /ri/include full CORS headers, allowing other ciprnodes to check ISE availability from the browser.
Every response carries the following headers:
| Header | Value |
|---|---|
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Content-Security-Policy |
default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; frame-ancestors 'none'; form-action 'self'; connect-src 'self' https: wss:; |
The CSP is intentionally strict. No external scripts (analytics, CDN-hosted libraries, etc.) are permitted to execute.
PUT and DELETE endpoints are rate-limited per originating IP before any processing occurs:
- PUT: 30 requests per IP per 60-second window.
- DELETE: 20 requests per IP per 60-second window.
- Exceeding the limit returns
429 Too Many RequestswithRetry-After: 60. - When behind Cloudflare, the
CF-Connecting-IPheader is used as the rate-limit key (not the Cloudflare edge IP, which would be shared across all clients). - The rate-limit map is garbage-collected every 5 minutes to prevent unbounded memory growth.
All outbound network calls from the ciprnode (peer sync, DNS, ISE queries) are routed through safeFetch(), which:
- Detects plain IP address URLs and blocks RFC 1918 / loopback ranges directly.
- For hostname-based URLs, performs a DNS resolution before connecting and blocks any target that resolves to a private or loopback IPv4 or IPv6 address.
- Injects a standardized
User-Agent: Ciprnode zero/1.0 (https://cipr.info)header into every outbound request to identify the software to peer nodes and WAFs.
- Content-Length guard: Requests with
Content-Lengthover 512 KB are rejected immediately with413 Payload Too Largebefore the body is read. - Streaming body limiter: Even if
Content-Lengthis absent or spoofed, the body reader enforces a hard 8 KB cap per PUT payload by counting actual bytes read from the stream.
The Triple Validation function does not use the OS DNS resolver for DoH lookups. Instead:
- It uses a Do53 resolver (from the configured pool) to resolve the DoH provider's hostname to an IP address via
Deno.resolveDns(). - It opens a direct TCP connection to that IP on port 443.
- It performs a TLS handshake using the hostname as the SNI parameter, which causes Deno's TLS stack to verify the certificate against the correct hostname, not the IP.
- It sends a minimal raw HTTP/1.1 request over the TLS tunnel and parses the DNS wire-format response.
This sequence ensures that no OS-level DNS poisoning or SSRF rebinding can forge the DoH resolver's identity. A fallback to standard fetch() (OS DNS) is used only if the Do53 bootstrap fails.
The Triple Validation requires all 3 randomly selected DoH servers to return the exact expected hash (3-of-3 consensus). It retries up to 12 times with an 8-second timeout per DoH query, giving the system ample opportunity to reach consensus even with slow or distant resolvers.
All FTS query strings pass through sanitizeFtsQuery() (see §1) before touching SQLite. Only structurally valid FTS5 expressions reach the database engine.
Ciprnode can automatically create and update the _cipr.{za} TXT record required for validation.
- Supported providers: Cloudflare and deSEC.io: each implemented as a self-contained adapter module.
- Extensible: New providers can be created by copying
integrations/dns/dns-template.example.js. - Zero-touch: If the computed
ciprHashchanges (e.g., you updatedtitleorkeywordsinciprnode.toml), the node detects the divergence at startup and triggers an automatic DNS update. - Auto-repair retry loop: After updating the TXT record, the node retries verification up to 3 times with 60-second delays to account for DNS propagation latency.
- Credentials: API tokens are read from environment variables (
CIPR_DNS_API_TOKEN, etc.) which take precedence over values inciprnode.toml.
When starting with an empty database, the ciprnode performs an initial population:
- DNS verification of the bootstrap node hostname (3 attempts, 1-second delays).
- Identity fetch: Retrieves and verifies the bootstrap node's own entry (
GET /{bootstrapZa}/) and inserts it as the first record. - Bulk fetch: Retrieves the bootstrap node's full ciprdup (
GET /). - Viral burst: Immediately after the bulk fetch, sends
GET /toNrandomly selected newly discovered peers to diversify the initial dataset beyond the single bootstrap source.
Each received entry is validated before insertion: hash consistency check, DNS TXT Triple Validation, and HTTP HEAD reachability. Invalid entries are silently skipped.
A configurable logging system with two independent output channels:
- Console output: Controlled by
log_levelinciprnode.toml.0: Silent (no output).1: Operational (startup messages, key events).2: Verbose (all outgoing requests, incoming responses, verification steps).- Styled with: ANSI 24-bit TrueColor escape codes in terminal, CSS substitutions in browser DevTools.
- Color legend:
OK= green,WA= yellow,KO= red,DNS= cyan,REQ= magenta,RES= amber.
- File output: When
debug = true, all output (equivalent tolog_level = 2) is also written to timestamped log files in/logs/. Logs rotate at 256 MB and files older than 24 hours are deleted automatically.
All responses with a body are transparently gzip-compressed when the client sends Accept-Encoding: gzip. Compression is applied to text/*, application/javascript, application/json, application/xml, and image/svg+xml content types. The Content-Length header is removed and Content-Encoding: gzip is added; a Vary: Accept-Encoding header is included for correct CDN handling.
Ciprnode can send notifications for key operational events. The system supports multiple simultaneous providers (currently email, with push notifications planned) and per-event routing so different events can go to different channels.
| Event | Trigger | Data |
|---|---|---|
startup_completed |
Startup sequence finishes successfully | Duration, entry count, timestamp |
startup_failed |
Startup sequence fails with an error | Error reason, timestamp |
self_validation_failed |
Self-validation fails after 3 retries | Node za, timestamp |
node_added |
A new entry is inserted into the ciprdup | Entry za, title, timestamp |
node_removed |
An entry is deleted from the ciprdup | Entry za, reason (dns_txt_mismatch, http_unreachable, reliability_mismatch, external_delete: ) |
bootstrap_completed |
Initial bootstrap sync succeeds | Entry count, duration |
bootstrap_failed |
Bootstrap retry window (1h) expires | Elapsed time |
dns_updated |
DNS TXT record is auto-updated | New ciprHash |
rate_limit_hit |
Per-IP rate limit is exceeded | Client IP, HTTP method |
periodic_digest |
Configurable interval status report | Entry count, uptime, DB size, memory, last pulse |
[notifications]
enabled = true # Global switch
providers = ["email"] # Active providers (multiple allowed)
digest_interval = 10800000 # Periodic digest interval in ms (0 = disabled)
[notifications.events] # Per-event provider routing (optional)
startup_completed = ["email"] # Default: all events → all providers
startup_failed = ["email"]
self_validation_failed = ["email"]
node_added = ["email"]
node_removed = ["email"]
bootstrap_completed = ["email"]
bootstrap_failed = ["email"]
dns_updated = ["email"]
rate_limit_hit = ["email"]
periodic_digest = ["email"]
[notifications.email] # Email provider settings
smtp_host = "mail.example.com"
smtp_port = 587
smtp_user = "user@example.com"
# smtp_pass = "your_password" # Use CIPR_SMTP_PASS env var
smtp_from = "ciprnode@example.com"
smtp_to = "admin@example.com"The SMTP password is treated as a secret with the same precedence chain as DNS credentials:
CIPR_SMTP_PASSOS environment variable (highest).env.${env}file.envfilesmtp_passinciprnode.toml(lowest, placeholder only)
rate_limit_hit notifications are throttled per IP with a 5-minute cooldown to prevent notification storms during attacks.
The email provider uses raw SMTP with STARTTLS via Deno's built-in Deno.connectTls() - zero external dependencies. It connects to the configured SMTP host, negotiates STARTTLS, authenticates with AUTH LOGIN, and sends plain text emails with X-Ciprnode headers.
New notification providers (e.g., push notifications, webhooks) can be added by creating a module in integrations/notifications/ that exports a send(subject, body, providerConfig) function returning Promise<boolean>. Multiple providers can be active simultaneously, and each event can be routed to specific providers via [notifications.events].
The system is designed to avoid cascading isolation caused by transient network failures. Multiple layers of protection work together:
When a Reliability Validation QUERY to a peer fails with a network error (timeout, refused connection, firewall drop), the validation is silently bypassed and the entry is retained. This applies to:
- Incoming
PUTrequests (the entry is accepted). - Incoming
DELETErequests (the DELETE is rejected, entry is retained). - Scheduled
runReliabilityChecks(the peer is not evicted).
This is intentional: hard-failing on network errors would break legitimate propagation under transient conditions. The trade-off is that a bad-faith node that simply does not respond to QUERY requests can always skip this check.
Entries that fail DNS TXT or HTTP HEAD verification during runPulseChecks are not immediately deleted. Instead, a consecutive failure counter (fail_count) is incremented. Only after 3 consecutive failures is the entry considered for deletion. A single successful verification resets the counter to 0.
Before deleting, the full Deletion Validation Sequence is run: if the Reliability check passes (the node is serving correct search results), the entry is retained and the counter is reset, even if DNS+HTTP failed. This prevents deletion of nodes that are alive but experiencing transient DNS or HTTP issues.
Three periodic mechanisms prevent permanent isolation and recover nodes that have been evicted:
| Mechanism | Interval | Purpose |
|---|---|---|
| Self-rebroadcast | 4 hours | Re-broadcasts the local entry to all known peers. |
| Bootstrap reconnection | 6 hours | Re-syncs from bootstrap nodes, breaking isolation. |
| Recovery sweep | 2 hours | Re-checks tombstoned entries and re-adds them if they pass verification. |
Download the latest ciprnode-zero-*.zip for your platform (Windows, Linux, macOS).
- Unzip the archive.
- Edit
ciprnode.toml. - Run the executable.
Requires Deno v2.7+ installed.
git clone https://github.com/barriteau/ciprnode-zero.git
cd ciprnode-zero
deno task start| Task | Command | Description |
|---|---|---|
| Start | deno task start |
Production start |
| Dev (watch) | deno task dev |
Watch mode with auto-reload on source changes |
| Front-end | deno task front-dev |
Skips sync/DNS/scheduler: for UI development |
| Debug | deno task debug |
Watch mode with --debug flag |
| Test | deno task test |
Run all tests |
| Build | deno task build |
Compile to standalone binary |
| Stop | deno task stop |
Stops a running background instance |
The node is fully configured via ciprnode.toml. All sections and parameters are documented below.
[network]
port = 443 # Listening port (use 443 in production)
bootstrap_nodes = [ # Trusted peers for initial sync
"https://ciprnode.cipr.info",
"https://ciprnode.barriteau.net",
"https://ciprnode.guasa.art",
]
expected_propagation_time = 120000 # Pulse interval in ms (120s default)
test_words = "dog casa network ..." # Words used for Reliability Validation audits
do53 = ["1.1.1.1", "9.9.9.9", ...] # DNS-over-UDP resolvers for DoH bootstrapping
doh = ["https://dns.google/dns-query", ...] # DNS-over-HTTPS endpoints (min 3 required)Sensitive values must never be committed to version control. The following measures protect credentials at every layer.
The env field in ciprnode.toml ("dev", "test", "prod") drives which secrets are loaded at runtime. This lets use test tokens in development and real tokens in production without changing configuration files between environments.
Loading precedence (highest to lowest):
- OS environment variables: set before the process starts (e.g.,
export CIPR_DNS_API_TOKEN=...or systemdEnvironment=). These always win. .env.${env}: environment-specific file (e.g.,.env.prod,.env.dev). Loaded first so its values take priority over the base file..env: base fallback file. Only fills keys not already set by the env-specific file or OS env vars.ciprnode.toml[dns_provider]section: lowest priority, used only when no other source provides the value.
Setup example:
# .env.dev - test tokens for local development
CIPR_DNS_API_TOKEN=test_token_123
# .env.prod - real tokens for production
CIPR_DNS_API_TOKEN=real_production_token_456
# .env - shared non-sensitive defaults (optional)
CIPR_DNS_PROVIDER=cloudflareWith env = "dev" in ciprnode.toml, the node loads .env.dev first, then .env as fallback. With env = "prod", it loads .env.prod first. OS environment variables override both files.
If the env-specific file (e.g., .env.dev) does not exist, it is silently skipped and .env provides all values. This means a deployment with only .env works regardless of the env setting - the env-specific files are optional.
API tokens and provider credentials are loaded from environment variables, which take precedence over any values in ciprnode.toml:
| Env Variable | Overrides |
|---|---|
CIPR_DNS_PROVIDER |
dns_provider.name |
CIPR_DNS_API_TOKEN |
dns_provider.api_token |
CIPR_DNS_ZONE_ID |
dns_provider.zone_id |
ciprnode.tomlis excluded from version control (.gitignore). A tracked template fileciprnode.example.tomlis provided with all placeholder values for new deployments..envand.env.*are excluded from version control.logs/anddata/directories are excluded - log files and the SQLite database never enter git history.
- No credential data is ever printed to stdout/stderr. The Cloudflare and deSEC DNS provider integrations use the project's
msg()function, which respects thelog_levelconfiguration. Even in verbose mode (log_level = 2), only operational status messages are shown - never tokens, keys, or secrets. - Zone IDs are truncated to their first 8 characters in debug output, preventing full identifier exposure.
When debug = true, all console output is also written to timestamped log files in logs/. The log formatter (formatForFile) automatically redacts any object keys matching api_token, api_key, token, secret, password, or authorization - replacing their values with [REDACTED] before writing to disk. This ensures that even if a credential-bearing object is accidentally passed to a logging function, the secret never reaches persistent storage.
The build script (deno task build) reads ciprnode.toml and replaces any api_token value with the placeholder "YOUR_TOKEN_HERE" before bundling it as ciprnode.example.toml in the distribution archive. Real credentials can never ship in a public release.
- Config parsing errors log only
error.message, not the full error object (which could contain raw file content). - Fatal startup errors log
error.messageanderror.stackseparately, never the full error object. - API error responses from DNS providers are truncated to 200 characters in debug output.
The configuration validator checks that if a dns_provider.name is set, api_token must be a non-empty string. Missing tokens are caught at startup with a clear error message, preventing silent failures that might lead to credential debugging in production.
- Config load: Parses and validates
ciprnode.tomlwith strict type and format checks. - Hash generation: Computes the
ciprHash(SHA-256) from all configured entry fields concatenated with¦separators. - DB init: Opens (or creates)
data/ciprdup.db, initializes the schema, FTS5 table, triggers, indexes, and language table if not already present. - Bootstrap sync: If the DB has 0 or 1 entries, performs initial population from the bootstrap node (DNS verify → identity fetch → bulk fetch → viral burst). Halts on fatal bootstrap failure if DB is empty.
- Self-validation: Checks the local entry's hash against the current config. If the hash has changed, triggers DNS auto-update (if a provider is configured) and enters the retry loop.
- DNS verification: Runs Triple Validation (3 random DoH providers via custom TLS) of the local
_cipr.{za}TXT record. Retries 3×60s if the record is stale after a managed update. - HTTP server start: Begins serving on the configured port. Static assets, API routes, and fallback HTML are handled in priority order.
- Reachability check: Sends
HEAD https://ciprnode.{za}/to confirm the node is publicly reachable. Ciprpulse does not start if this check fails (unless indebugmode, where a loopback fallback is tried). - Ciprpulse start: Begins the audit/propagation loop, reliability validation loop, self-validation loop, self-rebroadcast (4h), bootstrap reconnection (6h), and recovery sweep (2h). The
ciprdup_tombstonestable is created if it doesn't exist.
Ciprnode zero/
├── main.js # Entry point
├── ciprnode.toml # Configuration file
├── deno.json # Deno tasks and import map
├── src/
│ ├── api/
│ │ ├── server.js # HTTP server, compression, security headers, rate limiter
│ │ ├── routes.js # Request router
│ │ ├── controllers/ # root.js, entry.js, search.js
│ │ └── views/ # hal.js, renderer.js (Eta templates)
│ ├── bot/
│ │ └── scheduler.js # Ciprpulse: audit, reliability, self-validation, rebroadcast, recovery, propagation
│ ├── core/
│ │ ├── config.js # TOML config loader and parser
│ │ ├── crypto.js # SHA-256 hashing (Web Crypto API)
│ │ ├── dns.js # DoH/Do53 client, TXT Triple Validation, custom TLS
│ │ ├── fts_generator.js # Random FTS expression builder and search term cache
│ │ ├── logger.js # File log writer with rotation
│ │ ├── sync.js # Initial bootstrap sync and periodic reconnection
│ │ ├── utils.js # ciprHash generation, safeFetch, readBodyWithLimit, msg/line
│ │ ├── validator.js # Config and entry validation (za format, geo range, etc.)
│ │ └── verification.js # verifyNode (DNS+HTTP), verifyReliability (returns {reliable, networkError}), compareSearchResults (auto-scaled threshold)
│ ├── db/
│ │ ├── client.js # SQLite connection setup
│ │ ├── geo.js # Haversine is_within_radius function
│ │ ├── languages.json # ISO 639-1 dataset (180+ entries)
│ │ ├── repo.js # Data access: insertEntry, getEntry, deleteEntry, searchEntries, tombstone management
│ │ └── schema.js # Table/FTS5/trigger/index DDL, tombstones table, fail_count migration
│ ├── locales/ # 18 language JSON translation files
│ └── templates/ # Eta HTML templates (layouts, views, partials)
├── integrations/
│ ├── dns/ # cloudflare.js, desec.js, dns-template.example.js
│ └── ise/ # pagefind.js, ise-template.example.js
├── public/
│ ├── css/ # Stylesheets
│ ├── js/ # ciprnode.js (app logic), htmx.js
│ ├── figures/ # SVG icons
│ ├── profiles/cipr.json # ALPS profile
│ ├── manifest.webmanifest # PWA manifest
│ ├── robots.txt # Full crawl allowed
│ └── sw.js # Service worker (offline cache)
├── scripts/ # Build, control, and report scripts
├── tests/ # Deno test suites (api_endpoints, config, dns)
└── data/ # Runtime data (ciprdup.db, ciprnode.pid)- Runtime: Deno v2.7+
- Database: SQLite via FFI (
@db/sqlite) - Templating: Eta (
@eta-dev/eta) - Architecture: Modular monolith (Core, API, Bot/Scheduler, DB, Integrations)
Built with minimalism in mind, using the Deno Standard Library plus two focused third-party modules.
| Dependency | Purpose |
|---|---|
@std/http |
Core HTTP server and static file serving. |
@std/toml |
Parsing ciprnode.toml. |
@std/path |
Cross-platform file path manipulation. |
@std/fs |
File system operations (copy, exists, mkdir). |
@std/dotenv |
Loading environment variables from .env. |
@std/crypto |
SHA-256 hashing. |
@std/encoding |
Base64url encoding for DNS wire format. |
@std/assert |
Assertion library for tests. |
@db/sqlite |
Zero-dependency SQLite driver. |
@eta-dev/eta |
Lightweight templating engine for the ciprface. |
The ciprface ships with the following bundled, self-hosted front-end assets (no CDNs, no external requests, all served from public/):
| Asset | Version | Source | Purpose |
|---|---|---|---|
| htmx.js | v4.0.0-beta6 | htmx.org | Dynamic HTML interactions. Supports the QUERY HTTP method natively via hx-method (see htmx section). |
| highlight.js CSS theme | xcode.min.css | highlight.js | Code block syntax highlighting theme (CSS only, no JS runtime loaded). |
| Iosevka | woff2 | GitHub | Monospace font for code blocks. |
| Libertinus | woff2 | GitHub | Serif/sans-serif font family for body text and headings. |
| Poller One | woff2 | Google Fonts | Display font for the main title. |
All fonts are self-hosted as .woff2 files in public/css/typography/. The CSS includes KaTeX and MathJax compatibility classes (.katex, mjx-container.MathJax) but neither library's JS runtime is loaded.