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 (Daochi docs/entity-data.md), and entity records sealed and opened as daochi-record-v1 envelopes 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 by async_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/authorization.zi

authorization

Owner-signed authorization wire values. Apps own storage and approval UI.

Import with #import "daochi_client/authorization".

GrantScope :: struct { collection: string; visibility: string; read: bool; write: bool; key_id: string; key_envelope: string; }
Grant :: struct { version: s32; account_id: string; grant_id: string; request_id: string; app_id: string; client_id: string; node_id: string; audience: string; signing_key: string; encryption_key: string; issued_at: s64; not_before: s64; expires_at: s64; nonce: string; bot_id: s64; telegram_id: s64; scopes: []GrantScope; signature: string; }
ScopeStorage :: struct { collection: [65]u8; visibility: [8]u8; read: bool; write: bool; key_id: [129]u8; key_envelope: [16385]u8; }
GrantStorage :: struct { version: s32; account_id: [65]u8; grant_id: [33]u8; request_id: [33]u8; app_id: [65]u8; client_id: [65]u8; node_id: [65]u8; audience: [513]u8; signing_key: [65]u8; encryption_key: [2369]u8; issued_at: s64; not_before: s64; expires_at: s64; nonce: [33]u8; bot_id: s64; telegram_id: s64; scope_count: s32; scopes: [16]ScopeStorage; signature: [4841]u8; }
RequestedScope :: struct { collection: string; read: bool; write: bool; }
SessionChallenge :: struct { grant_id: [33]u8; challenge: [33]u8; node_id: [65]u8; audience: [513]u8; expires_at: s64; }
DelegateSession :: struct { session_id: [33]u8; grant_id: [33]u8; challenge: [33]u8; expires_at: s64; }
OwnerRendezvous :: struct { request_id: [33]u8; grant_id: [33]u8; account_id: [65]u8; app_id: [65]u8; node_id: [65]u8; audience: [513]u8; expires_at: s64; bot_id: s64; telegram_id: s64; client_id: [65]u8; signing_key: [65]u8; encryption_key: [2369]u8; challenge: [33]u8; status: [17]u8; scope_count: s32; scopes: [16]ScopeStorage; }
PublicRendezvous :: struct { request_id: [33]u8; grant_id: [33]u8; app_id: [65]u8; node_id: [65]u8; audience: [513]u8; expires_at: s64; challenge: [33]u8; status: [17]u8; }
LowerHex :: (value: string, size: s64) -> bool
LineText :: (value: string, limit: s64) -> bool
Identifier :: (value: string, minimum: s64, maximum: s64) -> bool
Namespace :: (value: string) -> bool
Collection :: (value: string) -> bool
CompareText :: (left: string, right: string) -> s32
GrantValid :: (grant: Grant, now: s64) -> bool
ViewGrant :: (storage: *GrantStorage, scopes: []GrantScope, grant: *Grant) -> bool
Line :: (writer: Writer, value: string) -> Writer
NumberLine :: (writer: Writer, value: s64) -> Writer
BuildGrantMessage :: (grant: Grant, output: []u8) -> bool
JsonTextField :: (writer: Writer, name: string, value: string) -> Writer
JsonNumberField :: (writer: Writer, name: string, value: s64) -> Writer
BuildGrantBody :: (grant: Grant, output: []u8) -> bool
ObjectFields :: (json: string, at: s32, names: []string, positions: []s32) -> bool

Known, unique decoded member names. Escaped aliases cannot bypass duplicates.

ObjectFieldsMinimum :: (json: string, at: s32, names: []string, positions: []s32, required: s32) -> bool
WholeJson :: (json: string) -> bool
ReadInteger :: (json: string, at: s32, output: *s64) -> bool
ReadBool :: (json: string, at: s32, output: *bool) -> bool
ReadGrantAt :: (json: string, at: s32, storage: *GrantStorage) -> bool
VerifyGrantSignature :: (grant: Grant, owner_public_key: string, now: s64) -> bool
BuildAuthorizationRequest :: (app_id: string, telegram: bool, scopes: []RequestedScope, output: []u8) -> bool
BuildIDBody :: (name: string, identifier: string, app_id: string, output: []u8) -> bool
HashText :: (value: string, output: []u8) -> bool
ReadChallenge :: (json: string, output: *SessionChallenge) -> bool
ReadCredentialAt :: (json: string, at: s32, output: *DelegateSession) -> bool
ReadOwnerRendezvous :: (json: string, output: *OwnerRendezvous) -> bool
BuildClaimMessage :: (pending: *PublicRendezvous, client_id: string, signing_key: string, encryption_key: string, init_digest: string, output: []u8) -> bool
BuildClaimBody :: (request_id: string, client_id: string, signing_key: string, encryption_key: string, raw_init_data: string, proof: string, output: []u8) -> bool
ReadPublicRendezvous :: (json: string, output: *PublicRendezvous) -> bool

