Documentation
¶
Index ¶
- Constants
- func Digest(b []byte) string
- func EmitAck(w io.Writer, a Ack) error
- func EmitChangeOp(w io.Writer, seq, serverTS Dec, op Op) error
- func Reject(r RejectReason) error
- func UnquoteJSONString(literal []byte) ([]byte, error)
- func ValidDigest(s string) bool
- type Ack
- type Dec
- type Op
- type RejectError
- type RejectReason
Constants ¶
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.
const DigestLen = 43
DigestLen is the length of a base64url SHA-256 digest with no padding.
const MaxDec = Dec(math.MaxUint64)
MaxDec is the largest value the protocol permits (uint64 max).
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 ¶
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 ¶
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 ¶
EmitChangeOp writes one entry of a /v1/sync/changes page.
func UnquoteJSONString ¶
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 ¶
ValidDigest reports whether s is shaped like a base64url SHA-256 digest.
Types ¶
type Ack ¶
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 ¶
DecFromInt64 converts a stored value back.
func ParseDec ¶
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 ¶
ParseDecPositive additionally rejects "0". Sequence numbers and entity revisions must be positive; the hub's first op is seq 1.
func (Dec) Int64 ¶
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 ¶
MarshalJSON always emits a quoted decimal.
func (*Dec) UnmarshalJSON ¶
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" 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.