hush/vendor/github.com/redis/go-redis/v9/array_commands.go
jx12n 4d9a26498e hush: one-time secret links the server cannot read
Paste a secret, get a link, send it. The first person to open it and press
Reveal sees the secret; the link dies at that moment. The recipient needs a
browser and nothing else — no account, no client, no installed tooling.

The server cannot read what it stores. AES-256-GCM happens in the browser and
the key lives in the URL fragment, which browsers never transmit, so hushd
holds ciphertext and no key material. That is a property of where the key sits
rather than a promise about our conduct, which is why there is deliberately no
endpoint accepting a plaintext secret and no server-side-encryption fallback:
two guarantees behind one URL would be worse than one honest guarantee.

Three decisions carry the design:

  * GET /s/{id} touches NO storage, not even to check existence. Slack, Teams,
    WhatsApp, iMessage and Outlook Safe Links all fetch a URL before a human
    sees it, so destroying on GET would destroy most secrets in transit and the
    recipient's "already used" would be indistinguishable from interception.
    Only POST /reveal consumes. Bot user-agent detection is an arms race;
    removing the side effect from GET is not. Pinned by
    TestGettingTheRevealPageNeverConsumesTheSecret.
  * Destruction is one Redis GETDEL, which is atomic. GET-then-DEL has a window
    where two simultaneous readers both win, and for a one-time secret that
    window is the product. The store contract demands atomicity and the same
    concurrency test runs against both implementations.
  * Missing, already-revealed, expired and evicted are ONE indistinguishable
    410. Separating them would confirm to a prober that a given link was real.

The secret id IS the capability, so secret.ID is a struct whose every
accidental path — %v, %s, String(), slog, json.Marshal — emits a redacted
handle or refuses, and the raw value needs an explicit Value(). The first
version tried to prevent leaks by implementing no String() at all; its own test
caught that Go's fmt prints unexported fields anyway, so forbidding the method
had removed the control rather than the leak.

Operationally: structured JSON on stdout in the fleet's wire format, which
Vector already collects with no annotation; six hush_* metrics on the chassis
registry with no id, IP or path in any label; five alert rules wired into
vmalert. The public Ingress enumerates /, /s/ and /api/ so /metrics, /healthz
and /readyz share the port but are unreachable from the internet — no
basic-auth middleware to maintain and get wrong.

Dependencies are vendored because go-chassis is private: the Woodpecker test
step and the in-cluster Kaniko build both run -mod=vendor with GOPROXY=off and
hold no git credential.

cmd/hush-mcp is a stdio MCP server doing the same client-side crypto locally,
so using hush from an agent preserves the same guarantee as using it from a
browser.
2026-09-03 00:08:38 -06:00

388 lines
12 KiB
Go

