package maintenanceguard import ( "bytes" "context" "errors" "os" ) // Store is structural so release, provision and host can share these rules // without importing each other. Put uses empty ETag for create-only writes. type Store interface { Get(context.Context, string) ([]byte, string, error) Put(context.Context, string, []byte, string) (string, error) } type Repository struct{ store Store } func New(store Store) *Repository { return &Repository{store: store} } type ReferenceKind string const ( RequestReference ReferenceKind = "requests" AttemptReference ReferenceKind = "attempts" ReceiptReference ReferenceKind = "receipts" ) func (k ReferenceKind) valid() bool { return k == RequestReference || k == AttemptReference || k == ReceiptReference } // PutReference stores permanent, nonsecret metadata. Expiring private payloads // belong elsewhere: active guards must keep resolving these immutable bytes. // The caller validates metadata, ownership and any encrypted payload bindings. func (r *Repository) PutReference(ctx context.Context, host string, kind ReferenceKind, id string, raw []byte) (Reference, error) { if !kind.valid() || len(raw) == 0 || len(raw) > MaxReferenceBytes { return Reference{}, ErrInvalid } key, e := refKey(host, string(kind), id) if e != nil { return Reference{}, e } if e = r.putImmutable(ctx, key, raw); e != nil { return Reference{}, e } return Reference{ID: id, Digest: digest(raw)}, nil } func (r *Repository) putImmutable(ctx context.Context, key string, raw []byte) error { old, _, e := r.store.Get(ctx, key) if e == nil { if bytes.Equal(old, raw) { return nil } return ErrConflict } if !errors.Is(e, os.ErrNotExist) { return e } _, e = r.store.Put(ctx, key, raw, "") return e } func (r *Repository) checkReference(ctx context.Context, host string, kind ReferenceKind, ref Reference) error { if !ref.valid() { return ErrInvalid } key, e := refKey(host, string(kind), ref.ID) if e != nil { return e } raw, _, e := r.store.Get(ctx, key) if e != nil || len(raw) == 0 || len(raw) > MaxReferenceBytes || digest(raw) != ref.Digest { return ErrUnavailable } return nil } func (r *Repository) checkGuardReferences(ctx context.Context, g Guard) error { for _, v := range []struct { kind ReferenceKind ref Reference }{{RequestReference, g.Request}, {AttemptReference, g.Attempt}} { if e := r.checkReference(ctx, g.Identity.Host, v.kind, v.ref); e != nil { return e } } if g.Evidence != nil { return r.checkReference(ctx, g.Identity.Host, ReceiptReference, *g.Evidence) } return nil }