Add the dependency
In your project’s ziran.toml; pin ref to a commit for reproducible builds.
[dependencies.daochi_client]
git = "https://github.com/ziranlang/daochi-client.git"
ref = "master"Overview
Daochi Client is the independent Ziran library for applications that sync with
Daochi. It contains no server, mesh node, database, or UI implementation.
It has no Kryon dependency and lives in the
Ziran organization. Add it with
ziran add https://github.com/ziranlang/daochi-client.git.
The protocol modules currently provide:
- canonical challenge, login body, and signed message builders (
wire.zi); - signed app releases (
daochi-zib-release-v2): shape, ordering and bounds, the canonical signed message, and the signature through the application's own Ed25519 check (release.zi); - HTTPS and local network URL policy (
url.zi); - challenge parsing, signed login preparation, and token parsing (
auth.zi); - login and bearer requests with one fresh login after a
401(client.zi); - canonical device registration and protocol v6 transaction messages
(
transaction.zi); - signed device registration and protocol v6 sync requests (
sync.zi); - encrypted record profile v1 (
daochi-record-v1) envelopes with canonical authenticated metadata and fresh XChaCha20-Poly1305 nonces (record.zi, native; link libsodium); - entity data keys and shared chunks (
entity.zi): the record key of each kind of account data and convergent 8 MiB chunk sealing, so every app of the account stores one copy of each photo or level (Daochidocs/entity-data.md), and entity records sealed and opened asdaochi-record-v1envelopes in plain Ziran, without libsodium; - polled login, bearer requests, and signed sync operations
(
async_client.zi,async_sync.zi); - resumable record pages, bounded transfer recovery and cursor validation
(
sync_flow.zi, used byasync_sync.zi); - alias and friend requests, actions, lists, and stats (
social.zi); - challenge signed account deletion without transmitting a key backup
(
account.zi). - WebSocket URL, event validation, and one event wait through a host callback
(
events.zi). - account keys: an ML-DSA-44 key pair with its SHA-256 public id, message
signing, and the plain (
account-key-v1) and passphrase-encrypted (ksync-account-key-v2) key files (keys.zi, signing through the Oqs package); - PBKDF2 for the key files, ChaCha20-Poly1305 and XChaCha20-Poly1305
(
crypto.zi), which also passes on SHA-256, HMAC-SHA-256 and hex from the crypto package.
The application supplies the account signer and body digest; keys.zi
provides both for Daochi account keys (SignAccountMessage, Sha256Hex), and
the app still stores the keys. A host supplies
bounded HTTP requests through SendRequest; the host must NUL terminate every
response buffer, including on error. This keeps private keys in the app's key
store and keeps platform transport outside the protocol core. Applications
persist Session if they want bearer tokens to survive restart. Account keys
must remain when a bearer token expires.
Session.registration remembers one successful device registration for up to
an hour between successful requests, scoped to the normalized node URL,
account, app, client, device key ID and public key. Sync then sends only its
signed transaction. The node still verifies both signatures and revocation on
every request. A missing-device response permits one fresh registration and
a newly signed transaction; revoked devices cannot register again. Clearing or
renewing the bearer session clears the hint. Hosts may save this public metadata
with WriteRegistration and restore it with ReadRegistration; it contains no
private key or bearer token.
record_codec.zi optionally packs JSON before encryption with a bounded gzip
frame. Keep the released identity ciphertext and send an independently sealed
packed_nonce/packed_ciphertext pair alongside it. Readers request
record_encoding:"gzip-v1" on record pages; older nodes and readers continue to
use the identity representation. After authenticating the ciphertext, validate
the frame's claimed length before allocating and call UnpackRecord with the
app's own record limit. Binary media bypass this codec. Native hosts link zlib
(-lz); Emscripten hosts use -sUSE_ZLIB=1.
A transport may resend a signed POST when a reused connection drops after the node committed it. Sync recognizes the exact signed-transaction replay response and retries with a fresh nonce within its existing bounded recovery budget. The node's replay protection remains enforced.
Import the package modules with #import "daochi_client/client" and
#import "daochi_client/sync". Set Client.app_id, supply a Device with a
persisted Ed25519 key and signer, and supply a cryptographically random 64-digit
hex nonce callback. Sync registers the device and signs the exact sync body
with both the account and device keys. The app owns the payload format and
merges the response. A native host can adapt SendRequest to Ziran's curl HTTP
module and ReceiveEvent to its bounded curl WebSocket module. Browser and
Android hosts must implement those host boundaries before those builds sync.
Event loops can use BeginRequest/PollRequest and BeginSync/PollSync
with AsyncTransport. Its start callback copies the request metadata and
retains the caller's bounded output buffer; poll returns -1 while pending,
0 for transport failure, or 1 with the HTTP status when complete. Every
completed response must be NUL terminated. Cancel releases all transport
references to that output buffer before returning. Keep the pending operation,
session, client identity, signing contexts, body, and extra header strings alive
until completion or cancellation. Each pending operation has its own transport
context. CancelRequest and CancelSync release an active transfer and finish
with AUTH_REQUEST_FAILED. Both request modes share the same one-login retry
after a 401; sync modes share the exact registration and transaction builders.
Pass a SyncFlow to BeginSync to sync accounts larger than one response
buffer. Daochi Client switches to the server's encrypted record pages after a
capacity failure, signs each page request, retries interrupted transfers and
validates forward cursor progress. It retains a bounded response buffer across
pages; total account size has no fixed client buffer limit. An optional transport
failure callback reports TRANSFER_FAILURE_CAPACITY; other failures retain the
existing result codes. Smaller record pages are tried if a page itself overflows.
The flow's host callbacks load/save account-and-node-local SyncPageState,
prepare the app's current encrypted records, and accept each response. accept
must atomically merge records and persist the proposed next state, returning
false without advancing durable progress on failure. Only a terminal page may
advance the account watermark. On restart the library resumes the saved cursor;
cancel releases the current transfer and owned request memory. The host's clock
callback refreshes transaction time between pages. These callbacks contain app
storage policy, not page requests or transport retry decisions.
prepare receives the maximum encrypted record count for the next upload,
including any reduction after an oversized page. Retain its returned body until
the next prepare call or completion.
Zero means download only: after overflow recovery or while resuming a partial
download, the library omits uploads until the terminal page. Otherwise repeated
uploads could keep moving the same account's download cursor. A later sync can
upload the retained local queue.
sh tests/run.sh checks the library using the toolchain selected by
ziran pkg path ziran, including portable .zx URL and wire tests. An
ignored ziran.local.toml can select the local compiler at
../../ziran; otherwise the test uses ziran.lock. The wire
test also checks a saved .zir to .zx round trip. Set ZIRAN_BIN to use
a different launcher, or ZIRAN_DIR and ZI2C_BIN for explicit compiler
overrides.
The library does not provision keys, revoke devices, store account data, or merge application records. Each app owns those operations and supplies its payload builder, response merge, key store, and platform transport.
The protocol uses Ziran's TextView and bounded TextUntilNul instead of
a native string-layout adapter. URL policy, wire builders, and signed device
registration message construction compile and run as portable .zx. The
full HTTP client is not yet a .zx bundle: host transport, digest, and
signer callbacks need bundle capabilities. Native Linux integration is tested;
browser and Android transports remain application host work.
sync.PrepareRequestHeader prepares the same account/device signed v6
transaction for an exact HTTP method, path and byte-counted body, including
empty GET/HEAD bodies and binary blob uploads. Hosts that already stream and
hash the body can use sync.SignTransactionHeader with an explicit
transaction.SyncTransaction; its account, app and device must match the
supplied client. PrepareSyncHeader retains the existing POST sync behavior.
The host still owns transport, pinned-node policy, session/key storage and
remote acknowledgment handling.
daochi-client/delegated.zi
delegated
Key-bound session and scoped sync transports. Never populates HttpRequest.token.
Import with #import "daochi_client/delegated".
CurrentTime :: #type (context: *void) -> s64RandomID :: #type (context: *void, output: []u8) -> boolDelegateClient :: struct { server_url: string; account_id: string; app_id: string; client_id: string; grant_id: string; node_id: string; audience: string; signing_key: string; encryption_key: string; bot_id: s64; telegram_id: s64; clock_context: *void; clock: CurrentTime; scopes: []RequestedScope; grant: *GrantStorage; signing_context: *void; signer: SignMessage; random_context: *void; random: RandomID; transport_context: *void; send: SendRequest; }RequestProof :: struct { grant_id: string; session_id: string; account_id: string; app_id: string; client_id: string; node_id: string; audience: string; challenge: string; method: string; path: string; query: string; body_sha256: string; expires_at: s64; nonce: string; }DelegatedRecord :: struct { id: string; key_id: string; nonce: string; ciphertext: string; updated_at: string; content_hash: string; schema_version: s32; parent_id: string; }DelegateClientValid :: (client: DelegateClient) -> boolCurrentDelegateTime :: (client: DelegateClient) -> s64DelegateExpiryCurrent :: (client: DelegateClient, expires_at: s64) -> boolSessionValid :: (client: DelegateClient, session: *DelegateSession, now: s64) -> boolClearDelegateSession :: (session: *DelegateSession)BuildSessionMessage :: (challenge: *SessionChallenge, output: []u8) -> boolPrepareSessionBody :: (client: DelegateClient, challenge: *SessionChallenge, now: s64, output: []u8) -> boolAcceptSessionAnswer :: (client: DelegateClient, status: s32, json: string, now: s64, session: *DelegateSession) -> AuthResultRequestRoute :: (method: string, path: string, query: string) -> boolBuildProofMessage :: (proof: RequestProof, output: []u8) -> boolBuildProofHeader :: (proof: RequestProof, signature: string, output: []u8) -> boolScopeIndex :: (client: DelegateClient, collection: string, write: bool) -> s32BuildDelegatedSyncBody :: (client: DelegateClient, collection: string, read: bool, since_version: s64, limit: s32, records: []DelegatedRecord, output: []u8) -> boolDelegatedSyncBodyValid :: (client: DelegateClient, body: string) -> boolReject foreign generations and every unsupported sync member before signing.
DelegatedResponseInfo :: struct { collection: [65]u8; next_version: s64; truncated: bool; applied: s64; records_at: s32; record_count: s32; }ReadDelegatedSyncResponse :: (client: DelegateClient, body: string, expected_collection: string, output: *DelegatedResponseInfo) -> boolResponseMatchesRequest :: (client: DelegateClient, request_body: string, response: string) -> boolPrepareDelegateHeaders :: (client: DelegateClient, session: *DelegateSession, now: s64, method: string, path: string, query: string, body: string, proof_header: []u8, credential: []u8) -> AuthResultRenewDelegateSession :: (client: DelegateClient, session: *DelegateSession, now: s64, response: []u8) -> AuthResultDelegateRequest :: (client: DelegateClient, session: *DelegateSession, now: s64, body: string, output: []u8, status: *s32) -> AuthResultdaochi-client/async_delegated.zi
async_delegated
Poll-driven counterpart of delegated session renewal and scoped requests.
Import with #import "daochi_client/async_delegated".
PendingDelegate :: struct { client: DelegateClient; session: *DelegateSession; transport: AsyncTransport; now: s64; body: string; output: []u8; session_only: bool; address: [768]u8; session_body: [512]u8; challenge_json: [2048]u8; proof: [4096]u8; credential: [46]u8; headers: [2]HttpHeader; stage: s32; waiting: bool; done: bool; result: AuthResult; status: s32; }FinishDelegate :: (pending: *PendingDelegate, result: AuthResult)StartDelegateTransfer :: (pending: *PendingDelegate, path: string, body: string, output: []u8, stage: s32, headers: []HttpHeader)StartDelegateChallenge :: (pending: *PendingDelegate)StartDelegateSync :: (pending: *PendingDelegate)InitializeDelegate :: (pending: *PendingDelegate, client: DelegateClient, session: *DelegateSession, now: s64, transport: AsyncTransport, output: []u8, session_only: bool, body: string) -> boolBeginDelegateSession :: (pending: *PendingDelegate, client: DelegateClient, session: *DelegateSession, now: s64, transport: AsyncTransport, output: []u8) -> boolBeginDelegateRequest :: (pending: *PendingDelegate, client: DelegateClient, session: *DelegateSession, now: s64, transport: AsyncTransport, body: string, output: []u8) -> boolPollDelegate :: (pending: *PendingDelegate) -> boolCancelDelegate :: (pending: *PendingDelegate)daochi-client/envelope.zi
envelope
Approved collection keys only. The host stores the separate recipient key.
Import with #import "daochi_client/envelope".
EncryptionKeys :: struct { public_key: [1184]u8; secret_key: [2400]u8; }CreateEncryptionKeys :: (keys: *EncryptionKeys) -> boolEncryptionPublicHex :: (keys: *EncryptionKeys, output: []u8) -> boolClearEncryptionKeys :: (keys: *EncryptionKeys)BuildEnvelopeAAD :: (grant: Grant, scope: GrantScope, output: []u8) -> boolSealCollectionKey :: (grant: Grant, scope: GrantScope, collection_key: []u8, output: []u8) -> boolOpenCollectionKey :: (grant: Grant, scope: GrantScope, keys: *EncryptionKeys, output: []u8) -> booldaochi-client/account.zi
account
Account deletion uses a fresh challenge and account signature. No private key or exported backup is sent to Daochi.
Import with #import "daochi_client/account".
BuildDeleteBody :: (account_id: string, public_key: string, output: []u8) -> boolPrepareDelete :: (client: Client, challenge: string, body: []u8, signature: []u8) -> AuthResultShared signed deletion preparation for blocking and polled hosts.
DeleteAccount :: (client: Client) -> AuthResultdaochi-client/async_account.zi
async_account
Signed account deletion advanced without blocking the host event loop.
Import with #import "daochi_client/async_account".
PendingDeletion :: struct { client: Client; transport: AsyncTransport; server: [512]u8; address: [768]u8; challenge: [4096]u8; body: [4096]u8; signature: [4841]u8; headers: [2]HttpHeader; response: [4096]u8; status: s32; deleting: bool; waiting: bool; done: bool; result: AuthResult; }FinishDeletion :: (pending: *PendingDeletion, result: AuthResult)BeginDeletion :: (pending: *PendingDeletion, client: Client, transport: AsyncTransport) -> boolPollDeletion :: (pending: *PendingDeletion) -> boolCancelDeletion :: (pending: *PendingDeletion)daochi-client/async_client.zi
async_client
A bounded request operation advanced by the host's frame/event loop. Request data, identity, session, and output remain caller-owned until done.
Import with #import "daochi_client/async_client".
StartTransfer :: #type (context: *void, request: HttpRequest, output: []u8) -> boolPollTransfer :: #type (context: *void, status: *s32) -> s32CancelTransfer :: #type (context: *void)TransferFailure :: #type (context: *void) -> s32TRANSFER_FAILURE_TRANSPORT :: 1TRANSFER_FAILURE_CAPACITY :: 2AsyncTransport :: struct { context: *void; start: StartTransfer; poll: PollTransfer; cancel: CancelTransfer; failure: TransferFailure; }PendingRequest :: struct { client: Client; session: *Session; transport: AsyncTransport; now: s64; method: string; path: string; body: string; output: []u8; extra: [14]HttpHeader; extra_count: s32; headers: [16]HttpHeader; address: [2048]u8; challenge: [4096]u8; login_body: [4096]u8; signature: [4841]u8; login_response: [8192]u8; stage: s32; retries: s32; session_only: bool; waiting: bool; done: bool; result: AuthResult; status: s32; failure: s32; }FinishRequest :: (pending: *PendingRequest, result: AuthResult)StartPendingTransfer :: (pending: *PendingRequest, request: HttpRequest, output: []u8, stage: s32)SetPendingAddress :: (pending: *PendingRequest, path: string) -> boolStartChallenge :: (pending: *PendingRequest)StartLogin :: (pending: *PendingRequest)StartBearer :: (pending: *PendingRequest)InitializeRequest :: (pending: *PendingRequest, client: Client, session: *Session, now: s64, transport: AsyncTransport) -> boolBeginSession :: (pending: *PendingRequest, client: Client, session: *Session, now: s64, transport: AsyncTransport) -> boolBeginRequest :: (pending: *PendingRequest, client: Client, session: *Session, now: s64, transport: AsyncTransport, method: string, path: string, body: string, output: []u8, extra_headers: []HttpHeader) -> boolPollRequest :: (pending: *PendingRequest) -> boolCancelRequest :: (pending: *PendingRequest)daochi-client/async_sync.zi
async_sync
The same v6 registration and signed transaction as Sync, advanced by polls.
Import with #import "daochi_client/async_sync".
PendingSync :: struct { request: PendingRequest; client: Client; session: *Session; transport: AsyncTransport; device: Device; now: s64; random_context: *void; random: RandomNonce; body: string; output: []u8; registration: [6144]u8; registration_response: [2048]u8; transaction_header: [8192]u8; stage: s32; done: bool; result: AuthResult; status: s32; flow: SyncFlow; page: SyncPageState; page_body: *u8; record_limit: s32; transfer_retries: s32; download_only: bool; cached_registration: bool; registration_retried: bool; }FinishSync :: (pending: *PendingSync, result: AuthResult)PrepareNextSync :: (pending: *PendingSync) -> boolStartSyncTransaction :: (pending: *PendingSync)BeginSync :: (pending: *PendingSync, client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce, transport: AsyncTransport, body: string, output: []u8, flow: SyncFlow) -> boolPollSync :: (pending: *PendingSync) -> boolCancelSync :: (pending: *PendingSync)daochi-client/sync_flow.zi
sync_flow
Resumable encrypted-record downloads. The host persists account/node-local state and atomically merges each accepted page; protocol policy stays here.
Import with #import "daochi_client/sync_flow".
SyncPageState :: struct { enabled: bool; pending: bool; cursor: [8192]u8; }LoadSyncState :: #type (context: *void, state: *SyncPageState) -> boolPrepareSyncBody :: #type (context: *void, maximum_records: s32) -> stringSaveSyncState :: #type (context: *void, state: *SyncPageState) -> boolAcceptSyncPage :: #type (context: *void, response: string, next: *SyncPageState) -> boolSyncClock :: #type (context: *void) -> s64SyncFlow :: struct { context: *void; load: LoadSyncState; prepare: PrepareSyncBody; save: SaveSyncState; accept: AcceptSyncPage; clock: SyncClock; }RecordCursor :: struct { version: s64; collection: [1024]u8; id: [1024]u8; }SyncJsonValue :: (text: string, object_at: s32, name: string) -> stringSyncVersion :: (text: string, at: s32, value: *s64) -> boolReadRecordCursor :: (text: string, at: s32, out: *RecordCursor) -> boolCompareSyncText :: (left: string, right: string) -> s32RecordCursorAfter :: (next: *RecordCursor, previous: *RecordCursor) -> boolRestartStaleSyncPage :: (body: string, state: *SyncPageState) -> boolA saved cursor behind the request's since_server_version is left from a download that began at an earlier position. The node refuses a cursor before since, so every page request would fail; the download starts again at the request's position instead. True when the cursor was dropped.
BuildSyncPageRequest :: (body: string, state: *SyncPageState, limit: s32, output: []u8, download_only: bool) -> boolOutput borrows the caller's request buffer. Legacy snapshot selectors are deliberately excluded: a page never means replacing an entire local account.
AcceptSyncResponse :: (body: string, response: string, state: *SyncPageState, next: *SyncPageState, limit: s32) -> boolNo cursor may advance until the host has committed this response. The next state is separate so a rejected or interrupted merge cannot skip records.
daochi-client/auth.zi
auth
Portable challenge/login protocol. The application owns keys and the host owns HTTP; neither is stored here.
Import with #import "daochi_client/auth".
AuthResult :: enum { AUTH_OK = 0; AUTH_INVALID_URL = 1; AUTH_NO_ACCOUNT = 2; AUTH_PAYLOAD_FAILED = 3; AUTH_CHALLENGE_FAILED = 4; AUTH_SIGN_FAILED = 5; AUTH_REQUEST_FAILED = 6; AUTH_FAILED = 7; }Identity :: struct { account_id: string; public_key: string; client_id: string; signing_context: *void; restore_hold: bool; }Registration :: struct { server: [512]u8; account: [65]u8; app: [65]u8; client: [129]u8; device: [65]u8; public_key: [65]u8; expires_at: s64; }Public registration metadata only. A cache never replaces the server's account/device signatures, revocation checks or transaction replay checks.
Session :: struct { token: [4096]u8; account_alias: [40]u8; account_alias_known: bool; profile_icon: s64; profile_icon_known: bool; expires_in_seconds: s64; server_time: s64; expires_at_local: s64; login_local_time: s64; registration: Registration; }DigestBody :: #type (body: string, output: []u8) -> boolSignMessage :: #type (context: *void, message: string, output: []u8) -> boolValidJson :: (json: string) -> boolFieldText :: (json: string, name: string, output: []u8) -> boolFieldNumber :: (json: string, name: string, fallback: s64) -> s64HexText :: (text: string, digits: s64) -> boolPrepareLogin :: (identity: Identity, challenge_json: string, digest: DigestBody, signer: SignMessage, body: []u8, signature: []u8) -> AuthResultAcceptLogin :: (status: s32, response_json: string, session: *Session) -> AuthResultdaochi-client/client.zi
client
Synchronous Daochi client entry points. Hosts provide the bounded request callback, so this module contains no platform or application implementation.
Import with #import "daochi_client/client".
SendRequest :: #type (context: *void, request: HttpRequest, output: []u8, status: *s32) -> boolClient :: struct { server_url: string; app_id: string; identity: Identity; digest: DigestBody; signer: SignMessage; send: SendRequest; transport_context: *void; }FetchChallengeJson :: (client: Client, output: []u8) -> AuthResultLogin :: (client: Client, session: *Session) -> AuthResultMethodValid :: (method: string) -> boolPathValid :: (path: string) -> boolTokenValid :: (session: *Session, now: s64) -> boolSetExpiry :: (session: *Session, now: s64)EnsureSession :: (client: Client, session: *Session, now: s64) -> AuthResultClearSession :: (session: *Session)BearerRequestWithHeaders :: (client: Client, session: *Session, now: s64, method: string, path: string, body: string, output: []u8, status: *s32, extra_headers: []HttpHeader) -> AuthResultA 401 invalidates the bearer token, then performs exactly one fresh login and retries the original request. The caller persists the resulting session.
BearerRequest :: (client: Client, session: *Session, now: s64, method: string, path: string, body: string, output: []u8, status: *s32) -> AuthResultdaochi-client/crypto.zi
crypto
Account and record cryptography for Daochi apps: SHA-256, HMAC-SHA-256 and hex encoding from the crypto package, the account key files' PBKDF2-HMAC-SHA-256, RFC 8439 ChaCha20-Poly1305 and its 24-byte nonce form XChaCha20-Poly1305. All of it is plain Ziran; the signatures themselves come from the Oqs package. The functions take byte pointers and lengths and return 1 on success, 0 on invalid input.
Import with #import "daochi_client/crypto".
Sha256 :: (data: *u8, length: s64, output: *u8)BytesToHex :: (bytes: *u8, length: s64, output: *u8, output_size: s64) -> s32HexToBytes :: (hex: string, output: *u8, output_size: s64) -> s32Sha256Hex :: (data: *u8, length: s64, output: *u8)HmacSha256 :: (key: *u8, key_length: s64, data: *u8, data_length: s64, output: *u8) -> s32HmacSha256Text :: (key: *u8, key_length: s64, data: string, output: *u8) -> s32Pbkdf2Hmac :: (password: *u8, password_length: s64, password_text: string, use_text: bool, data: *u8, data_length: s64, output: *u8) -> s32PBKDF2 keeps the account key-file derivation format: historical exports included at most 32 salt bytes.
Pbkdf2Input :: (password: *u8, password_length: s64, password_text: string, use_text: bool, salt: *u8, salt_length: s64, iterations: u64, output: *u8) -> s32Pbkdf2Sha256 :: (password: *u8, password_length: s64, salt: *u8, salt_length: s64, iterations: u64, output: *u8) -> s32Pbkdf2Sha256Text :: (password: string, salt: *u8, salt_length: s64, iterations: u64, output: *u8) -> s32Poly1305State :: struct { r: [5]u64; scaled: [4]u64; h: [5]u64; }Poly1305Load32 :: (bytes: *u8) -> u32Poly1305Store32 :: (bytes: *u8, value: u32)Poly1305Initialize :: (state: *Poly1305State, key: *u8)Poly1305Block :: (state: *Poly1305State, message: *u8, length: s32)Poly1305AeadPadded :: (state: *Poly1305State, data: *u8, length: s64)Poly1305Finish :: (state: *Poly1305State, key: *u8, tag: *u8)Poly1305AeadTag :: (key: *u8, aad: *u8, aad_length: s64, ciphertext: *u8, ciphertext_length: s64, tag: *u8)ChaChaBlockBytes :: 64RFC 8439 ChaCha20-Poly1305 for encrypted records and key files.
ChaChaTagBytes :: 16ChaChaLengthValid :: (length: s64) -> boolChaChaRotateLeft :: (value: u32, bits: u32) -> u32ChaChaQuarterRound :: (state: *u32, a: s32, b: s32, c: s32, d: s32)ChaChaBlock :: (key: *u8, nonce: *u8, counter: u32, output: *u8)ChaChaXor :: (key: *u8, nonce: *u8, input: *u8, length: s64, output: *u8)ChaChaMacKey :: (key: *u8, nonce: *u8, output: *u8)ChaCha20Poly1305Seal :: (key: *u8, nonce: *u8, plain: *u8, plain_length: s64, aad: *u8, aad_length: s64, output: *u8, output_capacity: s64) -> s32ChaCha20Poly1305Open :: (key: *u8, nonce: *u8, sealed: *u8, sealed_length: s64, aad: *u8, aad_length: s64, plain: *u8, plain_capacity: s64) -> s32HChaCha20 :: (key: *u8, nonce: *u8, output: *u8)HChaCha20 (draft-irtf-cfrg-xchacha): the ChaCha20 rounds over the key and a 16-byte nonce, without the final addition; words 0-3 and 12-15 are the 32-byte subkey.
XChaChaSubkey :: (key: *u8, nonce: *u8, subkey: *u8, short_nonce: *u8)The subkey and 12-byte nonce that XChaCha20-Poly1305 hands to ChaCha20-Poly1305: four zero bytes, then the last 8 nonce bytes.
XChaChaWipe :: (bytes: *u8, length: s32)XChaCha20Poly1305Seal :: (key: *u8, nonce: *u8, plain: *u8, plain_length: s64, aad: *u8, aad_length: s64, output: *u8, output_capacity: s64) -> s32XChaCha20-Poly1305 with a 24-byte nonce, the same construction as libsodium's crypto_aead_xchacha20poly1305_ietf and Monocypher's AEAD: output receives the ciphertext and its 16-byte tag.
XChaCha20Poly1305Open :: (key: *u8, nonce: *u8, sealed: *u8, sealed_length: s64, aad: *u8, aad_length: s64, plain: *u8, plain_capacity: s64) -> s32daochi-client/crypto_native.zi
crypto_native
Native provider for existing Daochi client content encryption. The account, content key, nonce and authenticated context are supplied by the caller; this module never creates identities, persists keys or sends requests. Libsodium supplies the same XChaCha20-Poly1305 bytes used by existing blobs.
Import with #import "daochi_client/crypto_native".
Initialize :: () -> boolClear :: (bytes: []u8)RandomBytes :: (bytes: []u8) -> boolDigest :: (input: []u8, output: []u8) -> boolHmacSha256 :: (input: []u8, key: []u8, output: []u8) -> boolSeal :: (input: []u8, aad: []u8, key: []u8, nonce: []u8, output: []u8) -> s64Open :: (sealed: []u8, aad: []u8, key: []u8, nonce: []u8, output: []u8) -> s64DigestStateBytes :: () -> usizeDigestStart :: (state: []u8) -> boolDigestUpdate :: (state: []u8, input: []u8) -> boolDigestFinish :: (state: []u8, output: []u8) -> boolDerivePassword :: (password: []u8, salt: []u8, output: []u8, operations: u64, memory: usize) -> boolCompatibility KDF for existing Argon2id-protected exports. The caller owns the container format and supplies its fixed, trusted work factors.
DevicePublic :: (seed: []u8, output: []u8) -> boolSignDevice :: (seed: []u8, input: []u8, output: []u8) -> booldaochi-client/events.zi
events
Remote sync events are transport independent. Hosts open the WebSocket and return one bounded JSON event through ReceiveEvent.
Import with #import "daochi_client/events".
EventKind :: enum { EVENT_INVALID = 0; EVENT_READY = 1; EVENT_CHANGED = 2; }RemoteEvent :: struct { kind: EventKind; server_version: s64; }ReceiveEvent :: #type (context: *void, url: string, token: string, output: []u8, status: *s32) -> boolEventUrl :: (server_url: string, output: []u8) -> boolParseRemoteEvent :: (json: string, account_id: string) -> RemoteEventWaitForEvent :: (client: Client, session: *Session, now: s64, receive_context: *void, receive: ReceiveEvent, event: *RemoteEvent) -> AuthResultdaochi-client/keys.zi
keys
Import with #import "daochi_client/keys".
ACCOUNT_PUBLIC_ID_HEX_SIZE :: 65An account's wire sizes, as hex text with room for a NUL.
ACCOUNT_PUBLIC_KEY_HEX_SIZE :: 2625ACCOUNT_PRIVATE_KEY_HEX_SIZE :: 5121ACCOUNT_SIGNATURE_HEX_SIZE :: 4841ACCOUNT_EXPORT_TEXT_SIZE :: 8200ACCOUNT_EXPORT_ENCRYPTED_TEXT_SIZE :: 17664ACCOUNT_PASSPHRASE_ITERATIONS :: 600000AccountKeys :: struct { public_id: [65]u8; public_key_hex: [2625]u8; private_key_hex: [5121]u8; }SecureRandom :: (output: *u8, length: s64) -> s32SecureWipe :: (output: *u8, length: s64)AccountPublicKeyBytes :: 1312ML-DSA-44's fixed sizes are part of the account format.
AccountPrivateKeyBytes :: 2560Validate an existing binary account without generating or replacing keys.
ValidateIdentity :: (account_id: []u8, public_key: []u8, private_key: []u8) -> boolValidate an existing binary account without generating or replacing keys. A private-key proof is required as well as the public-key account digest.
AccountValuesEqual :: (left: *AccountKeys, right: *AccountKeys) -> boolCreateAccountKeys :: (account: *AccountKeys) -> s32AccountSignatureBytes :: 2420AccountSigningKeyBytes :: 2560AccountBufferText :: (data: *u8, capacity: s64) -> stringCopySigningText :: (value: string, output: []u8) -> boolSignAccountKeysHex :: (account: *AccountKeys, message: *u8, message_length: s64, output: *u8, output_size: s64) -> s32SignAccountMessage :: (context: *void, message: string, output: []u8) -> boolSigns the exact login, registration, or transaction message bytes.
AccountTextAppend :: (output: *u8, capacity: s64, cursor: *s64, value: string) -> boolAccountTextAppendField :: (output: *u8, capacity: s64, cursor: *s64, field: *u8, field_capacity: s64) -> boolAccountKeysText :: (account: *AccountKeys, output: *u8, output_size: s64) -> s32AccountTrimValue :: (value: string) -> stringAccountStartsWith :: (value: string, prefix: string) -> boolAccountField :: (line: string, prefix: string, output: []u8) -> boolAccountJsonWhitespace :: (byte: u8) -> boolAccountJsonExportedKey :: (json: string, output: []u8) -> boolValidateAccountKeys :: (account: *AccountKeys) -> s32ParseAccountKeys :: (body: string, account: *AccountKeys) -> s32AccountEncryptedHeader :: "ksync-account-key-v2"AccountEncryptedSaltBytes :: 16AccountEncryptedNonceBytes :: 12AccountEncryptedTagBytes :: 16AccountDerivePassphraseKey :: (passphrase: string, salt: *u8, output: *u8, iterations: u64) -> boolAccountAppendHex :: (output: *u8, capacity: s64, cursor: *s64, bytes: *u8, byte_length: s64) -> boolAccountEncryptedLineValue :: (body: string, prefix: string) -> stringAccountParseIterations :: (text: string) -> u64AccountKeysEncryptedText :: (account: *AccountKeys, passphrase: string, output: *u8, output_capacity: s64) -> s32ParseEncryptedAccountKeys :: (body: string, passphrase: string, account: *AccountKeys) -> s32AccountCopyText :: (value: string, output: []u8) -> booldaochi-client/record.zi
record
Daochi encrypted record profile v1 ("daochi-record-v1"). The caller supplies the content key and the authoritative routing metadata; this module only seals. Keys and plaintext never leave the caller's buffers.
Import with #import "daochi_client/record".
RecordMetadata :: struct { account_id: string; app_id: string; collection: string; id: string; schema: string; }RecordNonceBytes :: 24RecordPlainMaximum :: 8388608RecordMetadataValid :: (metadata: RecordMetadata) -> boolBuildRecordAAD :: (metadata: RecordMetadata, output: []u8) -> s64{"account_id":…,"app_id":…,"collection":…,"id":…,"schema":…}; the length excludes the terminating NUL, or -1 when invalid or too large.
RecordEnvelopeBytes :: (metadata: RecordMetadata, key_id: string, plain_count: s64) -> s64Room for the sealed envelope of plain_count bytes, including its NUL.
SealRecord :: (metadata: RecordMetadata, key_id: string, key: []u8, plaintext: []u8, nonce: []u8, output: []u8) -> s64Seal with a fresh random nonce into the profile's sorted-key JSON envelope: {"aad":{…},"alg":"xchacha20poly1305","ciphertext":"…","key_id":"…", "nonce":"…","profile":"daochi-record-v1"}, base64url without padding. The nonce is also returned because record stores repeat it. Returns the envelope length without its NUL, or -1 with every output cleared.
RecordBase64 :: (input: []u8, output: []u8) -> s64Base64url without padding, as the profile encodes nonces and ciphertext. Returns the length without its NUL, or -1 when output is too small.
daochi-client/record_codec.zi
record_codec
Optional packed JSON record payloads. Hosts seal both the released JSON and this frame with independent nonces. Nodes serve the packed copy only to a reader that requests gzip-v1; older readers retain the released plaintext.
Import with #import "daochi_client/record_codec".
RecordPayloadMaximum :: 8388608RecordPackingHeader :: 12RecordPackingMagic :: "DAOGZIP1"RecordIsPacked :: (input: []u8) -> boolPackRecord :: (input: []u8, output: []u8) -> s64Zero means keep the identity representation; -1 means invalid bounds. The caller owns disjoint input/output buffers and caps output to input size.
PackedRecordSize :: (input: []u8, maximum: s64) -> s64Validate the authenticated frame's claimed length before any allocation.
UnpackRecord :: (input: []u8, output: []u8, maximum: s64) -> s64daochi-client/release.zi
release
Signed app releases, format "daochi-zib-release-v2". A release names an app, its sequence and version, the runtime and host APIs it needs, the artifacts that deliver it and the packages it depends on, all signed with Ed25519 by the key the publisher registered for the app. DaochiParseRelease checks the release's shape and signature; the caller checks its own policy: the app it expects, a runtime and API range it supports, and which dependencies it accepts. The signature check is a callback, so applications keep their own Ed25519 implementation.
Import with #import "daochi_client/release".
DAOCHI_RELEASE_ARTIFACTS :: 2DAOCHI_RELEASE_DEPENDENCIES :: 16DAOCHI_RELEASE_JSON_LIMIT :: 65536DaochiReleaseArtifact :: struct { variant: [32]u8; sha256: [65]u8; size: s64; }DaochiReleaseDependency :: struct { app_id: [64]u8; version: [32]u8; sha256: [65]u8; size: s64; }DaochiRelease :: struct { app_id: [64]u8; sequence: s64; version: [32]u8; runtime: [32]u8; host_api: s64; module_api: s64; data_schema: s64; key_id: [64]u8; artifacts: [2]DaochiReleaseArtifact; artifact_count: s32; dependencies: [16]DaochiReleaseDependency; dependency_count: s32; valid: bool; }DaochiReleaseVerify :: #type (signature: *u8, public_key: *u8, message: *u8, length: usize) -> boolDaochiVersionValid :: (version: string) -> boolA dotted version of three numbers without leading zeros, such as 2.1.28.
DaochiIdentifierValid :: (value: string) -> boolLowercase letters and digits, then also ".", "_" and "-"; at most 63 bytes.
DaochiHashValid :: (hash: string) -> bool64 lowercase hex digits.
DaochiCompare :: (left: string, right: string) -> s32Byte order, shorter first.
DaochiSha256Matches :: (bytes: string, sha256: string) -> boolWhether bytes hash to sha256, 64 lowercase hex digits.
ReleaseField :: (json: string, at: s32, key: string, output: []u8) -> stringReleaseNumber :: (json: string, at: s32, key: string) -> s64ReleaseCopy :: (value: string, output: []u8)DaochiParseRelease :: (json: string, public_hex: string, maximum_size: s64, verify: DaochiReleaseVerify) -> DaochiReleaseParses json and checks its signature under public_hex, the app key's 64 hex digits. Each artifact and dependency is at least 8 bytes and at most maximum_size. The result is valid only when everything checks out.
daochi-client/entity.zi
entity
Entity data: the account's own photos, levels and other things, kept once and shared by every app of the account (Daochi docs/entity-data.md). All keys come from the account private key bytes, so every app derives the same ones. Keys and plaintext never leave the caller's buffers.
Import with #import "daochi_client/entity".
ENTITY_KEY_BYTES :: 32ENTITY_RECORD_KEY_ID :: "entity-v1"ENTITY_KIND_MAX :: 48ENTITY_HASH_HEX_BYTES :: 64Content is sealed in chunks of at most 8 MiB, below the node's 16 MiB
ENTITY_CHUNK_BYTES :: 8388608Content is sealed in chunks of at most 8 MiB, below the node's 16 MiB blob limit; a record lists their addresses in order.
ENTITY_CHUNK_MAX :: 1000000EntityKindValid :: (kind: string) -> boolEntityRecordKey :: (private_key: []u8, kind: string, output: []u8) -> boolThe record key of kind: HMAC-SHA256(private_key, "daochi-entity-v1/records/" + kind). Records use key ID entity-v1.
EntityBlobKey :: (private_key: []u8, kind: string, hash: string, output: []u8) -> boolThe key of one blob: HMAC-SHA256(HMAC-SHA256(private_key, "daochi-entity-v1/blobs/" + kind), hash), where hash is the lowercase SHA-256 hex of the plain bytes. The same content always gets the same key.
EntityChunkKey :: (private_key: []u8, kind: string, hash: string, index: s64, output: []u8) -> boolThe key of chunk index of the content named hash: HMAC-SHA256(EntityBlobKey(private_key, kind, hash), "chunk/" + index).
SealEntityChunk :: (private_key: []u8, kind: string, hash: string, index: s64, plain: []u8, output: []u8) -> s64Seal chunk index (at most ENTITY_CHUNK_BYTES plain bytes) of the content named hash, the lowercase SHA-256 hex of the whole plain content, into output, which needs plain.count + 16 bytes. The node stores the sealed bytes at their own SHA-256. Returns the sealed length, or -1 with output cleared.
OpenEntityChunk :: (private_key: []u8, kind: string, hash: string, index: s64, sealed: []u8, output: []u8) -> s64Open chunk index of the content named hash. Returns the plain length, or -1 with output cleared when it does not authenticate. The reader checks the SHA-256 of all chunks together against hash.
ENTITY_RECORD_APP :: "entity"ENTITY_RECORD_SCHEMA_MAX :: 128ENTITY_RECORD_ID_MAX :: 128ENTITY_RECORD_NONCE_BYTES :: 24ENTITY_RECORD_TEXT_MAX :: 262144The largest plain text whose sync text stays within that: the envelope's
ENTITY_RECORD_PLAIN_MAX :: 146432What a record's associated data names; the collection is
EntityRecordMetadata :: struct { account_id: string; kind: string; id: string; schema: string; }What a record's associated data names; the collection is private.entity.v1.<kind>.
EntityRecordMetadataValid :: (metadata: EntityRecordMetadata) -> boolEntityRecordCollection :: (kind: string, output: []u8) -> stringprivate.entity.v1.<kind> into output; "" for an invalid kind.
EntityRecordKind :: (collection: string) -> stringThe kind of an entity collection, private.entity.v1.<kind>; "" otherwise.
EntityRecordNonceText :: (nonce: []u8, output: []u8) -> stringThe nonce as the sync request carries it, base64url (32 digits).
SealEntityRecord :: (private_key: []u8, metadata: EntityRecordMetadata, plain: []u8, nonce: []u8, sealed: []u8, output: []u8) -> s64Seals plain (at most ENTITY_RECORD_PLAIN_MAX bytes) under the record key of its kind with nonce, 24 fresh random bytes, and writes the record's sync ciphertext text, the base64url envelope, into output. sealed is scratch of plain.count + 16 bytes. Returns the text's length, or -1 with output cleared.
EntityRecordEnvelope :: (text: string, output: []u8) -> s64The envelope JSON of a record's sync ciphertext text, decoded into output (3/4 of text.count); its length, or -1.
OpenEntityRecord :: (private_key: []u8, metadata: EntityRecordMetadata, envelope: string, schema: []u8, output: []u8) -> s64Opens a record's envelope (EntityRecordEnvelope): it must name this account, the entity app, the kind's collection and the record id of metadata (whose schema is ignored), key entity-v1, and authenticate under the kind's record key. The record's schema goes into schema and its plain text into output, which needs room for the sealed bytes (16 more than the plain text); returns the plain length, or -1 with output cleared.
daochi-client/rewards.zi
rewards
Rewards an account claims from its node, such as a game's secret chest. The node keeps the catalog and gives each reward to an account once: a client names only the reward, never an amount, and reads back whether this claim credited it. Claiming again is safe and credits nothing.
Import with #import "daochi_client/rewards".
REWARDS_PATH :: "/api/v1/rewards"RewardClaim :: struct { units: s64; balance: s64; credited: bool; }RewardNameValid :: (reward: string) -> boolA reward name as the node accepts one: 1 to 64 letters, digits, '.', '_', ':' or '-'.
BuildRewardBody :: (reward: string, output: []u8) -> boolThe claim's request body, {"reward":"…"}; false for a name the node refuses.
ParseRewardClaim :: (json: string, claim: *RewardClaim) -> boolThe node's answer to a claim, {"reward","units","credited","balance"}.
ClaimReward :: (client: Client, session: *Session, now: s64, reward: string, output: []u8, status: *s32) -> AuthResultClaims a reward, waiting for the node's answer in output.
PendingReward :: struct { request: PendingRequest; body: [128]u8; answer: [1024]u8; }A claim advanced without blocking the host's event loop, with its own request body and the node's answer.
BeginRewardClaim :: (pending: *PendingReward, client: Client, session: *Session, now: s64, transport: AsyncTransport, reward: string) -> boolStarts a claim without blocking; false when the reward name is refused.
PollRewardClaim :: (pending: *PendingReward, claim: *RewardClaim) -> boolAdvances a claim; true once it has finished. A finished claim the node answered fills `claim` and reports whether it credited the account.
daochi-client/sync.zi
sync
Protocol v6 sync orchestration. The host provisions and stores device keys; this module signs the Daochi wire messages and sends them through Client.
Import with #import "daochi_client/sync".
RandomNonce :: #type (context: *void, output: []u8) -> boolDevice :: struct { key_id: string; public_key_hex: string; signing_context: *void; signer: SignMessage; }RegistrationCurrent :: (client: Client, session: *Session, device: Device, now: s64) -> boolRememberRegistration :: (client: Client, session: *Session, device: Device, now: s64)MissingRegisteredDevice :: (status: s32, response: string) -> boolReplayedSyncTransaction :: (status: s32, response: string) -> boolWriteRegistration :: (registration: *Registration, output: []u8) -> boolApps may persist this public hint alongside their bearer session. Cache readers validate every scope again before it can skip a registration.
ReadRegistration :: (text: string, registration: *Registration) -> boolSignatureHex :: (value: string, length: s64) -> boolServerNow :: (session: *Session, now: s64) -> s64CreateNonce :: (random_context: *void, random: RandomNonce, output: []u8) -> boolRegisterDevice :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce) -> AuthResultSync :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce, body: string, output: []u8, status: *s32) -> AuthResultPrepareDeviceRegistration :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce, body: []u8) -> AuthResultShared request preparation for blocking and frame-driven transports.
PrepareSyncHeader :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce, body: string, transaction_header: []u8) -> AuthResultPrepareRequestHeader :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce, method: string, path: string, body: string, transaction_header: []u8) -> AuthResultBinary bodies and empty GET/HEAD bodies retain their exact byte length. Both platform hosts and sync orchestration use the same signed preparation.
SignTransactionHeader :: (client: Client, device: Device, transaction: SyncTransaction, method: string, path: string, transaction_header: []u8) -> AuthResultHosts that stream and hash a body themselves supply the finished digest and expiration. The client owns the account/device signatures and wire format.
daochi-client/transaction.zi
transaction
Protocol v6 device registration and signed sync transaction wire formats.
Import with #import "daochi_client/transaction".
DeviceRegistration :: struct { account_id: string; app_id: string; device_key_id: string; client_id: string; public_key_hex: string; nonce: string; expires_at: s64; }SyncTransaction :: struct { account_id: string; app_id: string; device_key_id: string; tx_id: string; nonce: string; body_sha256_hex: string; expires_at: s64; }PlainText :: (value: string) -> boolHexText :: (value: string, length: s64) -> boolWriteDecimal :: (writer: Writer, value: s64) -> WriterRegistrationValid :: (request: DeviceRegistration) -> boolTransactionValid :: (transaction: SyncTransaction) -> boolBuildDeviceRegistrationMessage :: (request: DeviceRegistration, output: []u8) -> boolBuildDeviceRegistrationBody :: (request: DeviceRegistration, account_signature_hex: string, output: []u8) -> boolBuildSyncTransactionMessage :: (transaction: SyncTransaction, output: []u8) -> boolRequestValid :: (method: string, path: string) -> boolBuildTransactionMessage :: (transaction: SyncTransaction, method: string, path: string, output: []u8) -> boolBuildSyncTransactionHeader :: (transaction: SyncTransaction, account_signature_hex: string, device_signature_hex: string, output: []u8) -> boolBuildTransactionHeader :: (transaction: SyncTransaction, method: string, path: string, account_signature_hex: string, device_signature_hex: string, output: []u8) -> booldaochi-client/url.zi
url
Daochi server URLs use HTTPS except for loopback and private LAN hosts.
Import with #import "daochi_client/url".
StartsWith :: (value: string, prefix: string) -> boolHostBoundary :: (authority: string, index: s64) -> boolPrivateIpv4Authority :: (authority: string) -> boolLocalAuthority :: (authority: string) -> boolSafeUrlBytes :: (value: string) -> boolUrlValid :: (url: string) -> boolNormalizeUrl :: (input: string, output: []u8) -> boolWriteUrlText :: (output: []u8, cursor: s64, value: string) -> s64JoinUrl :: (base_url: string, path: string, output: []u8) -> boolJoinWebSocketUrl :: (base_url: string, path: string, output: []u8) -> booldaochi-client/wire.zi
wire
Daochi client wire values shared by apps. The caller owns account keys, hashing, HTTP transport, and the destination buffers.
Import with #import "daochi_client/wire".
Writer :: struct { output: []u8; cursor: s64; valid: bool; }WriteByte :: (writer: Writer, byte: u8) -> WriterWriteText :: (writer: Writer, value: string) -> WriterHexDigit :: (byte: u8) -> boolHex64 :: (value: string) -> boolHexUpper :: (value: u8) -> u8AppendJsonQuoted :: (writer: Writer, value: string) -> WriterBuildChallengePath :: (account_id: string, output: []u8) -> boolWriteLoginBody :: (account_id: string, client_id: string, public_key: string, restore: bool, output: []u8) -> boolBuildLoginBody :: (account_id: string, client_id: string, public_key: string, output: []u8) -> boolBuildRestoreLoginBody :: (account_id: string, client_id: string, public_key: string, output: []u8) -> boolA login that also asks the node to bring back this key's deleted account while the node still holds it, before any new data.
BuildLoginMessageHash :: (body_sha256_hex: string, nonce_hex: string, output: []u8) -> boolThe caller hashes the exact body bytes. This function fixes the signed message layout and prevents an app from choosing a different path or context.
BuildDeleteMessageHash :: (body_sha256_hex: string, nonce_hex: string, output: []u8) -> bool