Source ↗

daochi-client/authorization_owner.zi

authorization_owner

Owner operations retain protocol v6 ML-DSA and registered-device signatures.

Import with #import "daochi_client/authorization_owner".

OwnerAuthorizationRoute :: (path: string) -> bool
BuildOwnerMessage :: (transaction: SyncTransaction, path: string, output: []u8) -> bool
BuildOwnerHeader :: (transaction: SyncTransaction, path: string, account_signature: string, device_signature: string, output: []u8) -> bool
PrepareOwnerAuthorizationHeader :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce, path: string, body: string, output: []u8) -> AuthResult
OwnerAuthorizationRequest :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce, path: string, body: string, output: []u8, status: *s32) -> AuthResult
PrepareGrantApproval :: (client: Client, grant: Grant, output: []u8) -> AuthResult

Approval signs the immutable grant once, rather than signing each mutation.

Source ↗

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) -> s64
RandomID :: #type (context: *void, output: []u8) -> bool
DelegateClient :: 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) -> bool
CurrentDelegateTime :: (client: DelegateClient) -> s64
DelegateExpiryCurrent :: (client: DelegateClient, expires_at: s64) -> bool
SessionValid :: (client: DelegateClient, session: *DelegateSession, now: s64) -> bool
ClearDelegateSession :: (session: *DelegateSession)
BuildSessionMessage :: (challenge: *SessionChallenge, output: []u8) -> bool
PrepareSessionBody :: (client: DelegateClient, challenge: *SessionChallenge, now: s64, output: []u8) -> bool
AcceptSessionAnswer :: (client: DelegateClient, status: s32, json: string, now: s64, session: *DelegateSession) -> AuthResult
RequestRoute :: (method: string, path: string, query: string) -> bool
BuildProofMessage :: (proof: RequestProof, output: []u8) -> bool
BuildProofHeader :: (proof: RequestProof, signature: string, output: []u8) -> bool
ScopeIndex :: (client: DelegateClient, collection: string, write: bool) -> s32
BuildDelegatedSyncBody :: (client: DelegateClient, collection: string, read: bool, since_version: s64, limit: s32, records: []DelegatedRecord, output: []u8) -> bool
DelegatedSyncBodyValid :: (client: DelegateClient, body: string) -> bool

Reject 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) -> bool
ResponseMatchesRequest :: (client: DelegateClient, request_body: string, response: string) -> bool
PrepareDelegateHeaders :: (client: DelegateClient, session: *DelegateSession, now: s64, method: string, path: string, query: string, body: string, proof_header: []u8, credential: []u8) -> AuthResult
RenewDelegateSession :: (client: DelegateClient, session: *DelegateSession, now: s64, response: []u8) -> AuthResult
DelegateRequest :: (client: DelegateClient, session: *DelegateSession, now: s64, body: string, output: []u8, status: *s32) -> AuthResult

Source ↗

daochi-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) -> bool
BeginDelegateSession :: (pending: *PendingDelegate, client: DelegateClient, session: *DelegateSession, now: s64, transport: AsyncTransport, output: []u8) -> bool
BeginDelegateRequest :: (pending: *PendingDelegate, client: DelegateClient, session: *DelegateSession, now: s64, transport: AsyncTransport, body: string, output: []u8) -> bool
PollDelegate :: (pending: *PendingDelegate) -> bool
CancelDelegate :: (pending: *PendingDelegate)

