Approve/commit now write FAV staging in the domain transaction, recover-apply materializes scratch collections, and unpublished or GET paths cannot mutate the registry. Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
3.1 KiB
Go
104 lines
3.1 KiB
Go
package artifactvault
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type Publisher struct {
|
|
Reg Registry
|
|
Store *FilesystemStore
|
|
}
|
|
|
|
func (p *Publisher) Drain(ctx context.Context) error {
|
|
arts, err := p.Reg.LoadRequested(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var first error
|
|
for _, a := range arts {
|
|
if err := p.PublishOne(ctx, a); err != nil && first == nil {
|
|
first = err
|
|
}
|
|
}
|
|
return first
|
|
}
|
|
|
|
func (p *Publisher) PublishOne(ctx context.Context, art Artifact) error {
|
|
if p.Store == nil {
|
|
return fmt.Errorf("artifactvault: store required")
|
|
}
|
|
if p.Reg.IsDenied(ctx, art.UUID) {
|
|
return fmt.Errorf("artifactvault: artifact denied")
|
|
}
|
|
_ = p.Reg.MarkPublishing(ctx, art.UUID)
|
|
st, err := p.Reg.LoadStaging(ctx, art.SourceEventID)
|
|
if err != nil {
|
|
_ = p.Reg.MarkFailed(ctx, art.UUID, "staging_missing")
|
|
return err
|
|
}
|
|
if SHA256Hex(st.Payload) != art.PayloadSHA256 || SHA256Hex(st.Payload) != st.PayloadSHA256 {
|
|
_ = p.Reg.MarkFailed(ctx, art.UUID, "sha_mismatch")
|
|
return fmt.Errorf("artifactvault: staging sha mismatch")
|
|
}
|
|
if ClassifyPayload(st.Payload) != ClassificationOK {
|
|
_ = p.Reg.MarkFailed(ctx, art.UUID, "sensitive")
|
|
return ErrSensitiveRejected
|
|
}
|
|
art.VaultStorageIdentity = p.Store.Identity()
|
|
keyRef, err := p.Store.PutBlob(art.Environment, art.ProjectExternalKey, art.PayloadSHA256, st.Payload)
|
|
if err != nil {
|
|
_ = p.Reg.MarkFailed(ctx, art.UUID, "blob")
|
|
return err
|
|
}
|
|
man := BuildManifest(art, art.PayloadSHA256, len(st.Payload), keyRef)
|
|
if err := ValidateManifest(man); err != nil {
|
|
_ = p.Reg.MarkFailed(ctx, art.UUID, "manifest")
|
|
return err
|
|
}
|
|
manRaw := EncodeJSON(man)
|
|
if err := p.Store.PutManifest(art.UUID, man.ManifestSHA256, manRaw); err != nil {
|
|
_ = p.Reg.MarkFailed(ctx, art.UUID, "manifest_write")
|
|
return err
|
|
}
|
|
marker := CommitMarker{
|
|
ArtifactUUID: art.UUID, ManifestSHA256: man.ManifestSHA256,
|
|
MemberSHAs: []string{art.PayloadSHA256}, VaultStorageIdentity: p.Store.Identity(),
|
|
CommittedAt: time.Now().UTC().Format(time.RFC3339), PublisherVersion: PublisherVersion,
|
|
}
|
|
if err := ValidateCommitMarker(marker); err != nil {
|
|
return err
|
|
}
|
|
if err := p.Store.PutCommit(art.UUID, man.ManifestSHA256, EncodeJSON(marker)); err != nil {
|
|
_ = p.Reg.MarkFailed(ctx, art.UUID, "commit")
|
|
return err
|
|
}
|
|
_ = p.Reg.RecordReference(ctx, art.UUID, art.ProjectExternalKey, art.PayloadSHA256, keyRef)
|
|
return p.Reg.MarkVerified(ctx, art.UUID, man.ManifestSHA256)
|
|
}
|
|
|
|
func (p *Publisher) DegradedPayload(ctx context.Context, env, logical, projectKey string) ([]byte, string, error) {
|
|
art, err := p.Reg.GetByLogical(ctx, env, logical)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if p.Reg.IsDenied(ctx, art.UUID) || p.Store.HasTombstone(art.UUID) {
|
|
return nil, "", fmt.Errorf("artifactvault: union-deny")
|
|
}
|
|
if art.ManifestSHA256 == "" || !p.Store.HasCommit(art.UUID, art.ManifestSHA256) {
|
|
return nil, "", fmt.Errorf("artifactvault: unpublished")
|
|
}
|
|
b, err := p.Store.GetBlob(env, projectKey, art.PayloadSHA256)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return b, art.PayloadSHA256, nil
|
|
}
|
|
|
|
func MustJSON(v any) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|