package main import ( "context" "encoding/json" "fmt" "os" "strings" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) // Legacy migrations retain their create-only bytes. A populated sequence // cannot be transplanted into another key as a new incarnation; that needs // the claim create protocol, not a raw copy of archived authority. func putMigrationObject(ctx context.Context, store release.Store, key string, body []byte) (string, error) { if !hostClaimEnabled(os.Getenv("CARLOS_HOST_CLAIM")) { return store.Put(ctx, key, body, "") } if provision.IsRecordKey(key) { if err := migrationRecordBytes(key, body); err != nil { return "", err } if _, _, err := provision.RecordSequence(body); err != nil { // Main copied bodies that are not strict JSON objects verbatim; // with no sequence in them there is nothing to advance. return store.Put(ctx, key, body, "") } tag, _, err := provision.WriteRecord(ctx, store, key, []byte(`{}`), body, "") return tag, err } return store.Put(ctx, key, body, "") } // Preflight precedes every copy and also runs for --dry-run. The write helper // repeats the check because a source can change after preflight. func validateMigrationRecords(ctx context.Context, store release.Store, plan []copyOp) error { if !hostClaimEnabled(os.Getenv("CARLOS_HOST_CLAIM")) { return nil } for _, c := range plan { if !provision.IsRecordKey(c.to) { continue } raw, _, err := store.Get(ctx, c.from) if err != nil { return err } if err = migrationRecordBytes(c.from, raw); err != nil { return err } } return nil } func migrationRecordBytes(key string, body []byte) error { if !migrationHasSequence(body) { return nil } return fmt.Errorf("accounts migrate: %s has a host-claim sequence; use the claim create protocol to create a new incarnation: %w", key, provision.ErrRecordSequence) } // Only a present record_seq refuses: main copied malformed bodies, non-objects // and duplicate keys verbatim, so those still pass. A body no JSON reader can // parse carries no sequence; one that parses leniently is refused if any key // could be read as the sequence. func migrationHasSequence(body []byte) bool { if fields, _, err := provision.RecordSequence(body); err == nil { _, present := fields["record_seq"] return present } var loose map[string]json.RawMessage if json.Unmarshal(body, &loose) != nil { return false } for name := range loose { if strings.EqualFold(name, "record_seq") { return true } } return false }