holger-front 0.1.2

holger.rs's public front page: the page itself, the three-column release row, and the whole door as one pure function
Documentation
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
//! **holger.rs's front page** — the whole public door, as one pure function.
//!
//! holger-server is a hand-rolled `hyper` dispatcher with no router, no
//! template engine and no static-asset tree; until now `GET /` answered `404
//! Unknown repository`. So this crate is not a framework and does not try to
//! be one: it is [`route`], a function from a method and a path to a
//! [`Reply`], and mounting it in `server/lib/src/exposed/http.rs` is four
//! lines next to the `/-/search` arm.
//!
//! That shape is the point. Everything the page *is* — the bytes, the JSON,
//! the cache headers, the refusals — is decided here, where it can be tested
//! in a crate that builds in seconds, rather than inside a request handler
//! that needs the whole server to compile.
//!
//! ## The two doors
//!
//! | path | what it answers |
//! |---|---|
//! | `GET /` | the page (one self-contained HTML file) |
//! | `GET /holger.webp` | the picture on its left half |
//! | `GET /-/front` | who this server is, and whether it has a browser login |
//! | `GET /-/releases` | the catalogue: the public repositories, and the latest version of each package |
//!
//! ## ★ The one rule about version numbers
//!
//! **This crate never decides which version is newest.** [`latest_by`] groups
//! and folds, but the comparison is a function the caller passes in, and the
//! caller is `server/lib`, which passes `retention::cmp_version` — the
//! comparator holger already uses to decide which artifact a retention sweep
//! must not delete.
//!
//! That is not indirection for its own sake. A front page that ranked versions
//! with its own comparator would be a SECOND source of truth for "the latest
//! release", and the first time somebody published `1.10.0` beside `1.9.0` the
//! page and `holger retention` would name different artifacts as the newest —
//! with no way to tell which of them was wrong.

use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::BTreeMap;

use holger_errcode as ec;

/// The page, as served. One file: holger-server has nowhere to serve a second
/// one from.
pub const PAGE: &str = include_str!("../assets/front.html");

/// The picture on the left half.
///
/// Produced from the repository's OWN `.nornir/assets/holger-znippy-logo.png`
/// by `holger-ops logo` — the same file the readme shows, converted once, in
/// Rust, to lossless WebP. Compiled in rather than read from disk, because a
/// front page whose picture depends on a deploy having copied a file is a
/// front page with a half-drawn state nobody tests.
pub const PICTURE: &[u8] = include_bytes!("../assets/holger.webp");

/// Where the picture is served, named once here and once in the page's
/// `--backdrop-image`. A test asserts they agree.
pub const PICTURE_PATH: &str = "/holger.webp";

/// **The makers' marks, compiled in for the same reason the picture is** — and
/// served from THIS origin rather than hotlinked. An avatar fetched off a third
/// party's CDN would need a hole in this page's policy and would hand every
/// visitor's address to that third party for two files under 4 kB. Copied from
/// gunnar-ui, so the estate's two fronts credit their makers identically.
pub const VETRA_MARK: &[u8] = include_bytes!("../assets/vetra.svg");
/// Where the Vetra wordmark is served. A WORDMARK, so the page sizes it by
/// height only (96x18 from its own 1380x260 viewBox); forcing it square would
/// squash five letters.
pub const VETRA_PATH: &str = "/vetra.svg";
pub const IGNALINA_MARK: &[u8] = include_bytes!("../assets/ignalina.png");
/// Where the Ignalina mark is served — a square avatar, 18x18.
pub const IGNALINA_PATH: &str = "/ignalina.png";
pub const FRONT_PATH: &str = "/-/front";
pub const RELEASES_PATH: &str = "/-/releases";

// ─────────────────────────────────────────────────────────────────────────────
// The three columns
// ─────────────────────────────────────────────────────────────────────────────

/// **One row of the front page's table, and there is no fourth field.**
///
/// Size, content type, checksum, upload time and namespace all exist on
/// [`holger_traits::ArtifactEntry`](https://codeberg.org/nordisk/holger) and
/// none of them is here. The page a stranger reads first answers one question
/// — what is in this server and how new is it — and every further column is
/// noise around the answer. The console has the rest.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Release {
    /// The holger repository the artifact lives in (`crates-mirror`, `bundles`).
    pub repository: String,
    /// The artifact's name. A namespaced coordinate (maven `groupId`, npm
    /// scope) is rendered `namespace/name` by [`artifact_name`] — one string,
    /// because the column is one column.
    pub artifact: String,
    /// The version, verbatim. Never parsed, never normalised, never
    /// re-rendered: whatever was published is what is shown.
    pub version: String,
}

/// **One public repository, as a stranger may see it.**
///
/// ★ Why this exists beside [`Release`]: a repository with nothing in it
/// produced NO rows, so a freshly installed holger — or one whose store is
/// still empty, which is what holger.rs is today — answered `/-/releases` with
/// `[]` and the front page said "Nothing published yet." and named nothing at
/// all. That is a true sentence about the artifacts and a useless page about
/// the server: the repositories are configured, they are public, and they are
/// the first thing a visitor needs in order to point a package manager
/// anywhere.
///
/// So the roster is listed whether or not anything has been published into it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Repository {
    /// The repository's name, which is also its first path segment on this
    /// server.
    pub name: String,
    /// The format it speaks — `cargo`, `maven`, `npm`, `generic`. Verbatim from
    /// the server; never mapped to a prettier word here, because the word the
    /// server uses is the word the operator configured.
    pub format: String,
    /// How many distinct packages the listing found in it. `0` is an answer and
    /// is printed as one — an empty repository is a normal state.
    pub packages: usize,
}

/// **What `/-/releases` answers: the roster and the rows, in one document.**
///
/// One fetch, because it is one question — *what is in this server* — and two
/// fetches would be two chances for the page to render half an answer.
///
/// It is an object and not an array, and that is a wire change with a reason:
/// the array could only ever carry artifacts, so a server with repositories and
/// no artifacts had no way to say so.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Catalogue {
    /// Every repository a stranger may list. Already ordered by the caller.
    pub repositories: Vec<Repository>,
    /// The newest version of each package, already ranked and ordered by the
    /// caller. See the module note on why this crate ranks nothing.
    pub releases: Vec<Release>,
}