package redis
import (
"context"
)
// note: the APIs is experimental and may be subject to change.
//
// ArrayCmdable defines the interface for Redis Array data structure commands
// available in Redis 8.8.0+.
//
// Redis array supports index range [0, math.MaxUint64-1), so index parameters use uint64.
type ArrayCmdable interface {
ARSet(ctx context.Context, key string, index uint64, values ...string) *IntCmd
ARGet(ctx context.Context, key string, index uint64) *StringCmd
ARGetRange(ctx context.Context, key string, start, end uint64) *SliceCmd
ARMGet(ctx context.Context, key string, indexes ...uint64) *SliceCmd
ARMSet(ctx context.Context, key string, members ...AREntry) *IntCmd
ARInsert(ctx context.Context, key string, values ...string) *UintCmd
ARDel(ctx context.Context, key string, indexes ...uint64) *IntCmd
ARDelRange(ctx context.Context, key string, ranges ...ARRange) *UintCmd
ARLen(ctx context.Context, key string) *UintCmd
ARCount(ctx context.Context, key string) *UintCmd
ARNext(ctx context.Context, key string) *UintCmd
ARSeek(ctx context.Context, key string, index uint64) *IntCmd
ARInfo(ctx context.Context, key string) *MapStringInterfaceCmd
ARInfoFull(ctx context.Context, key string) *MapStringInterfaceCmd
ARScan(ctx context.Context, key string, start, end uint64, args *ARScanArgs) *AREntrySliceCmd
AROpSum(ctx context.Context, key string, start, end uint64) *StringCmd
AROpMin(ctx context.Context, key string, start, end uint64) *StringCmd
AROpMax(ctx context.Context, key string, start, end uint64) *StringCmd
AROpAnd(ctx context.Context, key string, start, end uint64) *IntCmd
AROpOr(ctx context.Context, key string, start, end uint64) *IntCmd
AROpXor(ctx context.Context, key string, start, end uint64) *IntCmd
AROpMatch(ctx context.Context, key string, start, end uint64, value string) *IntCmd
AROpUsed(ctx context.Context, key string, start, end uint64) *IntCmd
ARGrep(ctx context.Context, key string, start, end string, args *ARGrepArgs) *UintSliceCmd
ARGrepWithValues(ctx context.Context, key string, start, end string, args *ARGrepArgs) *AREntrySliceCmd
ARRing(ctx context.Context, key string, size uint64, values ...string) *UintCmd
ARLastItems(ctx context.Context, key string, count uint64, rev bool) *SliceCmd
}
// AREntry represents an index-value pair for ARMSET.
type AREntry struct {
Index uint64
Value string
}
// ARRange represents a start-end range for ARDELRANGE.
type ARRange struct {
Start uint64
End uint64
}
// ARScanArgs contains optional arguments for ARSCAN.
type ARScanArgs struct {
Limit uint64
}
// ARGrepPredicateType defines the type of predicate for ARGREP.
type ARGrepPredicateType string
const (
ARGrepExact ARGrepPredicateType = "EXACT"
ARGrepMatch ARGrepPredicateType = "MATCH"
ARGrepGlob ARGrepPredicateType = "GLOB"
ARGrepRegex ARGrepPredicateType = "RE"
)
// ARGrepPredicate represents a search predicate for ARGREP.
type ARGrepPredicate struct {
Type ARGrepPredicateType
Value string
}
// ARGrepArgs contains optional arguments for ARGREP.
// Redis ARGREP defaults to OR when multiple predicates are given.
// Set CombineAnd to true to combine predicates with AND instead.
type ARGrepArgs struct {
Predicates []ARGrepPredicate
CombineAnd bool
Limit uint64
NoCase bool
}
// ARSet sets one or more contiguous values starting at an index in an array.
// Returns the number of new slots that were set (previously empty).
func (c cmdable) ARSet(ctx context.Context, key string, index uint64, values ...string) *IntCmd {
args := make([]any, 3, 3+len(values))
args[0] = "arset"
args[1] = key
args[2] = index
for _, v := range values {
args = append(args, v)
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARGet gets the value at an index in an array.
// Returns redis.Nil if the key or index does not exist.
func (c cmdable) ARGet(ctx context.Context, key string, index uint64) *StringCmd {
cmd := NewStringCmd(ctx, "arget", key, index)
_ = c(ctx, cmd)
return cmd
}
// ARGetRange gets values in a range of indexes.
// Returns values in the range, with nil for unset indexes.
func (c cmdable) ARGetRange(ctx context.Context, key string, start, end uint64) *SliceCmd {
cmd := NewSliceCmd(ctx, "argetrange", key, start, end)
_ = c(ctx, cmd)
return cmd
}
// ARMGet gets values at multiple indexes in an array.
// Returns values at the specified indexes, with nil for unset indexes.
func (c cmdable) ARMGet(ctx context.Context, key string, indexes ...uint64) *SliceCmd {
args := make([]any, 2+len(indexes))
args[0] = "armget"
args[1] = key
for i, idx := range indexes {
args[2+i] = idx
}
cmd := NewSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARMSet sets multiple index-value pairs in an array.
// Returns the number of new slots that were set (previously empty).
func (c cmdable) ARMSet(ctx context.Context, key string, members ...AREntry) *IntCmd {
args := make([]any, 2, 2+2*len(members))
args[0] = "armset"
args[1] = key
for _, m := range members {
args = append(args, m.Index, m.Value)
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARInsert inserts one or more values at consecutive indexes.
// Returns the last index where a value was inserted.
func (c cmdable) ARInsert(ctx context.Context, key string, values ...string) *UintCmd {
args := make([]any, 2, 2+len(values))
args[0] = "arinsert"
args[1] = key
for _, v := range values {
args = append(args, v)
}
cmd := NewUintCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARDel deletes elements at the specified indexes in an array.
// Returns the number of elements deleted.
func (c cmdable) ARDel(ctx context.Context, key string, indexes ...uint64) *IntCmd {
args := make([]any, 2+len(indexes))
args[0] = "ardel"
args[1] = key
for i, idx := range indexes {
args[2+i] = idx
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARDelRange deletes elements in one or more ranges.
// Returns the number of elements deleted.
func (c cmdable) ARDelRange(ctx context.Context, key string, ranges ...ARRange) *UintCmd {
args := make([]any, 2, 2+2*len(ranges))
args[0] = "ardelrange"
args[1] = key
for _, r := range ranges {
args = append(args, r.Start, r.End)
}
cmd := NewUintCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARLen returns the length of an array (max index + 1).
// Returns 0 if the key does not exist.
func (c cmdable) ARLen(ctx context.Context, key string) *UintCmd {
cmd := NewUintCmd(ctx, "arlen", key)
_ = c(ctx, cmd)
return cmd
}
// ARCount returns the number of non-empty elements in an array.
// Returns 0 if the key does not exist.
func (c cmdable) ARCount(ctx context.Context, key string) *UintCmd {
cmd := NewUintCmd(ctx, "arcount", key)
_ = c(ctx, cmd)
return cmd
}
// ARNext returns the next index ARINSERT would use.
// Returns 0 for missing keys or when no insert happened yet.
// Returns nil when the insertion cursor is exhausted / would overflow.
func (c cmdable) ARNext(ctx context.Context, key string) *UintCmd {
cmd := NewUintCmd(ctx, "arnext", key)
_ = c(ctx, cmd)
return cmd
}
// ARSeek sets the ARINSERT / ARRING cursor to a specific index.
// Returns 1 if the cursor was set, 0 if the key does not exist.
func (c cmdable) ARSeek(ctx context.Context, key string, index uint64) *IntCmd {
cmd := NewIntCmd(ctx, "arseek", key, index)
_ = c(ctx, cmd)
return cmd
}
// ARInfo returns metadata about an array.
func (c cmdable) ARInfo(ctx context.Context, key string) *MapStringInterfaceCmd {
cmd := NewMapStringInterfaceCmd(ctx, "arinfo", key)
_ = c(ctx, cmd)
return cmd
}
// ARInfoFull returns detailed metadata about an array including slice statistics.
func (c cmdable) ARInfoFull(ctx context.Context, key string) *MapStringInterfaceCmd {
cmd := NewMapStringInterfaceCmd(ctx, "arinfo", key, "full")
_ = c(ctx, cmd)
return cmd
}
// ARScan iterates existing elements in a range, returning index-value pairs.
func (c cmdable) ARScan(ctx context.Context, key string, start, end uint64, scanArgs *ARScanArgs) *AREntrySliceCmd {
args := make([]any, 4, 6)
args[0], args[1], args[2], args[3] = "arscan", key, start, end
if scanArgs != nil && scanArgs.Limit > 0 {
args = append(args, "limit", scanArgs.Limit)
}
cmd := NewAREntrySliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// AROpSum returns the sum of numeric elements in a range.
func (c cmdable) AROpSum(ctx context.Context, key string, start, end uint64) *StringCmd {
cmd := NewStringCmd(ctx, "arop", key, start, end, "SUM")
_ = c(ctx, cmd)
return cmd
}
// AROpMin returns the minimum numeric element in a range.
func (c cmdable) AROpMin(ctx context.Context, key string, start, end uint64) *StringCmd {
cmd := NewStringCmd(ctx, "arop", key, start, end, "MIN")
_ = c(ctx, cmd)
return cmd
}
// AROpMax returns the maximum numeric element in a range.
func (c cmdable) AROpMax(ctx context.Context, key string, start, end uint64) *StringCmd {
cmd := NewStringCmd(ctx, "arop", key, start, end, "MAX")
_ = c(ctx, cmd)
return cmd
}
// AROpAnd returns the bitwise AND of integer elements in a range.
func (c cmdable) AROpAnd(ctx context.Context, key string, start, end uint64) *IntCmd {
cmd := NewIntCmd(ctx, "arop", key, start, end, "AND")
_ = c(ctx, cmd)
return cmd
}
// AROpOr returns the bitwise OR of integer elements in a range.
func (c cmdable) AROpOr(ctx context.Context, key string, start, end uint64) *IntCmd {
cmd := NewIntCmd(ctx, "arop", key, start, end, "OR")
_ = c(ctx, cmd)
return cmd
}
// AROpXor returns the bitwise XOR of integer elements in a range.
func (c cmdable) AROpXor(ctx context.Context, key string, start, end uint64) *IntCmd {
cmd := NewIntCmd(ctx, "arop", key, start, end, "XOR")
_ = c(ctx, cmd)
return cmd
}
// AROpMatch returns the count of elements matching a target string in a range.
func (c cmdable) AROpMatch(ctx context.Context, key string, start, end uint64, value string) *IntCmd {
cmd := NewIntCmd(ctx, "arop", key, start, end, "MATCH", value)
_ = c(ctx, cmd)
return cmd
}
// AROpUsed returns the count of non-empty slots in a range.
func (c cmdable) AROpUsed(ctx context.Context, key string, start, end uint64) *IntCmd {
cmd := NewIntCmd(ctx, "arop", key, start, end, "USED")
_ = c(ctx, cmd)
return cmd
}
// ARGrep searches array elements in a range using textual predicates.
// Returns matching indexes only. Use ARGrepWithValues to also get the values.
func (c cmdable) ARGrep(ctx context.Context, key string, start, end string, grepArgs *ARGrepArgs) *UintSliceCmd {
args := make([]any, 4, 4+grepArgs.Len())
args[0], args[1], args[2], args[3] = "argrep", key, start, end
args = grepArgs.Append(args)
cmd := NewUintSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARGrepWithValues searches array elements in a range using textual predicates.
// Returns matching indexes and their values as index-value pairs.
func (c cmdable) ARGrepWithValues(ctx context.Context, key string, start, end string, grepArgs *ARGrepArgs) *AREntrySliceCmd {
args := make([]any, 4, 5+grepArgs.Len())
args[0], args[1], args[2], args[3] = "argrep", key, start, end
args = grepArgs.Append(args)
args = append(args, "withvalues")
cmd := NewAREntrySliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (args *ARGrepArgs) Len() int {
if args == nil {
return 0
}
n := 2 * len(args.Predicates)
if args.CombineAnd {
n++
}
if args.Limit > 0 {
n += 2
}
if args.NoCase {
n++
}
return n
}
func (args *ARGrepArgs) Append(a []any) []any {
if args == nil {
return a
}
for _, p := range args.Predicates {
a = append(a, string(p.Type), p.Value)
}
if args.CombineAnd {
a = append(a, "and")
}
if args.Limit > 0 {
a = append(a, "limit", args.Limit)
}
if args.NoCase {
a = append(a, "nocase")
}
return a
}
// ARRing inserts values into a ring buffer of specified size, wrapping and truncating as needed.
// Returns the last index where a value was inserted.
func (c cmdable) ARRing(ctx context.Context, key string, size uint64, values ...string) *UintCmd {
args := make([]any, 3, 3+len(values))
args[0] = "arring"
args[1] = key
args[2] = size
for _, v := range values {
args = append(args, v)
}
cmd := NewUintCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARLastItems returns the most recently inserted elements.
// When rev is true, returns items in reverse order.
func (c cmdable) ARLastItems(ctx context.Context, key string, count uint64, rev bool) *SliceCmd {
args := make([]any, 3, 4)
args[0], args[1], args[2] = "arlastitems", key, count
if rev {
args = append(args, "rev")
}
cmd := NewSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}