Source ↗

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) -> bool
EncryptionPublicHex :: (keys: *EncryptionKeys, output: []u8) -> bool
ClearEncryptionKeys :: (keys: *EncryptionKeys)
BuildEnvelopeAAD :: (grant: Grant, scope: GrantScope, output: []u8) -> bool
SealCollectionKey :: (grant: Grant, scope: GrantScope, collection_key: []u8, output: []u8) -> bool
OpenCollectionKey :: (grant: Grant, scope: GrantScope, keys: *EncryptionKeys, output: []u8) -> bool

Source ↗

daochi-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) -> bool
PrepareDelete :: (client: Client, challenge: string, body: []u8, signature: []u8) -> AuthResult

Shared signed deletion preparation for blocking and polled hosts.

DeleteAccount :: (client: Client) -> AuthResult

Source ↗

daochi-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) -> bool
PollDeletion :: (pending: *PendingDeletion) -> bool
CancelDeletion :: (pending: *PendingDeletion)

Source ↗

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) -> bool
PollTransfer :: #type (context: *void, status: *s32) -> s32
CancelTransfer :: #type (context: *void)
TransferFailure :: #type (context: *void) -> s32
TRANSFER_FAILURE_TRANSPORT :: 1
TRANSFER_FAILURE_CAPACITY :: 2
AsyncTransport :: 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) -> bool
StartChallenge :: (pending: *PendingRequest)
StartLogin :: (pending: *PendingRequest)
StartBearer :: (pending: *PendingRequest)
InitializeRequest :: (pending: *PendingRequest, client: Client, session: *Session, now: s64, transport: AsyncTransport) -> bool
BeginSession :: (pending: *PendingRequest, client: Client, session: *Session, now: s64, transport: AsyncTransport) -> bool
BeginRequest :: (pending: *PendingRequest, client: Client, session: *Session, now: s64, transport: AsyncTransport, method: string, path: string, body: string, output: []u8, extra_headers: []HttpHeader) -> bool
PollRequest :: (pending: *PendingRequest) -> bool
CancelRequest :: (pending: *PendingRequest)

Source ↗

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) -> bool
StartSyncTransaction :: (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) -> bool
PollSync :: (pending: *PendingSync) -> bool
CancelSync :: (pending: *PendingSync)

Source ↗

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) -> bool
PrepareSyncBody :: #type (context: *void, maximum_records: s32) -> string
SaveSyncState :: #type (context: *void, state: *SyncPageState) -> bool
AcceptSyncPage :: #type (context: *void, response: string, next: *SyncPageState) -> bool
SyncClock :: #type (context: *void) -> s64
SyncFlow :: 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) -> string
SyncVersion :: (text: string, at: s32, value: *s64) -> bool
ReadRecordCursor :: (text: string, at: s32, out: *RecordCursor) -> bool
CompareSyncText :: (left: string, right: string) -> s32
RecordCursorAfter :: (next: *RecordCursor, previous: *RecordCursor) -> bool
RestartStaleSyncPage :: (body: string, state: *SyncPageState) -> bool

A 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) -> bool

Output 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) -> bool

No cursor may advance until the host has committed this response. The next state is separate so a rejected or interrupted merge cannot skip records.

Source ↗

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) -> bool
SignMessage :: #type (context: *void, message: string, output: []u8) -> bool
ValidJson :: (json: string) -> bool
FieldText :: (json: string, name: string, output: []u8) -> bool
FieldNumber :: (json: string, name: string, fallback: s64) -> s64
HexText :: (text: string, digits: s64) -> bool
PrepareLogin :: (identity: Identity, challenge_json: string, digest: DigestBody, signer: SignMessage, body: []u8, signature: []u8) -> AuthResult
AcceptLogin :: (status: s32, response_json: string, session: *Session) -> AuthResult

Source ↗

daochi-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) -> bool
Client :: struct { server_url: string; app_id: string; identity: Identity; digest: DigestBody; signer: SignMessage; send: SendRequest; transport_context: *void; }
FetchChallengeJson :: (client: Client, output: []u8) -> AuthResult
Login :: (client: Client, session: *Session) -> AuthResult
MethodValid :: (method: string) -> bool
PathValid :: (path: string) -> bool
TokenValid :: (session: *Session, now: s64) -> bool
SetExpiry :: (session: *Session, now: s64)
EnsureSession :: (client: Client, session: *Session, now: s64) -> AuthResult
ClearSession :: (session: *Session)
BearerRequestWithHeaders :: (client: Client, session: *Session, now: s64, method: string, path: string, body: string, output: []u8, status: *s32, extra_headers: []HttpHeader) -> AuthResult