/// How a namespaced coordinate becomes the single string in the middle column.
///
/// `Some("org.apache.arrow") + "arrow-vector"` -> `org.apache.arrow/arrow-vector`;
/// `None + "serde"` -> `serde`. An empty namespace is the same as none — some
/// backends hand back `Some("")` and a leading `/` in the column would be a
/// visible bug for an invisible cause.
pub fn artifact_name(namespace: Option<&str>, name: &str) -> String {
    match namespace {
        Some(ns) if !ns.is_empty() => format!("{ns}/{name}"),
        _ => name.to_string(),
    }
}

/// Fold every `(repository, artifact, version)` down to the newest version of
/// each `(repository, artifact)`, using `newer` to compare.
///
/// `newer(a, b)` must answer the ordering of `a` against `b` as VERSIONS. Pass
/// `retention::cmp_version`; do not pass `str::cmp`, which puts `1.9.0` after
/// `1.10.0` and is the whole reason this is a parameter.
///
/// The result is sorted by repository, then artifact — a stable order, so the
/// page does not reshuffle between two loads of an unchanged server.
pub fn latest_by<F>(rows: impl IntoIterator<Item = Release>, newer: F) -> Vec<Release>
where
    F: Fn(&str, &str) -> Ordering,
{
    // A BTreeMap, not a HashMap: the key order IS the output order, so the
    // sort is free and cannot be forgotten.
    let mut best: BTreeMap<(String, String), String> = BTreeMap::new();
    for r in rows {
        let key = (r.repository, r.artifact);
        match best.get(&key) {
            // `Greater` only — a tie keeps the FIRST one seen, so a repository
            // that somehow lists one version twice does not flap between loads.
            Some(have) if newer(&r.version, have) != Ordering::Greater => {}
            _ => {
                best.insert(key, r.version);
            }
        }
    }
    best.into_iter()
        .map(|((repository, artifact), version)| Release { repository, artifact, version })
        .collect()
}

// ─────────────────────────────────────────────────────────────────────────────
// Who this server is
// ─────────────────────────────────────────────────────────────────────────────

/// ★ **WHICH OF THE THREE STATES the door is in — the third one is new.**
///
/// There were two: no login at all ([`Front::login`] is `None`), and a login
/// somewhere else on this origin (a link the button follows). Neither of them
/// is what holger.rs needs, because on holger.rs the console IS this origin:
/// the link sent a visitor to `/login`, a second page with the same button on
/// it, and the reader pressed the same thing twice to reach one ceremony.
///
/// So there is a third: the ceremony runs **on this page**. The page holds the
/// WebAuthn call itself — `navigator.credentials.get` against the paths the
/// server names — and there is no navigation until it has succeeded.
///
/// It is a field and not a separate type because the airgapped appliance
/// serves this same crate with `/auth/*` absent entirely. The appliance
/// answers [`Door::Link`] or `None` and the ceremony code on the page is never
/// reached; nothing about the ceremony is compiled into the appliance's
/// choices, it is markup and script that a server has to opt into by naming
/// three paths.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Door {
    /// The button is a link: press it and the browser goes to `start`.
    ///
    /// The default, and it is the default on purpose: a `/-/front` document
    /// written before this field existed deserialises as a link, which is what
    /// it was.
    #[default]
    Link,
    /// The ceremony is HERE. `start`, `finish` and `next` are all rooted paths
    /// on this origin and the page never leaves until `finish` said yes.
    Here,
}

/// The door a browser can actually walk through, if there is one.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Login {
    /// What the button says. **The server's word, not the page's.**
    pub label: String,
    /// Where the button goes. A same-site rooted path; the page refuses
    /// anything else, so a misconfigured server cannot turn the button into an
    /// open redirect.
    ///
    /// For [`Door::Here`] this is the ceremony's START path, fetched rather
    /// than navigated to.
    pub start: String,
    /// Which of the two doors this is. Defaulted, so an older document still
    /// reads.
    #[serde(default)]
    pub door: Door,
    /// [`Door::Here`] only: where the signed assertion is POSTed. `None` for a
    /// link, and the page treats a `Here` with no `finish` as a link, because
    /// a ceremony with nowhere to finish is not a ceremony.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub finish: Option<String>,
    /// [`Door::Here`] only: where the browser goes once the session cookie is
    /// set. A same-site rooted path like the others; `None` means `/`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next: Option<String>,
}

/// What the button says when the door is the console.
///
/// Here rather than in the server for the reason [`Front::no_login_refusal`] is
/// here: the page prints it verbatim, so it exists in one place and a test can
/// hold the page to it.
pub const CONSOLE_LABEL: &str = "Open the console";

/// ★ **What the button says when the ceremony is on this page, and it is the
/// estate's word.**
///
/// Byte-for-byte gunnar's button
/// (`gunnar-ui/crates/gunnar-ui-server/src/auth/login.html`): the two fronts in
/// one estate name the same act the same way. "Open the console" describes a
/// navigation, and there is no navigation any more — the label has to say what
/// pressing it actually does.
pub const PASSKEY_LABEL: &str = "Log in with passkey";

/// ★ **The console door, or `None` — and `None` is the answer for anything the
/// PAGE would refuse to follow.**
///
/// The page's button follows only a same-site rooted path (see the `btn.onclick`
/// guard in `assets/front.html`), which is the open-redirect defence and is not
/// negotiable. So an operator who points this at `https://console.example.com`
/// would get a button that is enabled, labelled, and does **nothing at all** on
/// click — strictly worse than the honest dead button
/// [`Front::no_login_refusal`] explains, because there is no sentence anywhere
/// saying why.
///
/// This constructor therefore applies the page's own rule **server-side**, and
/// answers `None` for every value the page would silently drop. The two rules
/// are asserted equal by
/// [`the_rust_rule_and_the_pages_rule_refuse_the_same_values`] — one rule, two
/// languages, held together the way this crate holds its sentences together.
///
/// Note what this means for a deployment, because it is a real constraint and
/// not a detail: **the console must be reachable at a path on THIS server's
/// origin.** A console on its own host has no rooted path from here, and the
/// honest answer for it is the one the page already gives.
pub fn console_login(path: &str) -> Option<Login> {
    if !same_site_rooted_path(path) {
        return None;
    }
    Some(Login {
        label: CONSOLE_LABEL.to_string(),
        start: path.to_string(),
        door: Door::Link,
        finish: None,
        next: None,
    })
}

