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