@@ -18,6 +18,33 @@ type Tld = {
1818 created_at : string ;
1919} ;
2020
21+ type PinKind = "tls" | "mtp" ;
22+
23+ type Pin = {
24+ tld : string ;
25+ pin : string ;
26+ kind : PinKind ;
27+ note : string | null ;
28+ created_at : string ;
29+ } ;
30+
31+ type PinDraft = {
32+ kind : PinKind ;
33+ pin : string ;
34+ note : string ;
35+ } ;
36+
37+ const emptyPinDraft = ( ) : PinDraft => ( { kind : "tls" , pin : "" , note : "" } ) ;
38+
39+ const formatCreatedAt = ( value : string ) => {
40+ const sqliteUtc = / ^ \d { 4 } - \d { 2 } - \d { 2 } \d { 2 } : \d { 2 } : \d { 2 } $ / . test ( value ) ;
41+ const date = new Date ( sqliteUtc ? `${ value . replace ( " " , "T" ) } Z` : value ) ;
42+ return Number . isNaN ( date . getTime ( ) ) ? value : date . toLocaleString ( ) ;
43+ } ;
44+
45+ const tldRowId = ( tld : string ) => `moshpit-tld-${ tld } ` ;
46+ const pinPanelId = ( tld : string ) => `moshpit-pins-${ tld } ` ;
47+
2148export default function MoshpitTlds ( ) {
2249 const [ tlds , setTlds ] = useState < Tld [ ] > ( [ ] ) ;
2350 const [ loading , setLoading ] = useState ( true ) ;
@@ -29,6 +56,8 @@ export default function MoshpitTlds() {
2956 const [ aliasTarget , setAliasTarget ] = useState < Record < string , string > > ( { } ) ;
3057 const [ exempt , setExempt ] = useState < Record < string , string [ ] > > ( { } ) ;
3158 const [ exemptDraft , setExemptDraft ] = useState < Record < string , string > > ( { } ) ;
59+ const [ pins , setPins ] = useState < Partial < Record < string , Pin [ ] > > > ( { } ) ;
60+ const [ pinDrafts , setPinDrafts ] = useState < Partial < Record < string , PinDraft > > > ( { } ) ;
3261
3362 const load = useCallback ( async ( ) => {
3463 setError ( null ) ;
@@ -61,6 +90,25 @@ export default function MoshpitTlds() {
6190 setExempt ( ( prev ) => ( { ...prev , [ tld ] : json . labels ?? json . exempt ?? [ ] } ) ) ;
6291 } , [ ] ) ;
6392
93+ const loadPins = useCallback ( async ( tld : string ) => {
94+ const key = `pins:load:${ tld } ` ;
95+ setBusy ( key ) ;
96+ setError ( null ) ;
97+ try {
98+ const res = await fetch ( `/api/moshpit/tlds/${ tld } /pins` , { cache : "no-store" } ) ;
99+ const json = ( await res . json ( ) . catch ( ( ) => ( { } ) ) ) as { pins ?: Pin [ ] ; error ?: string } ;
100+ if ( ! res . ok ) {
101+ setError ( json . error || `Could not load key pins (${ res . status } )` ) ;
102+ return ;
103+ }
104+ setPins ( ( prev ) => ( { ...prev , [ tld ] : json . pins ?? [ ] } ) ) ;
105+ } catch ( e ) {
106+ setError ( e instanceof Error ? e . message : "Could not load key pins" ) ;
107+ } finally {
108+ setBusy ( null ) ;
109+ }
110+ } , [ ] ) ;
111+
64112 const run = useCallback (
65113 async ( key : string , fn : ( ) => Promise < Response > , okMessage : string ) => {
66114 setBusy ( key ) ;
@@ -102,6 +150,44 @@ export default function MoshpitTlds() {
102150 if ( ok ) setClaim ( "" ) ;
103151 } ;
104152
153+ const publishPin = async ( tld : string ) => {
154+ const draft = pinDrafts [ tld ] ?? emptyPinDraft ( ) ;
155+ const pin = draft . pin . trim ( ) ;
156+ if ( ! pin ) return ;
157+
158+ const ok = await run (
159+ `pin:add:${ tld } ` ,
160+ ( ) =>
161+ fetch ( `/api/moshpit/tlds/${ tld } /pins` , {
162+ method : "POST" ,
163+ headers : { "Content-Type" : "application/json" } ,
164+ body : JSON . stringify ( { pin, kind : draft . kind , note : draft . note . trim ( ) } ) ,
165+ } ) ,
166+ `Published ${ draft . kind . toUpperCase ( ) } key for .${ tld } ` ,
167+ ) ;
168+ if ( ! ok ) return ;
169+
170+ setPinDrafts ( ( prev ) => ( { ...prev , [ tld ] : emptyPinDraft ( ) } ) ) ;
171+ await loadPins ( tld ) ;
172+ } ;
173+
174+ const withdrawPin = async ( tld : string , entry : Pin ) => {
175+ const confirmed = window . confirm (
176+ `Withdraw this ${ entry . kind . toUpperCase ( ) } key from .${ tld } ? Connections using it may stop working.` ,
177+ ) ;
178+ if ( ! confirmed ) return ;
179+
180+ const ok = await run (
181+ `pin:remove:${ tld } :${ entry . pin } ` ,
182+ ( ) =>
183+ fetch ( `/api/moshpit/tlds/${ tld } /pins?pin=${ encodeURIComponent ( entry . pin ) } ` , {
184+ method : "DELETE" ,
185+ } ) ,
186+ `Withdrew ${ entry . kind . toUpperCase ( ) } key from .${ tld } ` ,
187+ ) ;
188+ if ( ok ) await loadPins ( tld ) ;
189+ } ;
190+
105191 if ( loading ) return < section className = "card2" > < h2 > Moshpit TLDs</ h2 > < p className = "sub" > Loading…</ p > </ section > ;
106192
107193 return (
@@ -126,8 +212,8 @@ export default function MoshpitTlds() {
126212 </ button >
127213 </ div >
128214
129- { error ? < p className = "sub" style = { { color : "#ff6b6b" } } > { error } </ p > : null }
130- { note ? < p className = "sub" style = { { color : "#5ad18c" } } > { note } </ p > : null }
215+ { error ? < p className = "sub" role = "alert" style = { { color : "#ff6b6b" } } > { error } </ p > : null }
216+ { note ? < p className = "sub" role = "status" aria-live = "polite" style = { { color : "#5ad18c" } } > { note } </ p > : null }
131217
132218 { tlds . length === 0 ? (
133219 < p className = "sub" > You don't hold any TLDs yet.</ p >
@@ -136,8 +222,14 @@ export default function MoshpitTlds() {
136222 { tlds . map ( ( t ) => {
137223 const target = aliasTarget [ t . tld ] ?? "" ;
138224 const labels = exempt [ t . tld ] ;
225+ const publishedPins = pins [ t . tld ] ;
226+ const pinDraft = pinDrafts [ t . tld ] ?? emptyPinDraft ( ) ;
139227 return (
140- < li key = { t . tld } style = { { borderTop : "1px solid rgba(255,255,255,.08)" , padding : "12px 0" } } >
228+ < li
229+ id = { tldRowId ( t . tld ) }
230+ key = { t . tld }
231+ style = { { borderTop : "1px solid rgba(255,255,255,.08)" , padding : "12px 0" , scrollMarginTop : 16 } }
232+ >
141233 < div style = { { display : "flex" , alignItems : "center" , gap : 8 , flexWrap : "wrap" } } >
142234 < b > .{ t . tld } </ b >
143235 { t . alias_of ? (
@@ -192,6 +284,30 @@ export default function MoshpitTlds() {
192284 < button onClick = { ( ) => void loadExempt ( t . tld ) } >
193285 { labels ? "Refresh held-back names" : "Held-back names" }
194286 </ button >
287+
288+ < button
289+ type = "button"
290+ onClick = { ( ) => {
291+ if ( publishedPins ) {
292+ setPins ( ( prev ) => {
293+ const next = { ...prev } ;
294+ delete next [ t . tld ] ;
295+ return next ;
296+ } ) ;
297+ } else {
298+ void loadPins ( t . tld ) ;
299+ }
300+ } }
301+ disabled = { busy === `pins:load:${ t . tld } ` }
302+ aria-expanded = { publishedPins !== undefined }
303+ aria-controls = { pinPanelId ( t . tld ) }
304+ >
305+ { busy === `pins:load:${ t . tld } `
306+ ? "Loading keys…"
307+ : publishedPins
308+ ? "Hide key pins"
309+ : "Key pins" }
310+ </ button >
195311 </ div >
196312
197313 { labels ? (
@@ -257,6 +373,122 @@ export default function MoshpitTlds() {
257373 </ div >
258374 </ div >
259375 ) : null }
376+
377+ { publishedPins ? (
378+ < div id = { pinPanelId ( t . tld ) } style = { { marginTop : 12 } } >
379+ < div style = { { display : "flex" , alignItems : "center" , gap : 8 , flexWrap : "wrap" } } >
380+ < p className = "sub" style = { { margin : 0 , flex : 1 } } >
381+ { t . alias_of
382+ ? `Pins published directly on .${ t . tld } protect held-back names only. Names that follow the redirect use .${ t . alias_of } 's pins.`
383+ : "Publish both old and new keys during rotation, then withdraw the old one after deployment." }
384+ </ p >
385+ { t . alias_of ? (
386+ < button
387+ type = "button"
388+ onClick = { ( ) => {
389+ void loadPins ( t . alias_of ! ) ;
390+ document
391+ . getElementById ( tldRowId ( t . alias_of ! ) )
392+ ?. scrollIntoView ( { behavior : "smooth" , block : "start" } ) ;
393+ } }
394+ disabled = { busy === `pins:load:${ t . alias_of } ` }
395+ >
396+ Manage .{ t . alias_of } pins
397+ </ button >
398+ ) : null }
399+ < button
400+ type = "button"
401+ onClick = { ( ) => void loadPins ( t . tld ) }
402+ disabled = { busy === `pins:load:${ t . tld } ` }
403+ >
404+ Refresh
405+ </ button >
406+ </ div >
407+
408+ { publishedPins . length === 0 ? (
409+ < p className = "sub" > No TLS or MTP keys published yet.</ p >
410+ ) : (
411+ < ul style = { { listStyle : "none" , padding : 0 , margin : "8px 0" } } >
412+ { publishedPins . map ( ( entry ) => (
413+ < li
414+ key = { entry . pin }
415+ style = { {
416+ borderTop : "1px solid rgba(255,255,255,.08)" ,
417+ padding : "8px 0" ,
418+ } }
419+ >
420+ < div style = { { display : "flex" , alignItems : "center" , gap : 8 , flexWrap : "wrap" } } >
421+ < b > { entry . kind . toUpperCase ( ) } </ b >
422+ < code style = { { overflowWrap : "anywhere" , flex : 1 } } > { entry . pin } </ code >
423+ < span className = "sub" > { formatCreatedAt ( entry . created_at ) } </ span >
424+ < button
425+ type = "button"
426+ onClick = { ( ) => void withdrawPin ( t . tld , entry ) }
427+ disabled = { busy === `pin:remove:${ t . tld } :${ entry . pin } ` }
428+ >
429+ { busy === `pin:remove:${ t . tld } :${ entry . pin } ` ? "Withdrawing…" : "Withdraw" }
430+ </ button >
431+ </ div >
432+ { entry . note ? < p className = "sub" style = { { margin : "4px 0 0" } } > { entry . note } </ p > : null }
433+ </ li >
434+ ) ) }
435+ </ ul >
436+ ) }
437+
438+ < div style = { { display : "flex" , gap : 8 , alignItems : "center" , flexWrap : "wrap" } } >
439+ < select
440+ value = { pinDraft . kind }
441+ onChange = { ( e ) =>
442+ setPinDrafts ( ( prev ) => ( {
443+ ...prev ,
444+ [ t . tld ] : { ...pinDraft , kind : e . target . value as PinKind } ,
445+ } ) )
446+ }
447+ aria-label = { `Key type for .${ t . tld } ` }
448+ >
449+ < option value = "tls" > TLS</ option >
450+ < option value = "mtp" > MTP</ option >
451+ </ select >
452+ < input
453+ value = { pinDraft . pin }
454+ onChange = { ( e ) =>
455+ setPinDrafts ( ( prev ) => ( {
456+ ...prev ,
457+ [ t . tld ] : { ...pinDraft , pin : e . target . value } ,
458+ } ) )
459+ }
460+ placeholder = "base64 SHA-256 SPKI pin"
461+ aria-label = { `Key pin for .${ t . tld } ` }
462+ spellCheck = { false }
463+ style = { { flex : "2 1 280px" } }
464+ />
465+ < input
466+ value = { pinDraft . note }
467+ onChange = { ( e ) =>
468+ setPinDrafts ( ( prev ) => ( {
469+ ...prev ,
470+ [ t . tld ] : { ...pinDraft , note : e . target . value } ,
471+ } ) )
472+ }
473+ placeholder = "note (optional)"
474+ aria-label = { `Key note for .${ t . tld } ` }
475+ maxLength = { 200 }
476+ style = { { flex : "1 1 180px" } }
477+ />
478+ < button
479+ type = "button"
480+ onClick = { ( ) => void publishPin ( t . tld ) }
481+ disabled = { busy === `pin:add:${ t . tld } ` || ! pinDraft . pin . trim ( ) }
482+ >
483+ { busy === `pin:add:${ t . tld } `
484+ ? "Publishing…"
485+ : t . alias_of
486+ ? "Publish held-back key"
487+ : "Publish key" }
488+ </ button >
489+ </ div >
490+ </ div >
491+ ) : null }
260492 </ li >
261493 ) ;
262494 } ) }
0 commit comments