/// ★ **The third state: the passkey ceremony, on this page.**
///
/// `start` and `finish` are the server's own ceremony doors and `next` is where
/// a successful login lands. All three go through `same_site_rooted_path`,
/// the same rule [`console_login`] applies, and **any one of them failing it
/// answers `None`** — not a partly-wired ceremony. A page that fetched a
/// ceremony from one origin and posted the assertion to another is not a door
/// with a bug in it, it is a different thing entirely.
///
/// The label is [`PASSKEY_LABEL`] and the server does not get to choose it: the
/// button's words and the button's behaviour are one decision, and a server
/// that could say "Open the console" over a ceremony that never navigates would
/// be lying to the reader in the one place the reader is looking.
pub fn passkey_login_here(start: &str, finish: &str, next: &str) -> Option<Login> {
    if !same_site_rooted_path(start)
        || !same_site_rooted_path(finish)
        || !same_site_rooted_path(next)
    {
        return None;
    }
    Some(Login {
        label: PASSKEY_LABEL.to_string(),
        start: start.to_string(),
        door: Door::Here,
        finish: Some(finish.to_string()),
        next: Some(next.to_string()),
    })
}

/// The page's `btn.onclick` rule, in Rust.
///
/// One leading slash, no authority, and no character the URL parser strips
/// before it decides what the value even is: `//host` and `/\host` are off-site
/// in every engine, and a value carrying a space, a tab, a newline or a NUL is
/// one whose meaning is settled after those are removed.
fn same_site_rooted_path(path: &str) -> bool {
    !path.is_empty()
        && path.starts_with('/')
        && !path.starts_with("//")
        && !path.starts_with("/\\")
        // The page's class is `[\x00-\x20\x7f]` — written in `assets/front.html`
        // as literal control bytes, which is why that file reads as binary.
        // `is_ascii_whitespace` is NOT this set (it omits NUL, the other C0
        // controls and DEL), so the range is spelled out rather than borrowed.
        && !path.chars().any(|c| (c as u32) <= 0x20 || c as u32 == 0x7f)
}

/// What `/-/front` answers: everything on the page that is a fact about THIS
/// server rather than about holger.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Front {
    /// The base URL a package manager is pointed at. `None` when the server
    /// was not told its own public name — printed by the server or not at all,
    /// because a page that guessed it from `location.origin` would print the
    /// proxy's address on any deployment behind one.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub base_url: Option<String>,
    /// ★ **`None` is an ANSWER, and it is the honest one today.**
    ///
    /// holger-server authenticates with mTLS, OIDC and bearer tokens — doors
    /// for `cargo`, `pip`, `docker` and the native console. There is no
    /// browser session, so there is no button, and the page says
    /// [`ec::FRONT_NO_BROWSER_LOGIN`] by name rather than offering one.
    ///
    /// It is serialised even when null (no `skip_serializing_if`) precisely so
    /// the page can tell a server with no login from a server too old to have
    /// the field.
    pub login: Option<Login>,
}

impl Front {
    /// A server that has told us nothing but its own address.
    pub fn anonymous(base_url: Option<String>) -> Self {
        Front { base_url, login: None }
    }

    /// The refusal the page prints when [`Front::login`] is `None`, by name
    /// and with its code. Kept here rather than only in the page's JavaScript
    /// so the words exist in one place and a test can hold them to it.
    pub fn no_login_refusal() -> String {
        format!(
            "{}: this server offers no browser login. Its doors are mTLS, OIDC and bearer — \
             for cargo, pip, docker and the console, not for a tab.",
            ec::FRONT_NO_BROWSER_LOGIN.code
        )
    }
}

/// ★ **The three sentences the PAGE prints on its own.**
///
/// They are raised in JavaScript, where no Rust compiler can see them and the
/// frozen registry cannot be consulted — which is exactly how a page ends up
/// showing a code nobody allocated, or the same failure worded two ways in two
/// places. So each one is built HERE, from the registry row, and the tests
/// assert the page carries the string this produces. Change the sentence here
/// and the test tells you the page has drifted; change it in the page and the
/// test tells you the same thing.
///
/// This is also what makes the rows honest to the wiring guard: a code marked
/// `wired` must be referenced from the file it claims, and these are the
/// references.
pub fn page_refusals() -> [(&'static ec::ErrCode, String); 3] {
    [
        (&ec::FRONT_NO_BROWSER_LOGIN, Front::no_login_refusal()),
        (
            &ec::FRONT_READS_GATED,
            format!(
                "{}: this server gates reads behind a credential, so its front page cannot name \
                 itself. Configure `require_auth_for_reads: false`, or read the console instead.",
                ec::FRONT_READS_GATED.code
            ),
        ),
        (
            &ec::FRONT_DOOR_ABSENT,
            format!(
                "{}: this build of holger-server has no `/-/front` door. The page is newer than \
                 the server behind it.",
                ec::FRONT_DOOR_ABSENT.code
            ),
        ),
    ]
}

// ─────────────────────────────────────────────────────────────────────────────
// The door
// ─────────────────────────────────────────────────────────────────────────────

/// One answer: a status, a content type, headers that matter, and the bytes.
#[derive(Debug, Clone, PartialEq)]
pub struct Reply {
    pub status: u16,
    pub content_type: &'static str,
    /// `Cache-Control`. Named rather than implied: the page and the JSON must
    /// NOT be cached (a stale release table is a lie about what is published)
    /// and the picture must be, because it is 300 kB that never changes
    /// without a redeploy.
    pub cache_control: &'static str,
    pub body: Vec<u8>,
}

impl Reply {
    fn html(body: &str) -> Reply {
        Reply {
            status: 200,
            content_type: "text/html; charset=utf-8",
            cache_control: "no-store",
            body: body.as_bytes().to_vec(),
        }
    }
    fn json(body: String) -> Reply {
        Reply {
            status: 200,
            content_type: "application/json",
            cache_control: "no-store",
            body: body.into_bytes(),
        }
    }
    fn webp(bytes: &'static [u8]) -> Reply {
        Reply {
            status: 200,
            content_type: "image/webp",
            // A year, and immutable: the file's content is compiled into the
            // binary, so it cannot change without a new binary, and a new
            // binary is a new deploy.
            cache_control: "public, max-age=31536000, immutable",
            body: bytes.to_vec(),
        }
    }
    /// A mark, cached like the picture: compiled into the binary, so it cannot
    /// change without a new deploy.
    fn mark(bytes: &'static [u8], content_type: &'static str) -> Reply {
        Reply {
            status: 200,
            content_type,
            cache_control: "public, max-age=31536000, immutable",
            body: bytes.to_vec(),
        }
    }
    fn refused(status: u16, code: &ec::ErrCode, detail: &str) -> Reply {
        Reply {
            status,
            content_type: "application/json",
            cache_control: "no-store",
            body: serde_json::json!({ "code": code.code, "error": detail }).to_string().into_bytes(),
        }
    }
}

/// What the server must hand the door to answer it.
///
/// It is a struct and not four arguments so that adding a fact to the front
/// page is a field here and a line in the caller, never a new function.
pub struct Doors<'a> {
    pub front: &'a Front,
    /// The roster and the rows. Already the latest, already ordered. The door
    /// does not rank — see the module note on why.
    pub catalogue: &'a Catalogue,
}