A 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) -> AuthResult

Source ↗

daochi-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) -> s32
HexToBytes :: (hex: string, output: *u8, output_size: s64) -> s32
Sha256Hex :: (data: *u8, length: s64, output: *u8)
HmacSha256 :: (key: *u8, key_length: s64, data: *u8, data_length: s64, output: *u8) -> s32
HmacSha256Text :: (key: *u8, key_length: s64, data: string, output: *u8) -> s32
Pbkdf2Hmac :: (password: *u8, password_length: s64, password_text: string, use_text: bool, data: *u8, data_length: s64, output: *u8) -> s32

PBKDF2 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) -> s32
Pbkdf2Sha256 :: (password: *u8, password_length: s64, salt: *u8, salt_length: s64, iterations: u64, output: *u8) -> s32
Pbkdf2Sha256Text :: (password: string, salt: *u8, salt_length: s64, iterations: u64, output: *u8) -> s32
Poly1305State :: struct { r: [5]u64; scaled: [4]u64; h: [5]u64; }
Poly1305Load32 :: (bytes: *u8) -> u32
Poly1305Store32 :: (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 :: 64

RFC 8439 ChaCha20-Poly1305 for encrypted records and key files.

ChaChaTagBytes :: 16
ChaChaLengthValid :: (length: s64) -> bool
ChaChaRotateLeft :: (value: u32, bits: u32) -> u32
ChaChaQuarterRound :: (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) -> s32
ChaCha20Poly1305Open :: (key: *u8, nonce: *u8, sealed: *u8, sealed_length: s64, aad: *u8, aad_length: s64, plain: *u8, plain_capacity: s64) -> s32
HChaCha20 :: (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) -> s32

XChaCha20-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) -> s32

Source ↗

daochi-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 :: () -> bool
Clear :: (bytes: []u8)
RandomBytes :: (bytes: []u8) -> bool
Digest :: (input: []u8, output: []u8) -> bool
HmacSha256 :: (input: []u8, key: []u8, output: []u8) -> bool
Seal :: (input: []u8, aad: []u8, key: []u8, nonce: []u8, output: []u8) -> s64
Open :: (sealed: []u8, aad: []u8, key: []u8, nonce: []u8, output: []u8) -> s64
DigestStateBytes :: () -> usize
DigestStart :: (state: []u8) -> bool
DigestUpdate :: (state: []u8, input: []u8) -> bool
DigestFinish :: (state: []u8, output: []u8) -> bool
DerivePassword :: (password: []u8, salt: []u8, output: []u8, operations: u64, memory: usize) -> bool

Compatibility KDF for existing Argon2id-protected exports. The caller owns the container format and supplies its fixed, trusted work factors.

DevicePublic :: (seed: []u8, output: []u8) -> bool
SignDevice :: (seed: []u8, input: []u8, output: []u8) -> bool

Source ↗

daochi-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) -> bool
EventUrl :: (server_url: string, output: []u8) -> bool
ParseRemoteEvent :: (json: string, account_id: string) -> RemoteEvent
WaitForEvent :: (client: Client, session: *Session, now: s64, receive_context: *void, receive: ReceiveEvent, event: *RemoteEvent) -> AuthResult

Source ↗

daochi-client/keys.zi

keys

Import with #import "daochi_client/keys".

ACCOUNT_PUBLIC_ID_HEX_SIZE :: 65

An account's wire sizes, as hex text with room for a NUL.

ACCOUNT_PUBLIC_KEY_HEX_SIZE :: 2625
ACCOUNT_PRIVATE_KEY_HEX_SIZE :: 5121
ACCOUNT_SIGNATURE_HEX_SIZE :: 4841
ACCOUNT_EXPORT_TEXT_SIZE :: 8200
ACCOUNT_EXPORT_ENCRYPTED_TEXT_SIZE :: 17664
ACCOUNT_PASSPHRASE_ITERATIONS :: 600000
AccountKeys :: struct { public_id: [65]u8; public_key_hex: [2625]u8; private_key_hex: [5121]u8; }
SecureRandom :: (output: *u8, length: s64) -> s32
SecureWipe :: (output: *u8, length: s64)
AccountPublicKeyBytes :: 1312

