1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//! Hot-rotating secrets without restarts and without locks.
//!
//! ## The problem
//!
//! Secrets read from env at boot are frozen for the process lifetime: rotating
//! `JWT_SECRET` means restarting the whole fleet and invalidating every live
//! token at once. Plaintext also lingers in the container environment
//! (`/proc/<pid>/environ`), which fails most compliance reviews.
//!
//! ## The model
//!
//! - [`SecretSource`] — *where* secrets come from (Vault, AWS Secrets Manager,
//! env for dev). The app implements it; the framework never links a cloud SDK
//! — the same rule that keeps `OAuth2Provider` implementations app-side.
//! - [`Rotating<T>`] — *how* live key material is held: an `ArcSwap`, so the
//! request hot path pays one atomic pointer load (no `Mutex`/`RwLock`),
//! while a background watcher swaps in new material atomically.
//! - [`spawn_secret_watcher`] — polls the source on an interval from
//! `ArclyPlugin::on_start` and invokes a callback when the version changes.
//!
//! Services that own derived key material (`JwtService`, `CookieService`)
//! hold a `Rotating<…bundle…>` internally and keep the **previous** key for
//! verification during a grace window, so rotation never mass-invalidates
//! tokens that are still inside their TTL.
use Arc;
use Duration;
use ArcSwap;
use BoxFuture;
use SecretString;
// ─── Source abstraction ───────────────────────────────────────────────────────
/// One fetched secret value plus a monotonically increasing version.
///
/// The version lets watchers detect change without comparing plaintext, and
/// guards against accidental rollback (a lower version is never applied).
/// External secret backend — Vault, AWS Secrets Manager, env (dev), …
///
/// Object-safe via `BoxFuture`, same pattern as `SessionStore`.
// ─── Lock-free rotating holder ────────────────────────────────────────────────
/// Atomically swappable key material.
///
/// Hot path: [`load`](Self::load) is a single atomic pointer load (~1 ns) —
/// no locks, no allocation. Control path: [`store`](Self::store) replaces the
/// whole bundle at once, so readers always observe a consistent key set.
// ─── Background watcher ───────────────────────────────────────────────────────
/// Poll `source` for `key` every `interval`; when the version increases,
/// invoke `on_change` with the new secret.
///
/// Spawn from `ArclyPlugin::on_start` — the existing lifecycle hook — so no
/// new mechanism is needed for graceful shutdown (the task dies with the
/// runtime). Fetch errors are logged and retried on the next tick; the
/// previous key material stays live, so a flaky source can never lock the
/// service out of its own keys.