/// ★ **The whole public surface, as a pure function.**
///
/// `None` means "not mine" — the caller falls through to its own routing, so
/// mounting this cannot shadow a repository. Every path it DOES own is under
/// `/` or the already-reserved `/-/` namespace, which can never route into a
/// repository.
pub fn route(method: &str, path: &str, doors: &Doors<'_>) -> Option<Reply> {
    let owned = matches!(
        path,
        "/" | PICTURE_PATH | VETRA_PATH | IGNALINA_PATH | FRONT_PATH | RELEASES_PATH
    );
    if !owned {
        return None;
    }

    // ★ Read-only, and the refusal is by name. A `POST /` that fell through to
    // the repository router would be a write attempt against a repository
    // called "" — a 404 that looks like a typo instead of a method that is not
    // allowed here.
    if method != "GET" && method != "HEAD" {
        return Some(Reply::refused(
            405,
            &ec::FRONT_METHOD_NOT_ALLOWED,
            "the front door answers GET and HEAD only",
        ));
    }

    let mut reply = match path {
        "/" => Reply::html(PAGE),
        PICTURE_PATH => Reply::webp(PICTURE),
        VETRA_PATH => Reply::mark(VETRA_MARK, "image/svg+xml"),
        IGNALINA_PATH => Reply::mark(IGNALINA_MARK, "image/png"),
        FRONT_PATH => Reply::json(
            serde_json::to_string(doors.front).unwrap_or_else(|_| "{}".to_string()),
        ),
        RELEASES_PATH => Reply::json(
            serde_json::to_string(doors.catalogue)
                .unwrap_or_else(|_| r#"{"repositories":[],"releases":[]}"#.to_string()),
        ),
        _ => unreachable!("`owned` above is the same list"),
    };
    // HEAD is the same answer without the bytes. Written here rather than left
    // to the caller, because a HEAD that returned the body would be the kind
    // of thing nothing notices until a health check downloads the picture
    // every thirty seconds.
    if method == "HEAD" {
        reply.body.clear();
    }
    Some(reply)
}

/// The refusal for a server whose reads are gated, so `/-/front` cannot be
/// read at all. Produced by the CALLER, which is the half that knows about
/// `require_auth_for_reads`; it lives here so the code and the words are in
/// the same place as the page that prints them.
pub fn reads_gated() -> Reply {
    Reply::refused(
        403,
        &ec::FRONT_READS_GATED,
        "this server gates reads behind a credential, so its front page cannot name itself",
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A comparator that is WRONG on purpose — plain string order, which puts
    /// `1.9.0` above `1.10.0`. Used to prove the fold asks the comparator it
    /// was given and holds no opinion of its own.
    fn lexicographic(a: &str, b: &str) -> Ordering {
        a.cmp(b)
    }

    /// A toy numeric comparator standing in for `retention::cmp_version`, so
    /// the grouping can be tested without depending on `server/lib`.
    fn numeric(a: &str, b: &str) -> Ordering {
        let parts = |v: &str| v.split('.').map(|p| p.parse::<u64>().unwrap_or(0)).collect::<Vec<_>>();
        parts(a).cmp(&parts(b))
    }

    fn rel(repo: &str, art: &str, ver: &str) -> Release {
        Release { repository: repo.into(), artifact: art.into(), version: ver.into() }
    }

    // ── The console door ─────────────────────────────────────────────────────

    /// A path the page will follow becomes a button, labelled in the server's
    /// own word.
    #[test]
    fn a_rooted_path_becomes_the_console_button() {
        let login = console_login("/console").expect("a rooted path is a door");
        assert_eq!(login.start, "/console");
        assert_eq!(login.label, CONSOLE_LABEL);
    }

    /// ★ **The Rust rule and the PAGE's rule refuse exactly the same values.**
    ///
    /// This is the test the whole constructor exists for. The page's
    /// `btn.onclick` drops a value it will not follow **silently** — no
    /// sentence, no code, just a button that does nothing — so a server that
    /// answered with one would produce the one failure state the front page's
    /// design is otherwise free of. Rather than trust two prose descriptions of
    /// one rule, the page's own class is read out of [`PAGE`] and applied here.
    ///
    /// It is spelled `\x00-\x20` plus `\x7f`, and since 2026-09-20 it is
    /// written in the HTML as JS ESCAPES and not as literal control bytes.
    /// That is not a style preference, it is the fix for a page that hung:
    /// the literal NUL made `assets/front.html` read as binary to `file(1)`,
    /// and in a browser the HTML tokenizer's script-data state replaces a
    /// U+0000 with U+FFFD, so the engine parsed `/[\u{fffd}-\u{20}\u{7f}]/`,
    /// refused it as "range out of order in character class", and threw away
    /// the WHOLE inline script — the `/-/front` fetch with it. The page then
    /// sat on its literal `reading the server …` with the button still
    /// `disabled` for ever. [`the_page_carries_no_raw_control_bytes`] is the
    /// guard that keeps a literal from coming back.
    #[test]
    fn the_rust_rule_and_the_pages_rule_refuse_the_same_values() {
        // The page still carries the guard this mirrors. If the button's
        // handler is rewritten, this test must be re-read, not re-blessed.
        assert!(
            PAGE.contains("if (!start.startsWith('/') || start.startsWith('//') || start.startsWith('/\\\\')) return false;"),
            "the page's same-site guard has moved; console_login may no longer mirror it"
        );
        assert!(
            PAGE.contains(r"if (/[\x00-\x20\x7f]/.test(start)) return false;"),
            "the page's control-character class has changed; same_site_rooted_path is now a second opinion"
        );

        for refused in [
            "",                            // the page returns early on empty
            "https://console.example.com", // an off-site URL — the tempting mistake
            "console",                     // a relative path is not rooted
            "//evil.example.com",          // protocol-relative: off-site everywhere
            "/\\evil.example.com",         // backslash authority: off-site everywhere
            "/console\u{0}",               // NUL
            "/console\u{9}",               // tab
            "/console\u{a}",               // newline
            "/con sole",                   // space
            "/console\u{7f}",              // DEL — the one the first draft missed
        ] {
            assert!(
                console_login(refused).is_none(),
                "{refused:?} must not become a button: the page would drop it in silence"
            );
        }
    }

    /// An unconfigured console leaves the page exactly as it is today — the
    /// named refusal, not a button to nowhere. A self-hoster who runs no
    /// console must not be offered one.
    #[test]
    fn no_console_configured_is_still_the_named_refusal() {
        let f = Front { base_url: None, login: None };
        let body = serde_json::to_string(&f).unwrap();
        assert!(body.contains("\"login\":null"), "{body}");
        assert!(Front::no_login_refusal().starts_with(ec::FRONT_NO_BROWSER_LOGIN.code));
    }

    /// ★ **The fold has no opinion about versions.** The same input with two
    /// comparators gives two different answers — which is the proof that the
    /// ranking comes from the caller and not from this crate.
    #[test]
    fn the_comparator_decides_and_this_crate_does_not() {
        let rows = vec![rel("crates", "serde", "1.9.0"), rel("crates", "serde", "1.10.0")];
        assert_eq!(latest_by(rows.clone(), numeric)[0].version, "1.10.0");
        assert_eq!(latest_by(rows, lexicographic)[0].version, "1.9.0");
    }

    /// One row per `(repository, artifact)`, and the same artifact name in two
    /// repositories is two rows — a mirror and a local store legitimately hold
    /// different versions of `serde`, and collapsing them would hide it.
    #[test]
    fn one_row_per_repository_and_artifact() {
        let out = latest_by(
            vec![
                rel("crates", "serde", "1.0.1"),
                rel("crates", "serde", "1.0.9"),
                rel("mirror", "serde", "1.0.4"),
                rel("crates", "tokio", "1.2.0"),
            ],
            numeric,
        );
        assert_eq!(out.len(), 3);
        assert_eq!(
            out.iter().map(|r| (r.repository.as_str(), r.artifact.as_str(), r.version.as_str())).collect::<Vec<_>>(),
            vec![("crates", "serde", "1.0.9"), ("crates", "tokio", "1.2.0"), ("mirror", "serde", "1.0.4")]
        );
    }

    /// The order is stable and does not depend on the order rows arrived in.
    /// A page that reshuffled between two loads of an unchanged server would
    /// read as a server that is changing.
    #[test]
    fn the_order_does_not_depend_on_the_input_order() {
        let a = vec![rel("b", "y", "1"), rel("a", "z", "1"), rel("a", "y", "1")];
        let mut b = a.clone();
        b.reverse();
        assert_eq!(latest_by(a, numeric), latest_by(b, numeric));
    }

    #[test]
    fn a_namespace_joins_the_name_with_one_slash_and_an_empty_one_does_not() {
        assert_eq!(artifact_name(Some("org.apache.arrow"), "arrow-vector"), "org.apache.arrow/arrow-vector");
        assert_eq!(artifact_name(Some("@scope"), "pkg"), "@scope/pkg");
        assert_eq!(artifact_name(None, "serde"), "serde");
        assert_eq!(artifact_name(Some(""), "serde"), "serde", "an empty namespace put a slash on the front");
    }

    // ── the door ─────────────────────────────────────────────────────────────

    fn doors() -> (Front, Catalogue) {
        (
            Front::anonymous(Some("https://holger.rs".into())),
            Catalogue {
                repositories: vec![Repository {
                    name: "bundles".into(),
                    format: "generic".into(),
                    packages: 1,
                }],
                releases: vec![rel("bundles", "site-a", "3")],
            },
        )
    }

    #[test]
    fn the_page_is_served_at_the_root_and_is_not_cached() {
        let (f, r) = doors();
        let d = Doors { front: &f, catalogue: &r };
        let reply = route("GET", "/", &d).expect("the root is this door's");
        assert_eq!(reply.status, 200);
        assert_eq!(reply.content_type, "text/html; charset=utf-8");
        assert_eq!(reply.cache_control, "no-store", "the page must not be cached");
        assert_eq!(reply.body, PAGE.as_bytes());
    }

    /// The picture is cached hard, because it cannot change without a new
    /// binary — and the JSON is not, because it changes whenever anybody
    /// publishes.
    #[test]
    fn the_picture_is_cached_forever_and_the_json_never() {
        let (f, r) = doors();
        let d = Doors { front: &f, catalogue: &r };
        let pic = route("GET", PICTURE_PATH, &d).unwrap();
        assert_eq!(pic.content_type, "image/webp");
        assert!(pic.cache_control.contains("immutable"), "{}", pic.cache_control);
        for p in [FRONT_PATH, RELEASES_PATH] {
            assert_eq!(route("GET", p, &d).unwrap().cache_control, "no-store", "{p} was cacheable");
        }
    }

    /// ★ **The picture the page names is the picture the door serves.** The
    /// path is written twice — once in CSS, once as a constant — and this is
    /// what keeps the two from drifting into a broken left half that nobody
    /// notices because the layout still works.
    #[test]
    fn the_page_asks_for_the_picture_this_door_serves() {
        assert!(
            PAGE.contains(&format!("url(\"{PICTURE_PATH}\")")),
            "the page's --backdrop-image does not name {PICTURE_PATH}"
        );
        assert!(PAGE.contains(RELEASES_PATH), "the page does not fetch {RELEASES_PATH}");
        assert!(PAGE.contains(FRONT_PATH), "the page does not fetch {FRONT_PATH}");
    }

    /// ★ **The page carries no raw control byte, because one of them silently
    /// deleted the whole inline script.**
    ///
    /// MEASURED 2026-09-20 on the live holger.rs: `assets/front.html` held a
    /// literal U+0000 inside the `btn.onclick` guard's regex. `curl` saw a
    /// perfectly good 23 115-byte document and `/-/front` answered 200; a
    /// browser did not. The HTML tokenizer's script-data state turns U+0000
    /// into U+FFFD before the JS engine ever sees it, so the engine was handed
    /// `/[\u{fffd}-\u{20}\u{7f}]/`, whose range runs backwards. That is an
    /// early SyntaxError, which kills the ENTIRE `<script>` element — the
    /// `/-/front` fetch, the button's label, the button's `disabled = false`,
    /// all of it — and leaves the page showing its own literal placeholder
    /// `reading the server …` next to a grey button that cannot be pressed.
    ///
    /// The class is therefore spelled with escapes now, and this test is the
    /// reason a literal cannot come back. `\t`, `\n` and `\r` are the three
    /// bytes a text document is allowed to contain.
    #[test]
    fn the_page_carries_no_raw_control_bytes() {
        for (i, b) in PAGE.bytes().enumerate() {
            let allowed = matches!(b, b'\t' | b'\n' | b'\r');
            assert!(
                allowed || !(b.is_ascii_control() || b == 0x7f),
                "assets/front.html carries a raw control byte {b:#04x} at offset {i}; \
                 a browser replaces U+0000 with U+FFFD in script data and throws the whole \
                 inline script away. Write it as a JS escape."
            );
        }
    }

    /// The picture really is a WebP, and really is the one that was converted
    /// — not a PNG somebody renamed.
    #[test]
    fn the_compiled_in_picture_is_a_webp() {
        assert!(PICTURE.len() > 12, "the picture is empty — run `holger-ops logo`");
        assert_eq!(&PICTURE[0..4], b"RIFF", "not a RIFF container");
        assert_eq!(&PICTURE[8..12], b"WEBP", "not a WebP");
    }

    /// ★ **Nothing but this door's own four paths is claimed.** A door that
    /// answered `/crates-mirror` would shadow a repository, and it would do it
    /// silently — the repository would simply stop existing.
    #[test]
    fn a_repository_path_is_never_this_doors() {
        let (f, r) = doors();
        let d = Doors { front: &f, catalogue: &r };
        for p in ["/crates-mirror", "/v2/alpine/manifests/latest", "/-/search", "/healthz", "/bundles/x", ""] {
            assert!(route("GET", p, &d).is_none(), "{p} was claimed by the front door");
        }
    }

    /// A write against the front door is refused BY NAME with a code, not
    /// dropped into the repository router where it becomes a confusing 404.
    #[test]
    fn a_write_is_refused_by_name_with_its_code() {
        let (f, r) = doors();
        let d = Doors { front: &f, catalogue: &r };
        for m in ["POST", "PUT", "DELETE", "PATCH"] {
            let reply = route(m, "/", &d).unwrap();
            assert_eq!(reply.status, 405, "{m}");
            let body = String::from_utf8(reply.body).unwrap();
            assert!(body.contains(ec::FRONT_METHOD_NOT_ALLOWED.code), "{m}: {body}");
        }
    }

    #[test]
    fn head_answers_the_same_thing_without_the_bytes() {
        let (f, r) = doors();
        let d = Doors { front: &f, catalogue: &r };
        for p in ["/", PICTURE_PATH, FRONT_PATH, RELEASES_PATH] {
            let get = route("GET", p, &d).unwrap();
            let head = route("HEAD", p, &d).unwrap();
            assert_eq!(head.status, get.status);
            assert_eq!(head.content_type, get.content_type);
            assert!(head.body.is_empty(), "HEAD {p} carried a body");
        }
    }

    /// The three columns go over the wire under the names the page reads, and
    /// there is no fourth.
    #[test]
    fn the_wire_carries_exactly_three_columns() {
        let (f, r) = doors();
        let d = Doors { front: &f, catalogue: &r };
        let body = String::from_utf8(route("GET", RELEASES_PATH, &d).unwrap().body).unwrap();
        let doc: serde_json::Value = serde_json::from_str(&body).unwrap();
        let rows: Vec<serde_json::Map<String, serde_json::Value>> =
            serde_json::from_value(doc["releases"].clone()).unwrap();
        assert_eq!(rows.len(), 1);
        // A SET, not a sequence: `serde_json::Map` is a BTreeMap without the
        // `preserve_order` feature, so the wire order is alphabetical and
        // asserting declaration order here would only be asserting serde's
        // build configuration. The COLUMN order is the page's, and it is
        // checked where it lives — in `the_page_is_holgers`, against the
        // `<th>` headings a reader actually sees.
        let mut keys: Vec<&str> = rows[0].keys().map(|k| k.as_str()).collect();
        keys.sort_unstable();
        assert_eq!(keys, vec!["artifact", "repository", "version"], "the row grew or lost a column");
    }

    /// ★ **`login: null` is sent, not omitted.** The page must be able to tell
    /// a server that HAS no browser login from a server too old to have the
    /// field — they need different sentences, and a skipped field makes them
    /// the same byte sequence.
    #[test]
    fn a_server_with_no_login_says_so_rather_than_saying_nothing() {
        let f = Front::anonymous(None);
        let body = serde_json::to_string(&f).unwrap();
        assert!(body.contains("\"login\":null"), "{body}");
        assert!(!body.contains("base_url"), "an absent base URL should not be sent at all: {body}");
    }

    /// The refusal the page prints carries the registry's code, and the page
    /// and the Rust say the same words — one sentence, two places, held
    /// together by this.
    #[test]
    fn the_no_login_refusal_is_the_same_sentence_in_the_page_and_in_the_code() {
        let r = Front::no_login_refusal();
        assert!(r.starts_with(ec::FRONT_NO_BROWSER_LOGIN.code), "{r}");
        assert!(PAGE.contains(&r), "the page's sentence has drifted from Front::no_login_refusal():\n{r}");
    }

    /// Every code this door and its page can show is a row in the FROZEN
    /// registry. A refusal with a number nobody allocated is a refusal nobody
    /// can look up.
    #[test]
    fn every_code_the_page_prints_is_in_the_registry() {
        for code in [
            ec::FRONT_METHOD_NOT_ALLOWED,
            ec::FRONT_NO_BROWSER_LOGIN,
            ec::FRONT_READS_GATED,
            ec::FRONT_DOOR_ABSENT,
        ] {
            assert_eq!(code.subsystem, "front");
            assert!(holger_errcode::ALL.iter().any(|c| c.code == code.code), "{} is not in ALL", code.code);
        }
    }

    /// ★ **Every sentence the page prints is the sentence this crate builds.**
    /// The page raises three refusals in JavaScript, where nothing checks
    /// them; this is the check. A word changed on either side fails here,
    /// naming which.
    #[test]
    fn the_pages_refusals_are_the_ones_this_crate_words() {
        for (code, sentence) in page_refusals() {
            assert!(sentence.starts_with(code.code), "{sentence}");
            assert!(
                PAGE.contains(&sentence),
                "the page has drifted from the wording of {}:\n  expected: {sentence}",
                code.code
            );
        }
    }

    /// ★ **No gunnar sentence survived the copy.** The page's shape is
    /// gunnar's login page and its words must be holger's; a page that says
    /// gunnar's sentences under holger's logo is worse than one that says
    /// nothing. This is the test that catches a paste.
    ///
    /// `vetra` and `ignalina` were on this list until 2026-09-17 and are
    /// DELIBERATELY off it now. They were never gunnar's words — they are the
    /// two companies that make both products, and the credit line is now
    /// gunnar's block verbatim BY INSTRUCTION, marks and all, so the estate's
    /// two fronts credit their makers identically instead of one crediting
    /// companies and the other listing two people and a repository URL. What
    /// this test exists to catch is a gunnar SENTENCE arriving under holger's
    /// logo; a shared maker is not that, and keeping them here would make the
    /// guard fail on the one paste that was asked for.
    #[test]
    ///
    /// `passkey` and `webauthn` were on this list until 2026-09-20 and are off
    /// it now for a reason that is not a relaxation: they were banned because
    /// holger-server HAD no browser session, so either word on this page could
    /// only have arrived by paste. The console's ceremony now runs ON this page
    /// ([`Door::Here`]), so "passkey" is holger's own word for holger's own
    /// door. `gunnar`, `badger` and `git server` stay: those describe a
    /// different product and can still only arrive by paste.
    #[test]
    fn the_page_says_nothing_about_gunnar() {
        let lower = PAGE.to_lowercase();
        for word in ["gunnar", "badger", "git server"] {
            assert!(!lower.contains(word), "the page still says `{word}`");
        }
    }

    /// ★ **The credit names the makers and serves their marks from HERE.** The
    /// page carried "Rickard Lundin & Henrik Torp · codeberg.org/nordisk/holger"
    /// until 2026-09-17; it now carries gunnar's block. Both marks are compiled
    /// in and routed by this door, because a credit whose images 404 is worse
    /// than a credit in plain text.
    #[test]
    fn the_credit_names_both_makers_and_this_door_serves_their_marks() {
        assert!(PAGE.contains("Vetra AB"), "the credit does not name Vetra AB");
        assert!(PAGE.contains("Ignalina ApS"), "the credit does not name Ignalina ApS");
        assert!(!PAGE.contains("Rickard"), "the old people-and-repo credit is still here");
        assert!(!PAGE.contains("codeberg.org/nordisk/holger"), "the old repo link is still here");
        for path in [VETRA_PATH, IGNALINA_PATH] {
            assert!(PAGE.contains(path), "the page does not reference {path}");
            let (f, rel) = doors();
            let d = Doors { front: &f, catalogue: &rel };
            let r = route("GET", path, &d).unwrap_or_else(|| panic!("{path} is not served"));
            assert_eq!(r.status, 200, "{path} answered {}", r.status);
            assert!(!r.body.is_empty(), "{path} served an empty body");
        }
    }

    /// ★ **No price, no currency, no amount.** The console's discipline,
    /// carried over: holger's front page states what is published, and every
    /// number on it came from the server.
    #[test]
    fn the_page_names_no_price_and_no_currency() {
        for token in ["", "$", "£", "kr", "SEK", "EUR", "USD", "/month", "per month", "free tier", "pricing"] {
            assert!(!PAGE.contains(token), "the page carries `{token}`");
        }
    }

    /// The page says what holger is, in the repository's own words, and names
    /// the product it is a front for.
    #[test]
    fn the_page_is_holgers() {
        assert!(PAGE.contains("<h1>Holger</h1>"), "the page does not name the product");
        assert!(PAGE.contains("Immutable artifact repository"), "the tagline is not the readme's");
        assert!(PAGE.contains("Latest releases"), "the list is not named");
        // ★ The three columns, in the order a reader sees them. The JSON is
        // alphabetical and says nothing about this; the markup is where the
        // order lives, so the markup is where it is checked.
        let mut at = 0usize;
        for col in ["Repository", "Artifact", "Version"] {
            let head = format!(">{col}</th>");
            let found = PAGE.find(&head).unwrap_or_else(|| panic!("the `{col}` column heading is missing"));
            assert!(found > at, "the columns are out of order at `{col}`");
            at = found;
        }
        // …and the script fills them in that same order, so the heading and
        // the cell under it are the same field.
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        assert!(
            script.contains("[['repository', ''], ['artifact', ''], ['version', 'version']]"),
            "the script no longer fills the columns in the order the headings promise"
        );
    }

    /// The picture is on the LEFT: `.art` is the first child of `<body>` and
    /// takes the first half of a row. A `flex-direction: row-reverse` or a
    /// reordered body would move it, and it is one line either way — so it is
    /// asserted rather than trusted.
    #[test]
    fn the_picture_is_on_the_left() {
        let body = PAGE.split("<body>").nth(1).expect("the page has a body");
        let art = body.find("class=\"art\"").expect("the page has the picture half");
        let side = body.find("class=\"side\"").expect("the page has the column");
        assert!(art < side, "the picture is not the first half of the page");
        assert!(!PAGE.contains("row-reverse"), "something reversed the row and put the picture on the right");
        assert!(PAGE.contains(".art {\n    flex: 0 0 50%;"), "the picture no longer owns a half");
    }

    /// ★ **The page computes no version ordering.** The one rule of this
    /// crate, checked against the page's own script: no sort, no compare, no
    /// localeCompare. The server ranks; the page prints.
    #[test]
    fn the_page_does_not_rank_anything() {
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        for banned in [".sort(", "localeCompare", "parseFloat", "parseInt"] {
            assert!(!script.contains(banned), "the page's script carries `{banned}` — it is deciding something");
        }
    }

    /// Nothing is fetched from a third party. This server runs airgapped by
    /// design; a font or a script from a CDN would be a front page that is
    /// blank in exactly the deployment holger exists for.
    #[test]
    fn the_page_fetches_nothing_from_outside() {
        for scheme in ["http://", "//cdn", "googleapis", "cdnjs", "jsdelivr", "unpkg"] {
            assert!(!PAGE.contains(scheme), "the page reaches out to `{scheme}`");
        }
        // The one external link is the forge the source is published from, and
        // it is a link a reader clicks — not something the page loads.
        assert_eq!(PAGE.matches("https://").count(), 1, "the page names more than one external URL");
    }

    // ── the third state ──────────────────────────────────────────────────────

    /// ★★ **THE BUTTON SAYS "Log in with passkey" AND THE CEREMONY IS HERE.**
    ///
    /// The owner's words, twice: the same label as gunnar's, and no second page
    /// that shows the same thing again. Both halves are asserted, because
    /// either one alone is the bug that was shipped — a label over a
    /// navigation, or a ceremony behind a button that says "Open the console".
    #[test]
    fn the_ceremony_door_is_labelled_and_wired_to_this_page() {
        let login = passkey_login_here(
            "/auth/passkey/login/start",
            "/auth/passkey/login/finish",
            "/overview",
        )
        .expect("three rooted paths are a ceremony");
        assert_eq!(login.label, PASSKEY_LABEL);
        assert_eq!(login.label, "Log in with passkey");
        assert_eq!(login.door, Door::Here);
        assert_eq!(login.finish.as_deref(), Some("/auth/passkey/login/finish"));
        assert_eq!(login.next.as_deref(), Some("/overview"));

        // …and it goes over the wire naming its own state, because the page
        // branches on that word and nothing else.
        let body = serde_json::to_string(&Front {
            base_url: None,
            login: Some(login),
        })
        .unwrap();
        assert!(body.contains(r#""door":"here""#), "{body}");
    }

    /// The link door is unchanged and still says so on the wire. The appliance
    /// and any self-hoster with a console elsewhere on the origin keep exactly
    /// the page they had.
    #[test]
    fn a_link_door_still_says_link_and_carries_no_ceremony() {
        let login = console_login("/login?next=/overview").unwrap();
        assert_eq!(login.door, Door::Link);
        assert_eq!(login.label, CONSOLE_LABEL);
        assert!(login.finish.is_none() && login.next.is_none());
        let body = serde_json::to_string(&login).unwrap();
        assert!(body.contains(r#""door":"link""#), "{body}");
        assert!(!body.contains("finish"), "a link door sent a ceremony field: {body}");
    }

    /// A `/-/front` document written before `door` existed still reads, and
    /// reads as the link it was. The field defaults rather than failing, which
    /// is the whole reason it is a field on a struct and not a tag on an enum.
    #[test]
    fn a_document_from_before_this_field_is_still_a_link() {
        let old = r#"{"label":"Open the console","start":"/login"}"#;
        let login: Login = serde_json::from_str(old).unwrap();
        assert_eq!(login.door, Door::Link);
    }

    /// ★ **All three paths go through one rule, and one bad path is no door.**
    ///
    /// A half-wired ceremony — a start on this origin and a finish somewhere
    /// else — is not a door with a bug in it. It is the assertion being posted
    /// to a stranger.
    #[test]
    fn one_off_site_path_refuses_the_whole_ceremony() {
        let good = ("/auth/passkey/login/start", "/auth/passkey/login/finish", "/overview");
        assert!(passkey_login_here(good.0, good.1, good.2).is_some());
        for bad in ["https://evil.example.com/finish", "//evil.example.com", "/\\evil", "", "relative"] {
            assert!(passkey_login_here(bad, good.1, good.2).is_none(), "start {bad:?}");
            assert!(passkey_login_here(good.0, bad, good.2).is_none(), "finish {bad:?}");
            assert!(passkey_login_here(good.0, good.1, bad).is_none(), "next {bad:?}");
        }
    }

    /// ★ **The page holds the ceremony itself.** The three calls a WebAuthn
    /// login is made of are in the document, so the button cannot be a
    /// navigation dressed up as one.
    #[test]
    fn the_page_carries_the_ceremony_and_not_a_second_page() {
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        assert!(
            script.contains("navigator.credentials.get"),
            "the page does not call the authenticator — the button is still a link"
        );
        assert!(script.contains("allowCredentials"), "the page does not decode the challenge list");
        assert!(script.contains("clientDataJSON"), "the page does not send the assertion");
        // The LABEL is deliberately not in the page. It is the server's word —
        // `/-/front` names it and the page prints what it is handed — which is
        // why the button could say "Open the console" over a ceremony if the
        // two halves were decided in two places. They are decided in one:
        // `PASSKEY_LABEL` is set by `passkey_login_here` and by nothing else,
        // and `the_ceremony_door_is_labelled_and_wired_to_this_page` is where
        // the words are held.
        assert!(
            !PAGE.contains("Open the console"),
            "the page hard-codes a label; the server names the button"
        );
    }

    // ── the roster ───────────────────────────────────────────────────────────

    /// ★ **A server with repositories and nothing published still has a
    /// catalogue.** The state holger.rs is in: an empty array said "nothing
    /// here" about a server that has repositories a stranger can use.
    #[test]
    fn an_empty_store_still_names_its_repositories() {
        let f = Front::anonymous(None);
        let c = Catalogue {
            repositories: vec![
                Repository { name: "crates-mirror".into(), format: "cargo".into(), packages: 0 },
            ],
            releases: vec![],
        };
        let d = Doors { front: &f, catalogue: &c };
        let body = String::from_utf8(route("GET", RELEASES_PATH, &d).unwrap().body).unwrap();
        let doc: serde_json::Value = serde_json::from_str(&body).unwrap();
        assert_eq!(doc["repositories"].as_array().unwrap().len(), 1);
        assert_eq!(doc["repositories"][0]["name"], "crates-mirror");
        assert_eq!(doc["repositories"][0]["format"], "cargo");
        assert_eq!(doc["repositories"][0]["packages"], 0);
        assert!(doc["releases"].as_array().unwrap().is_empty());
    }

    /// The page reads the two lists by the names the wire uses, and names the
    /// roster for a reader.
    #[test]
    fn the_page_reads_both_halves_of_the_catalogue() {
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        assert!(script.contains("doc.repositories"), "the page ignores the roster");
        assert!(script.contains("doc.releases"), "the page ignores the rows");
        assert!(PAGE.contains("Public repositories"), "the roster has no heading");
    }

    /// No `innerHTML` anywhere: a repository name, an artifact name and a
    /// version are operator-supplied text arriving over a socket.
    #[test]
    fn nothing_on_the_page_turns_text_into_markup() {
        assert!(!PAGE.contains("innerHTML"), "the page writes markup from data");
        assert!(!PAGE.contains("document.write"), "the page uses document.write");
    }
}