ML-DSA-44's fixed sizes are part of the account format.

AccountPrivateKeyBytes :: 2560

Validate an existing binary account without generating or replacing keys.

ValidateIdentity :: (account_id: []u8, public_key: []u8, private_key: []u8) -> bool

Validate 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) -> bool
CreateAccountKeys :: (account: *AccountKeys) -> s32
AccountSignatureBytes :: 2420
AccountSigningKeyBytes :: 2560
AccountBufferText :: (data: *u8, capacity: s64) -> string
CopySigningText :: (value: string, output: []u8) -> bool
SignAccountKeysHex :: (account: *AccountKeys, message: *u8, message_length: s64, output: *u8, output_size: s64) -> s32
SignAccountMessage :: (context: *void, message: string, output: []u8) -> bool

Signs the exact login, registration, or transaction message bytes.

AccountTextAppend :: (output: *u8, capacity: s64, cursor: *s64, value: string) -> bool
AccountTextAppendField :: (output: *u8, capacity: s64, cursor: *s64, field: *u8, field_capacity: s64) -> bool
AccountKeysText :: (account: *AccountKeys, output: *u8, output_size: s64) -> s32
AccountTrimValue :: (value: string) -> string
AccountStartsWith :: (value: string, prefix: string) -> bool
AccountField :: (line: string, prefix: string, output: []u8) -> bool
AccountJsonWhitespace :: (byte: u8) -> bool
AccountJsonExportedKey :: (json: string, output: []u8) -> bool
ValidateAccountKeys :: (account: *AccountKeys) -> s32
ParseAccountKeys :: (body: string, account: *AccountKeys) -> s32
AccountEncryptedHeader :: "ksync-account-key-v2"
AccountEncryptedSaltBytes :: 16
AccountEncryptedNonceBytes :: 12
AccountEncryptedTagBytes :: 16
AccountDerivePassphraseKey :: (passphrase: string, salt: *u8, output: *u8, iterations: u64) -> bool
AccountAppendHex :: (output: *u8, capacity: s64, cursor: *s64, bytes: *u8, byte_length: s64) -> bool
AccountEncryptedLineValue :: (body: string, prefix: string) -> string
AccountParseIterations :: (text: string) -> u64
AccountKeysEncryptedText :: (account: *AccountKeys, passphrase: string, output: *u8, output_capacity: s64) -> s32
ParseEncryptedAccountKeys :: (body: string, passphrase: string, account: *AccountKeys) -> s32
AccountCopyText :: (value: string, output: []u8) -> bool

Source ↗

daochi-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 :: 24
RecordPlainMaximum :: 8388608
RecordMetadataValid :: (metadata: RecordMetadata) -> bool
BuildRecordAAD :: (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) -> s64

Room 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) -> s64

Seal 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) -> s64

Base64url without padding, as the profile encodes nonces and ciphertext. Returns the length without its NUL, or -1 when output is too small.

Source ↗

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 :: 8388608
RecordPackingHeader :: 12
RecordPackingMagic :: "DAOGZIP1"
RecordIsPacked :: (input: []u8) -> bool
PackRecord :: (input: []u8, output: []u8) -> s64

Zero 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) -> s64

Validate the authenticated frame's claimed length before any allocation.

UnpackRecord :: (input: []u8, output: []u8, maximum: s64) -> s64

Source ↗

daochi-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 :: 2
DAOCHI_RELEASE_DEPENDENCIES :: 16
DAOCHI_RELEASE_JSON_LIMIT :: 65536
DaochiReleaseArtifact :: 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) -> bool
DaochiVersionValid :: (version: string) -> bool

A dotted version of three numbers without leading zeros, such as 2.1.28.

DaochiIdentifierValid :: (value: string) -> bool

Lowercase letters and digits, then also ".", "_" and "-"; at most 63 bytes.

DaochiHashValid :: (hash: string) -> bool

64 lowercase hex digits.

DaochiCompare :: (left: string, right: string) -> s32

Byte order, shorter first.

