proto

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Index

Constants

View Source
const (
	MaxBodyBytes    = 256_000
	MaxBodyLiteral  = 6*MaxBodyBytes + 2
	MaxRequestBytes = 4_000_000
	MaxOpsPerPush   = 1_000
	MaxPageLimit    = 500
	MaxDeviceIDLen  = 128
)

Limits taken from the client. MaxBodyBytes applies to the decoded body; a literal can be several times longer once escapes are counted, so it gets its own looser guard rather than a shared one.

View Source
const DigestLen = 43

DigestLen is the length of a base64url SHA-256 digest with no padding.

View Source
const MaxDec = Dec(math.MaxUint64)

MaxDec is the largest value the protocol permits (uint64 max).

View Source
const NullPayloadDigest = "dCNOmK_nSY-12vHzasLXiswzlGT5UHA7jAGYkvmCuQs"

NullPayloadDigest is the digest of the canonical JSON literal `null`. Mutation ops carry it as payload_sha256; having the constant here lets tests assert our digest function agrees with the client's without a live client.

Variables

This section is empty.

Functions

func Digest

func Digest(b []byte) string

Digest returns the base64url, unpadded SHA-256 of b.

The client uses base64url (`-` and `_`), not standard base64, and not hex. Emitting the wrong alphabet makes every ack tuple miss and wedges the push pipeline, so the encoding is pinned here and asserted in tests.

func EmitAck

func EmitAck(w io.Writer, a Ack) error

EmitAck writes one acknowledgement.

origin_local_id is emitted as null when absent rather than omitted: the client requires the key to be present and type-checks it as string|null, so an omitted key is undefined and throws.

func EmitChangeOp

func EmitChangeOp(w io.Writer, seq, serverTS Dec, op Op) error

EmitChangeOp writes one entry of a /v1/sync/changes page.

func Reject

func Reject(r RejectReason) error

Reject builds a RejectError.

func UnquoteJSONString

func UnquoteJSONString(literal []byte) ([]byte, error)

UnquoteJSONString decodes a JSON string literal (quotes included) into the bytes the client hashed.

The semantics deliberately match Node, because the client computes operation_sha256 as sha256(body, 'utf8') and Node's UTF-8 encoder replaces an unpaired surrogate with U+FFFD rather than failing. Verified:

JSON.stringify("a\ud800b")            -> "a\ud800b"
Buffer.from("a\ud800b", "utf8")       -> 61 ef bf bd 62

So an unpaired surrogate is a value the client considers perfectly valid and has already hashed as U+FFFD. Rejecting it here would 400 a legitimate body, and a 400 parks that op at the head of the client's outbox forever, blocking every op behind it. Substituting is the compatible behaviour, not a shortcut.

Genuinely malformed input — a bare control character, a truncated escape, a missing quote — is still an error: no conforming client can produce it.

func ValidDigest

func ValidDigest(s string) bool

ValidDigest reports whether s is shaped like a base64url SHA-256 digest.

Types

type Ack

type Ack struct {
	ID            string
	Kind          string
	EntityRev     Dec
	Digest        string
	OriginLocalID *Dec
	Seq           Dec
}

Ack is one entry in a push response.

Every field is mandatory on the wire. OriginLocalID is nullable but must be present: the client type-checks it as `string | null` and an omitted key is undefined, which fails that check and wedges the push pipeline.

type Dec

type Dec uint64

Dec is an unsigned integer that crosses the wire as a canonical decimal string, never a JSON number.

claude-mem's client validates every sequence, revision, epoch, and timestamp with a strict decimal-string check and rejects JSON numbers outright. A number where a string belongs makes the client throw while applying a page, which rolls the page back, leaves the cursor unmoved, and retries the same page forever — a permanently wedged device with no error surfaced to the user. Routing every such value through this one type is what keeps a handler from emitting a bare integer by accident.

func DecFromInt64

func DecFromInt64(v int64) (Dec, error)

DecFromInt64 converts a stored value back.

func ParseDec

func ParseDec(s string) (Dec, error)

ParseDec accepts a canonical decimal string: "0" or a digit string with no leading zero, no sign, no whitespace, no exponent, within uint64.

Canonical form is enforced here rather than at the storage layer because entity revisions are stored in a TEXT unique index: "1" and "01" would become two rows for one logical revision, which breaks first-write-wins dedupe and lets a retry consume a fresh sequence number.

func ParseDecPositive

func ParseDecPositive(s string) (Dec, error)

ParseDecPositive additionally rejects "0". Sequence numbers and entity revisions must be positive; the hub's first op is seq 1.

func (Dec) Int64

func (d Dec) Int64() (int64, error)

Int64 converts for SQLite storage, which has no unsigned integer type.

A value above MaxInt64 would silently wrap to a negative on a plain conversion, turning a large cursor into one that matches no rows. Sequence allocation therefore stops at MaxInt64 with an error rather than wrapping.

func (Dec) MarshalJSON

func (d Dec) MarshalJSON() ([]byte, error)

MarshalJSON always emits a quoted decimal.

func (Dec) String

func (d Dec) String() string

func (*Dec) UnmarshalJSON

func (d *Dec) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts only a quoted canonical decimal. A JSON number is an error, matching the client's own validator.

type Op

type Op struct {
	RawLiteral     []byte
	Digest         string
	ID             string
	Kind           string
	EntityRev      Dec
	OriginDeviceID string
	OriginLocalID  *Dec
}

Op is a validated operation.

RawLiteral is the JSON string literal exactly as received, quotes included. It is what gets stored and what gets written back out; nothing re-encodes it. The decoded form exists only long enough to verify the digest and read the routing fields, then is discarded.

func ParseOp

func ParseOp(wrapper json.RawMessage) (Op, error)

ParseOp validates a single operation wrapper and extracts what the hub needs.

Validation here is deliberately minimal. A rejected push is not dropped by the client — it stays at the head of the outbox and is retried forever, blocking every op behind it. So this rejects only what the hub cannot store or route, never anything a conforming client could legitimately produce.

type RejectError

type RejectError struct {
	Reason RejectReason
}

RejectError carries a reason and, by construction, nothing else. The struct has exactly one field so there is nowhere for request bytes to hide.

func (*RejectError) Error

func (e *RejectError) Error() string

type RejectReason

type RejectReason string

RejectReason is the fixed vocabulary of rejection causes.

Error responses carry a reason from this list and nothing else. The client slices the first 200 bytes of an error body into its own log on another machine, so echoing any part of a request would copy memory content — the exact data this project exists to keep local — into a second device's logs.

const (
	ReasonProtocolVersion RejectReason = "protocol_version"
	ReasonWrapperShape    RejectReason = "wrapper_shape"
	ReasonDigestMismatch  RejectReason = "digest_mismatch"
	ReasonBodyShape       RejectReason = "body_shape"
	ReasonUnknownKind     RejectReason = "unknown_kind"
	ReasonEntityRev       RejectReason = "entity_rev"
	ReasonTooLarge        RejectReason = "too_large"
	ReasonBadCursor       RejectReason = "bad_cursor"
	ReasonUserMismatch    RejectReason = "user_mismatch"
	ReasonUnauthorized    RejectReason = "unauthorized"
	ReasonStorageFull     RejectReason = "storage_full"
	ReasonBadRequest      RejectReason = "bad_request"
	ReasonOverloaded      RejectReason = "overloaded"
	ReasonInternal        RejectReason = "internal"
)

func ReasonOf

func ReasonOf(err error) RejectReason

ReasonOf extracts the reason from err, or ReasonInternal if err is not a RejectError. Handlers use this so an unexpected error can never leak its text.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL