package release import ( "context" "crypto/ed25519" "errors" "fmt" "strings" "time" ) // TemporaryAppPrefix is the reserved namespace every temporary app lives in // (temporary static sites spec, ruling 7). Nothing permanent may be CREATED // under it; an app made permanent with `carlos apps keep` keeps its name. // The reservation is what makes "brand new" provable: no permanent app, row // or route can hold a tmp- name, so a temporary app's expiry can only drop // routes a temporary app created (spec §3). const TemporaryAppPrefix = "tmp-" // IsTemporaryName reports whether app is in the reserved namespace. It says // nothing about whether the app IS temporary: a kept app keeps its tmp- name, // and only the console's row knows. func IsTemporaryName(app string) bool { return strings.HasPrefix(app, TemporaryAppPrefix) } // TemporaryCooloff is how long a trashed temporary app stays restorable // before the console purges it (spec ruling 2). The box's own 30-day // TrashCooloff still applies to every app as the backstop. const TemporaryCooloff = 72 * time.Hour // ErrTrashClockMismatch is PurgeAppIfTrashedAt's refusal: the app is in the // Trash, but not the trash the purge decision was made about. var ErrTrashClockMismatch = errors.New("release: the trash marker is not the one the purge was decided for") // PurgeAppIfTrashedAt is PurgeApp with one more precondition: the current // marker's DeletedAt, truncated to whole seconds, equals wantDeletedAt (the // console row's purge_for). A restore followed by a keep and a fresh trash // writes a marker with a new clock, so a decision taken about the old trash // can no longer fire (spec §5, astra 4). // // The check runs on the SAME tombstone read PurgeApp's own preconditions use // (purgeReady), so there is no second read and no window between two. Every // other guard (production flag, object-store estate) applies unchanged. func PurgeAppIfTrashedAt(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app string, wantDeletedAt, now time.Time) error { return purgeApp(ctx, s, pub, acct, app, func(t *AppTombstone) error { if t.DeletedAt.Unix() != wantDeletedAt.Unix() { return fmt.Errorf("release: %s/%s was trashed at %s, not %s: %w", acct, app, t.DeletedAt.UTC().Format(time.RFC3339), wantDeletedAt.UTC().Format(time.RFC3339), ErrTrashClockMismatch) } return nil }) }