DaochiSha256Matches :: (bytes: string, sha256: string) -> bool

Whether bytes hash to sha256, 64 lowercase hex digits.

ReleaseField :: (json: string, at: s32, key: string, output: []u8) -> string
ReleaseNumber :: (json: string, at: s32, key: string) -> s64
ReleaseCopy :: (value: string, output: []u8)
DaochiParseRelease :: (json: string, public_hex: string, maximum_size: s64, verify: DaochiReleaseVerify) -> DaochiRelease

Parses 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.

Source ↗

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 :: 32
ENTITY_RECORD_KEY_ID :: "entity-v1"
ENTITY_KIND_MAX :: 48
ENTITY_HASH_HEX_BYTES :: 64

Content is sealed in chunks of at most 8 MiB, below the node's 16 MiB

ENTITY_CHUNK_BYTES :: 8388608

Content 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 :: 1000000
EntityKindValid :: (kind: string) -> bool
EntityRecordKey :: (private_key: []u8, kind: string, output: []u8) -> bool

The 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) -> bool

The 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) -> bool

The 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) -> s64

Seal 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) -> s64

Open 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 :: 128
ENTITY_RECORD_ID_MAX :: 128
ENTITY_RECORD_NONCE_BYTES :: 24
ENTITY_RECORD_TEXT_MAX :: 262144

The largest plain text whose sync text stays within that: the envelope's

ENTITY_RECORD_PLAIN_MAX :: 146432

What 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) -> bool
EntityRecordCollection :: (kind: string, output: []u8) -> string

private.entity.v1.<kind> into output; "" for an invalid kind.

EntityRecordKind :: (collection: string) -> string

The kind of an entity collection, private.entity.v1.<kind>; "" otherwise.

EntityRecordNonceText :: (nonce: []u8, output: []u8) -> string

The nonce as the sync request carries it, base64url (32 digits).

SealEntityRecord :: (private_key: []u8, metadata: EntityRecordMetadata, plain: []u8, nonce: []u8, sealed: []u8, output: []u8) -> s64

Seals 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) -> s64

The 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) -> s64

Opens 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.

Source ↗

daochi-client/social.zi

social

Account and friend operations shared by every Daochi application.

Import with #import "daochi_client/social".

SpaceByte :: (byte: u8) -> bool
LowerByte :: (byte: u8) -> u8
NormalizeFriendTarget :: (target: string, output: []u8) -> bool
BuildFriendRequestBody :: (target: string, output: []u8) -> bool
HexUpper :: (digit: u8) -> u8
AppendQuery :: (writer: Writer, value: string) -> Writer
BuildFriendStatsPath :: (app_id: string, practice: string, metric: string, output: []u8) -> bool
PathSegment :: (value: string) -> bool
BuildFriendActionPath :: (request_id: string, action: string, output: []u8) -> bool
BuildFriendPath :: (friend_id: string, output: []u8) -> bool
BuildAliasBody :: (account_id: string, alias: string, output: []u8) -> bool
SendFriendRequest :: (client: Client, session: *Session, now: s64, target: string, output: []u8, status: *s32) -> AuthResult
FriendAction :: (client: Client, session: *Session, now: s64, request_id: string, action: string, output: []u8, status: *s32) -> AuthResult
RemoveFriend :: (client: Client, session: *Session, now: s64, friend_id: string, output: []u8, status: *s32) -> AuthResult
FriendRequests :: (client: Client, session: *Session, now: s64, output: []u8, status: *s32) -> AuthResult
Friends :: (client: Client, session: *Session, now: s64, output: []u8, status: *s32) -> AuthResult
FriendStats :: (client: Client, session: *Session, now: s64, practice: string, metric: string, output: []u8, status: *s32) -> AuthResult
RegisterAlias :: (client: Client, session: *Session, now: s64, alias: string, output: []u8, status: *s32) -> AuthResult

Source ↗

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) -> bool

A reward name as the node accepts one: 1 to 64 letters, digits, '.', '_', ':' or '-'.

BuildRewardBody :: (reward: string, output: []u8) -> bool

The claim's request body, {"reward":"…"}; false for a name the node refuses.

ParseRewardClaim :: (json: string, claim: *RewardClaim) -> bool

The node's answer to a claim, {"reward","units","credited","balance"}.

ClaimReward :: (client: Client, session: *Session, now: s64, reward: string, output: []u8, status: *s32) -> AuthResult

Claims 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) -> bool

Starts a claim without blocking; false when the reward name is refused.

PollRewardClaim :: (pending: *PendingReward, claim: *RewardClaim) -> bool

Advances a claim; true once it has finished. A finished claim the node answered fills `claim` and reports whether it credited the account.

Source ↗

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) -> bool
Device :: struct { key_id: string; public_key_hex: string; signing_context: *void; signer: SignMessage; }
RegistrationCurrent :: (client: Client, session: *Session, device: Device, now: s64) -> bool
RememberRegistration :: (client: Client, session: *Session, device: Device, now: s64)
MissingRegisteredDevice :: (status: s32, response: string) -> bool
ReplayedSyncTransaction :: (status: s32, response: string) -> bool
WriteRegistration :: (registration: *Registration, output: []u8) -> bool

Apps 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) -> bool
SignatureHex :: (value: string, length: s64) -> bool
ServerNow :: (session: *Session, now: s64) -> s64
CreateNonce :: (random_context: *void, random: RandomNonce, output: []u8) -> bool
RegisterDevice :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce) -> AuthResult
Sync :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce, body: string, output: []u8, status: *s32) -> AuthResult
PrepareDeviceRegistration :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce, body: []u8) -> AuthResult

Shared 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) -> AuthResult
PrepareRequestHeader :: (client: Client, session: *Session, device: Device, now: s64, random_context: *void, random: RandomNonce, method: string, path: string, body: string, transaction_header: []u8) -> AuthResult

Binary 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) -> AuthResult

Hosts that stream and hash a body themselves supply the finished digest and expiration. The client owns the account/device signatures and wire format.

Source ↗

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) -> bool
HexText :: (value: string, length: s64) -> bool
WriteDecimal :: (writer: Writer, value: s64) -> Writer
RegistrationValid :: (request: DeviceRegistration) -> bool
TransactionValid :: (transaction: SyncTransaction) -> bool
BuildDeviceRegistrationMessage :: (request: DeviceRegistration, output: []u8) -> bool
BuildDeviceRegistrationBody :: (request: DeviceRegistration, account_signature_hex: string, output: []u8) -> bool
BuildSyncTransactionMessage :: (transaction: SyncTransaction, output: []u8) -> bool
RequestValid :: (method: string, path: string) -> bool
BuildTransactionMessage :: (transaction: SyncTransaction, method: string, path: string, output: []u8) -> bool
BuildSyncTransactionHeader :: (transaction: SyncTransaction, account_signature_hex: string, device_signature_hex: string, output: []u8) -> bool
BuildTransactionHeader :: (transaction: SyncTransaction, method: string, path: string, account_signature_hex: string, device_signature_hex: string, output: []u8) -> bool

Source ↗

daochi-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) -> bool
HostBoundary :: (authority: string, index: s64) -> bool
PrivateIpv4Authority :: (authority: string) -> bool
LocalAuthority :: (authority: string) -> bool
SafeUrlBytes :: (value: string) -> bool
UrlValid :: (url: string) -> bool
NormalizeUrl :: (input: string, output: []u8) -> bool
WriteUrlText :: (output: []u8, cursor: s64, value: string) -> s64
JoinUrl :: (base_url: string, path: string, output: []u8) -> bool
JoinWebSocketUrl :: (base_url: string, path: string, output: []u8) -> bool

Source ↗

daochi-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) -> Writer
WriteText :: (writer: Writer, value: string) -> Writer
HexDigit :: (byte: u8) -> bool
Hex64 :: (value: string) -> bool
HexUpper :: (value: u8) -> u8
AppendJsonQuoted :: (writer: Writer, value: string) -> Writer
BuildChallengePath :: (account_id: string, output: []u8) -> bool
WriteLoginBody :: (account_id: string, client_id: string, public_key: string, restore: bool, output: []u8) -> bool
BuildLoginBody :: (account_id: string, client_id: string, public_key: string, output: []u8) -> bool
BuildRestoreLoginBody :: (account_id: string, client_id: string, public_key: string, output: []u8) -> bool

A 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) -> bool

The 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

Source ↗