clone

one developer profile for links and private tcp services, reached peer to peer over iroh with no open ports

refactor: name service ticket and client paths

Dami committed 29954bd
Rename the browser viewer dialog around its ticket-minting role and rename the native data-plane endpoint to ClientEndpoint. Refresh transport, login, profile presentation, and security documentation to match the current device-to-device architecture.
commit
29954bd465cbce89a833f81e5e9cd554a74afed2
parent
a97e9327e2bd8ce608c53f9393eb317c10b6e76e
author
Dami <dami.etoile@gmail.com>
12 files changed, +115 -111
MREADME.md31+++++++++---------
Mcrates/devsite-cli/src/main.rs22++++++------
Mcrates/devsite-client/src/lib.rs19+++++------
Mcrates/devsite-daemon/src/lib.rs10+++---
Mcrates/devsite-daemon/src/verify.rs2+-
Mcrates/devsite-daemon/tests/authz.rs32+++++++++---------
Mcrates/devsite-proto/src/capability.rs4+-
Mcrates/devsite-proto/src/lib.rs3+-
Mdocs/profile-template.md39+++++++++++------------
Mweb/app.js52+++++++++++++++++-------------
Mweb/index.html10+++---
Mweb/site.css2+-
M README.md +15 -16
@@ -5,11 +5,9 @@ Services stay on the machines that own them. An approved user maps a service ont
55 loopback port with the `devsite` CLI.66 77 The control plane handles identity, profiles, sharing, and short-lived capabilities. It8-never carries service traffic. Hosts make outbound Iroh connections, so there is no public9-listener or inbound port to configure.10-11-Version 0.2.0 replaces the early single-page HTTP fetch experiment. Services are12-protocol-agnostic TCP byte streams; they are not assumed to be websites.8+never carries service traffic. Iroh establishes direct or relayed connectivity between9+devices, so there is no public application listener or inbound TCP port to configure.10+Services are protocol-agnostic TCP byte streams; they are not assumed to be websites.1311 1412 ## Install1513
@@ -40,8 +38,8 @@ devsite service host 5432 --name postgres
4038 devsite daemon run4139 ```4240 43-Approve a share on the dashboard, open the service, and choose **Get ticket**. On the44-viewer machine:41+Accept a share on the dashboard, open its profile entry, and choose **Get ticket**. On the42+connecting machine:4543 4644 ```bash4745 devsite connect dst_...
@@ -52,7 +50,7 @@ devsite connect dst_...
5250 ## Shape5351 5452 ```text55-viewer application53+local application5654 │ TCP to 127.0.0.1:ephemeral5755 ▼5856 devsite connect
@@ -97,8 +95,8 @@ Sharing remains per service. A recipient must approve the invitation on their da
9795 devsite service host 6379 --name redis --folder databases --share @bob9896 ```9997 100-A permitted viewer opens the service on dev.site, clicks **Get ticket**, and passes the101-single-use ticket to the CLI. Viewer machines do not need a saved login:98+An approved user opens the service's profile entry on dev.site, clicks **Get ticket**, and99+passes the single-use ticket to the CLI. The connecting machine does not need a saved login:102100 103101 ```bash104102 devsite connect dst_...
@@ -213,9 +211,9 @@ mutex. `fly.toml` and `Dockerfile` describe it:
213211 fly deploy214212 ```215213 216-The traffic path scales independently because it does not pass through Fly. Iroh relays217-carry end-to-end encrypted QUIC between clients and daemons; the control plane only signs218-authorization metadata.214+The traffic path scales independently because it does not pass through Fly. Iroh carries215+end-to-end encrypted QUIC directly when possible and through a relay when necessary; the216+control plane only signs authorization metadata.219217 220218 The two durable identities are `DEVSITE_PUBLIC_ORIGIN`, which Shoo uses when deriving221219 accounts, and `DEVSITE_SIGNING_KEY`, whose public half every daemon pins at login. Changing
@@ -226,7 +224,7 @@ either is intentionally disruptive.
226224 | Path | Responsibility |227225 | --- | --- |228226 | `crates/devsite-proto` | Opaque ids, signed capabilities, and the TCP stream handshake. |229-| `crates/devsite-client` | Native Iroh viewer endpoint and authorized service streams. |227+| `crates/devsite-client` | Native Iroh client endpoint and authorized service streams. |230228 | `crates/devsite-daemon` | Capability verification and fixed-target TCP forwarding. |231229 | `crates/devsite-cli` | Login, links, service hosting/connect, themes, and daemon lifecycle. |232230 | `crates/devsite-server` | Axum/SQLite control plane and capability issuance. |
@@ -237,8 +235,9 @@ either is intentionally disruptive.
237235 - A peer supplies only a signed capability. It never supplies the daemon's target address.238236 - `devsite service host PORT` always stores `127.0.0.1:PORT`; port zero is rejected.239237 - `devsite connect` listens only on a loopback address.240-- Browser-minted tickets are short-lived, stored only as hashes, and consumed once. A241- successful redemption becomes a client-key-bound tunnel session kept only in CLI memory.238+- Browser-minted tickets are short-lived, stored only as hashes, and consumed once.239+ Redemption creates a client-key-bound session; its raw credential exists only in CLI240+ memory, while the control plane stores its hash until expiry or disconnect.242241 - Capabilities are bound to the authenticated Iroh client endpoint and cannot be replayed243242 by another endpoint.244243 - Each capability opens one stream and its nonce is consumed once.
M crates/devsite-cli/src/main.rs +11 -11
@@ -4,7 +4,7 @@ mod client;
44 55 use anyhow::{bail, Context, Result};66 use clap::{error::ErrorKind, Parser, Subcommand};7-use devsite_client::{ServiceStream, ViewerEndpoint};7+use devsite_client::{ClientEndpoint, ServiceStream};88 use devsite_daemon::config::{DaemonConfig, HostedService, Paths, Visibility};99 use devsite_daemon::Daemon;1010 use devsite_proto::SignedCapability;
@@ -43,9 +43,9 @@ struct Cli {
4343 4444 #[derive(Subcommand)]4545 enum Command {46- /// Store a session token from the website and pin the control plane's signing key.46+ /// Store a revocable machine credential and pin the control plane's signing key.4747 Login {48- /// Session token shown by the website after signing in.48+ /// Machine credential created on the signed-in dashboard.4949 token: Option<String>,5050 },5151 /// Manage ordinary external links.
@@ -807,14 +807,14 @@ async fn connect(server: &str, ticket: &str, listen: SocketAddr, output: Output)
807807 if !listen.ip().is_loopback() {808808 bail!("--listen must be a loopback address; refusing to publish the tunnel on the LAN");809809 }810- let viewer = Arc::new(ViewerEndpoint::create().await?);810+ let client = Arc::new(ClientEndpoint::create().await?);811811 let bootstrap = ControlPlane::new(server, None);812812 let redeemed: RedeemTicketResponse = bootstrap813813 .post(814814 "/api/tickets/redeem",815815 &serde_json::json!({816816 "ticket": ticket,817- "client_endpoint_id": viewer.endpoint_id().to_string(),817+ "client_endpoint_id": client.endpoint_id().to_string(),818818 }),819819 )820820 .await
@@ -859,14 +859,14 @@ async fn connect(server: &str, ticket: &str, listen: SocketAddr, output: Output)
859859 if let Err(err) = api.delete("/api/tunnel/session").await {860860 tracing::warn!("could not revoke tunnel session during shutdown: {err:#}");861861 }862- viewer.close().await;862+ client.close().await;863863 return Ok(());864864 }865865 };866866 let api = Arc::clone(&api);867- let viewer = Arc::clone(&viewer);867+ let client = Arc::clone(&client);868868 tokio::spawn(async move {869- if let Err(err) = forward_connection(&api, &viewer, local).await {869+ if let Err(err) = forward_connection(&api, &client, local).await {870870 tracing::warn!(%peer, "service connection failed: {err:#}");871871 }872872 });
@@ -875,7 +875,7 @@ async fn connect(server: &str, ticket: &str, listen: SocketAddr, output: Output)
875875 876876 async fn forward_connection(877877 api: &ControlPlane,878- viewer: &ViewerEndpoint,878+ client: &ClientEndpoint,879879 local: TcpStream,880880 ) -> Result<()> {881881 let grant: CapabilityResponse = api
@@ -890,7 +890,7 @@ async fn forward_connection(
890890 .daemon_endpoint_id891891 .parse()892892 .context("the server returned an invalid daemon endpoint")?;893- let ServiceStream { mut send, mut recv } = viewer.connect(daemon, capability).await?;893+ let ServiceStream { mut send, mut recv } = client.connect(daemon, capability).await?;894894 let (mut local_read, mut local_write) = local.into_split();895895 896896 let local_to_service = async {
@@ -1266,7 +1266,7 @@ async fn run_daemon(paths: &Paths, server: &str, output: Output) -> Result<()> {
12661266 // it is the same on every run and there is nothing to refresh. The relay12671267 // above is printed for the operator and deliberately not uploaded: the12681268 // endpoint publishes its own address through iroh's address lookup, and1269- // viewers resolve it from there. The control plane holds permissions; it is1269+ // clients resolve it from there. The control plane holds permissions; it is12701270 // not a directory service.12711271 let api = ControlPlane::new(&server_url, token);12721272 api.put_empty(
M crates/devsite-client/src/lib.rs +9 -10
@@ -1,4 +1,4 @@
1-//! The viewer side of the dev.site data plane.1+//! The connecting-client side of the dev.site data plane.22 //!33 //! Each accepted local TCP connection becomes one authenticated Iroh bidirectional stream.44
@@ -52,9 +52,9 @@ pub struct ServiceStream {
5252 pub recv: iroh::endpoint::RecvStream,5353 }5454 55-/// An ephemeral viewer endpoint. A capability is bound to this key and therefore cannot55+/// An ephemeral client endpoint. A capability is bound to this key and therefore cannot5656 /// be redeemed by another devsite client.57-pub struct ViewerEndpoint {57+pub struct ClientEndpoint {5858 endpoint: Endpoint,5959 /// One live connection per daemon, reused across service streams.6060 ///
@@ -65,14 +65,13 @@ pub struct ViewerEndpoint {
6565 connections: Mutex<HashMap<EndpointId, Connection>>,6666 }6767 68-impl ViewerEndpoint {69- /// Bind a new endpoint with a freshly generated key and wait until a relay has70- /// accepted us.68+impl ClientEndpoint {69+ /// Bind a new endpoint with a freshly generated key and wait until it is online.7170 pub async fn create() -> Result<Self> {7271 let endpoint = Endpoint::builder(iroh::endpoint::presets::N0)7372 .bind()7473 .await75- .context("binding viewer endpoint")?;74+ .context("binding client endpoint")?;7675 endpoint.online().await;7776 Ok(Self {7877 endpoint,
@@ -80,7 +79,7 @@ impl ViewerEndpoint {
8079 })8180 }8281 83- /// This viewer's public key. The control plane binds capabilities to it, and the82+ /// This client's public key. The control plane binds capabilities to it, and the8483 /// daemon checks that binding against the authenticated peer of the connection.8584 pub fn endpoint_id(&self) -> EndpointId {8685 self.endpoint.id()
@@ -96,8 +95,8 @@ impl ViewerEndpoint {
9695 /// `daemon` is normally a bare [`EndpointId`]: the daemon publishes its own9796 /// address through iroh's address lookup, and the client resolves it. Nothing has to9897 /// tell us where it is. Callers that already know an99- /// address — the tests, which want a relay pinned rather than a lookup — can100- /// pass a fuller [`EndpointAddr`].98+ /// address — such as tests that avoid address lookup — can pass a fuller99+ /// [`EndpointAddr`].101100 ///102101 /// The capability must have been issued for this endpoint's key; the daemon checks103102 /// that binding against the connection and will refuse otherwise.
M crates/devsite-daemon/src/lib.rs +5 -5
M crates/devsite-daemon/src/verify.rs +1 -1
M crates/devsite-daemon/tests/authz.rs +16 -16
@@ -3,7 +3,7 @@
33 //! These run the same `devsite-client` code the CLI runs. Capabilities are minted directly44 //! by a test issuer rather than by the55 //! control plane, which keeps the tests focused on what the *daemon* enforces — the6-//! control plane's own "may this viewer have a capability at all" rules are covered by the6+//! control plane's own "may this account have a capability at all" rules are covered by the77 //! policy tests in devsite-server.88 //!99 //! Negative cases assert `ConnectError::Denied` specifically, never merely "an error". A
@@ -16,7 +16,7 @@
1616 1717 use std::sync::Arc;1818 19-use devsite_client::{ConnectError, ServiceStream, ViewerEndpoint};19+use devsite_client::{ClientEndpoint, ConnectError, ServiceStream};2020 use devsite_daemon::config::{DaemonConfig, HostedService, Visibility};2121 use devsite_daemon::Daemon;2222 use devsite_proto::capability::{
@@ -58,10 +58,10 @@ struct Harness {
5858 hermes: ResourceId,5959 agent: ResourceId,6060 unknown: ResourceId,61- /// The legitimate viewer.62- viewer: ViewerEndpoint,63- /// A second viewer, for anything about one client using another's grant.64- mallory: ViewerEndpoint,61+ /// The legitimate client.62+ client: ClientEndpoint,63+ /// A second client, for anything about one client using another's grant.64+ mallory: ClientEndpoint,6565 }6666 6767 impl Harness {
@@ -104,7 +104,7 @@ impl Harness {
104104 105105 tokio::spawn(Arc::clone(&daemon).serve());106106 107- let (viewer, mallory) = tokio::join!(ViewerEndpoint::create(), ViewerEndpoint::create());107+ let (client, mallory) = tokio::join!(ClientEndpoint::create(), ClientEndpoint::create());108108 109109 Self {110110 daemon,
@@ -114,7 +114,7 @@ impl Harness {
114114 hermes,115115 agent,116116 unknown: ResourceId::generate(),117- viewer: viewer.unwrap(),117+ client: client.unwrap(),118118 mallory: mallory.unwrap(),119119 }120120 }
@@ -152,14 +152,14 @@ impl Harness {
152152 .unwrap()153153 }154154 155- /// An ordinary, valid capability for `resource`, bound to the legitimate viewer.155+ /// An ordinary, valid capability for `resource`, bound to the legitimate client.156156 fn valid(&self, resource: ResourceId) -> SignedCapability {157157 let now = self.now();158158 self.mint(159159 &self.signing_key,160160 resource,161161 self.daemon_key,162- *self.viewer.endpoint_id().as_bytes(),162+ *self.client.endpoint_id().as_bytes(),163163 now,164164 now + DEFAULT_LIFETIME_SECS,165165 )
@@ -167,12 +167,12 @@ impl Harness {
167167 168168 async fn connect_as(169169 &self,170- who: &ViewerEndpoint,170+ who: &ClientEndpoint,171171 capability: SignedCapability,172172 input: &str,173173 ) -> Result<String, ConnectError> {174174 // The relay is pinned rather than looked up. In the browser a bare175- // endpoint id is enough — the daemon publishes its address and the viewer175+ // endpoint id is enough — the daemon publishes its address and the client176176 // resolves it — but that path depends on a third-party lookup service,177177 // and these tests are about what the daemon enforces, not about how it178178 // was found. Handing the address over directly keeps a denial a denial
@@ -193,7 +193,7 @@ impl Harness {
193193 }194194 195195 async fn connect(&self, capability: SignedCapability) -> Result<String, ConnectError> {196- self.connect_as(&self.viewer, capability, "hello").await196+ self.connect_as(&self.client, capability, "hello").await197197 }198198 199199 /// Assert the daemon reached a verdict and refused, rather than being unreachable.
@@ -244,7 +244,7 @@ async fn each_resource_maps_to_its_own_service(h: &Harness) {
244244 245245 async fn capability_bound_to_another_client_is_refused(h: &Harness) {246246 // The core claim: a leaked or forwarded capability is useless without the private key247- // it was bound to. Mallory presents the viewer's capability from her own endpoint.247+ // it was bound to. Mallory presents the client's capability from her own endpoint.248248 let stolen = h.valid(h.agent);249249 match h.connect_as(&h.mallory, stolen, "stolen").await {250250 Err(ConnectError::Denied) => {}
@@ -256,7 +256,7 @@ async fn capability_bound_to_another_client_is_refused(h: &Harness) {
256256 257257 async fn capabilities_that_fail_verification_are_refused(h: &Harness) {258258 let now = h.now();259- let mine = *h.viewer.endpoint_id().as_bytes();259+ let mine = *h.client.endpoint_id().as_bytes();260260 261261 // Signed by someone who is not the control plane.262262 h.assert_denied(
@@ -315,7 +315,7 @@ async fn denials_reveal_nothing_about_why(h: &Harness) {
315315 // A peer must not be able to tell "no such resource" from "not signed correctly" —316316 // that difference is enough to enumerate a profile's private services.317317 let now = h.now();318- let mine = *h.viewer.endpoint_id().as_bytes();318+ let mine = *h.client.endpoint_id().as_bytes();319319 320320 let unknown = h.connect(h.valid(h.unknown)).await.unwrap_err();321321 let forged = h
M crates/devsite-proto/src/capability.rs +2 -2
M crates/devsite-proto/src/lib.rs +1 -2
M docs/profile-template.md +19 -20
@@ -69,7 +69,7 @@ structure, so changes here and changes to the whitelist belong in the same commi
6969 <small class="state"><span class="host"><span>klot.ski&nbsp;</span></span>↗</small>7070 </li>7171 <!-- reached through its owner's daemon, so it is a button that opens72- connection instructions here. -->72+ the browser's ticket-minting prompt here. -->7373 <li class="entry" data-kind="service" data-visibility="private">7474 <button class="outline">Hermes</button>7575 <small class="state">private</small>
@@ -87,7 +87,7 @@ structure, so changes here and changes to the whitelist belong in the same commi
8787 </article>8888 </main>8989 90- <dialog id="viewer">…a Get ticket action, then the devsite connect command…</dialog>90+ <dialog id="service-ticket-dialog">…Get ticket, then devsite connect…</dialog>9191 </body>9292 ```9393
@@ -126,7 +126,7 @@ to create and nothing to delete, so a profile can never be left holding an empty
126126 one, and renaming a folder is retagging what is in it.127127 128128 That falls out of the rendering too. The set of folders is derived from the129-entries the viewer is allowed to see, so a folder holding only private sites does129+entries the visitor is allowed to see, so a folder holding only private sites does130130 not appear to a stranger as an empty fold — it is never built at all.131131 132132 Folds are semantic `<details>` elements. They are open by default for profiles
@@ -136,13 +136,13 @@ the reader controls the ordinary open state. Pico draws the marker and handles
136136 the interaction; `.folder > summary` is the label.137137 138138 `--devsite-folder-order` names folders that should appear first. A name not139-visible to the current viewer is ignored; visible folders omitted from the list139+visible to the current visitor is ignored; visible folders omitted from the list140140 retain their first-appearance order. Folder names use JSON-style quoted strings,141141 the same non-control Unicode accepted by resource folders, with the same142142 40-scalar limit. Quotes and backslashes use JSON escapes.143143 144144 `--pico-ins-color` and `--pico-del-color` are still in the whitelist, and are145-what the viewer's success and failure messages are drawn with.145+what the page's success and failure messages are drawn with.146146 147147 ## What profile presentation may set148148
@@ -169,11 +169,11 @@ stored, and left to do nothing.
169169 | | `--pico-font-weight` | `400` or `700` |170170 | | `--pico-text-decoration` | `none` or `underline` |171171 172-`--devsite-scheme` is the one key that is not a Pico variable. It sets172+The `--devsite-*` keys are not Pico variables. `--devsite-scheme` sets173173 `data-theme` on the page, which is how Pico chooses between its own light and174-dark palettes; the rest of the theme then adjusts that starting point. `auto`175-leaves `data-theme` unset and follows the viewer's system preference. Explicit176-`light` or `dark` forces that scheme.174+dark palettes; the folder-layout keys are consumed as data while rendering the175+semantic folds. `auto` leaves `data-theme` unset and follows the visitor's system176+preference. Explicit `light` or `dark` forces that scheme.177177 178178 ### Value grammars179179
@@ -184,10 +184,10 @@ leaves `data-theme` unset and follows the viewer's system preference. Explicit
184184 | length | a non-negative number with `px`, `rem`, `em`, `ch`, `ex`, `vw`, `vh`, `vmin`, `vmax` or `%`, or bare `0` |185185 | number | a non-negative unitless number |186186 187-`var()`, `calc()` and `url()` are refused everywhere. Every accepted value is188-drawn from the alphabet `[0-9a-z#%.,/()+- ]`, so it cannot carry `<`, `"` or `}`189-— the rendered rule is safe to inline in a `<style>` element as a property of the190-grammar, not of an escaping step someone has to remember.187+`var()`, `calc()` and `url()` are refused from CSS values. Every accepted Pico188+value is drawn from the alphabet `[0-9a-z#%.,/()+- ]`, so it cannot carry `<`,189+`"` or `}`. Folder names instead use the bounded JSON-string grammar and never190+enter the generated `<style>` rule.191191 192192 `light-dark()` uses its first colour in light mode and its second in dark mode.193193 Either side may use any supported colour form, including a functional colour:
@@ -198,13 +198,13 @@ Either side may use any supported colour form, including a functional colour:
198198 --pico-background-color: light-dark(rgb(250 248 255), rgb(24 18 32));199199 ```200200 201-With `auto`, the pair follows the viewer's system preference. Setting201+With `auto`, the pair follows the visitor's system preference. Setting202202 `--devsite-scheme: light` always selects the first colour; setting it to `dark`203203 always selects the second. The parser understands only this one level of204204 nesting: another `light-dark()`, `var()`, `calc()`, `url()`, or any other205205 function inside the pair is refused.206206 207-## What a theme cannot do207+## What profile presentation cannot do208208 209209 Refused outright, with a message naming the reason:210210
@@ -221,11 +221,10 @@ use its presentation to disguise itself as another part of the site.
221221 222222 ## Setting one223223 224-From the CLI, and only from the CLI. The website is where a profile is read, not225-where it is written — links, hosted services, sharing and themes are all set with226-`devsite`, and a theme is no more a browser concern than a hosted service is. The one227-exception is claiming a handle, which has to happen in the browser because that228-is where signing in finishes.224+From the CLI, and only from the CLI. The website reads profiles, manages account225+controls, approves shares, and mints service tickets; links, hosted services,226+sharing targets, themes, and folder layout are set with `devsite`. Claiming a227+handle remains in the browser because that is where signing in finishes.229228 230229 ```bash231230 devsite theme properties # what you may set
M web/app.js +30 -22
@@ -233,13 +233,13 @@ function renderSession() {
233233 /// Everything on a profile is a site. The only difference between them is how you234234 /// get there: a link is reached at its own address, so it is an anchor and takes235235 /// you away; a service is reached through its owner's daemon, so it is a button236-/// and opens here. That is the whole of the distinction, and the row says it with237-/// an arrow rather than by sorting the page into kinds.236+/// that opens its ticket prompt here. That is the whole of the distinction, and237+/// the row says it with an arrow rather than by sorting the page into kinds.238238 ///239239 /// A service carries no reachability state. Nothing on this page knows whether240240 /// the far end is running — the control plane is not told, and the only thing241-/// that could answer is a connection attempt. So the row makes no claim, and242-/// clicking it finds out.241+/// that could answer is the CLI's later connection attempt. The browser only242+/// mints a ticket, so the row makes no reachability claim.243243 function siteRow(item, { onClick, from } = {}) {244244 const li = document.createElement('li');245245 li.className = 'entry';
@@ -291,7 +291,7 @@ function entryList(items, options = {}) {
291291 list.append(siteRow(item, {292292 ...options,293293 from: item.owner_handle,294- onClick: () => openService(item),294+ onClick: () => showServiceTicketDialog(item),295295 }));296296 }297297 return list;
@@ -301,7 +301,7 @@ function entryList(items, options = {}) {
301301 ///302302 /// Derived from the entries rather than stored anywhere: a folder is a name303303 /// repeated across the sites that share it, so this is the only place the set of304-/// them exists. A folder that a viewer may see nothing inside does not appear at304+/// them exists. A folder that a visitor may see nothing inside does not appear at305305 /// all, because it never gets built.306306 function foldersOf(entries) {307307 const folders = new Map();
@@ -340,7 +340,7 @@ function profileLayout(declarations = []) {
340340 }341341 342342 /// Named folders come first in the requested order. A name that does not exist343-/// for this viewer is ignored, and unlisted visible folders retain the order in343+/// for this visitor is ignored, and unlisted visible folders retain the order in344344 /// which they first appeared.345345 function orderedFolders(folders, order) {346346 const result = [];
@@ -408,20 +408,20 @@ function renderProfile(profile) {
408408 main.append(article);409409 }410410 411-// -- opening a service ---------------------------------------------------------411+// -- service tickets -----------------------------------------------------------412412 413413 /// Pico's modal convention: the `open` attribute on the dialog, and a lock class414414 /// on the document while it is up.415-function openViewer() {415+function openServiceTicketDialog() {416416 document.documentElement.classList.add('modal-is-open', 'modal-is-opening');417- $('viewer').setAttribute('open', '');417+ $('service-ticket-dialog').setAttribute('open', '');418418 setTimeout(() => document.documentElement.classList.remove('modal-is-opening'), 400);419419 }420420 421-function closeViewer() {421+function closeServiceTicketDialog() {422422 document.documentElement.classList.remove('modal-is-open', 'modal-is-opening');423- $('viewer').removeAttribute('open');424- $('viewer-body').replaceChildren();423+ $('service-ticket-dialog').removeAttribute('open');424+ $('service-ticket-body').replaceChildren();425425 }426426 427427 function maskedSecret(secret) {
@@ -465,7 +465,7 @@ function renderSecretCommand(container, verb, secret) {
465465 container.replaceChildren(group, status);466466 }467467 468-function renderTicketPrompt(resourceId, container, status) {468+function renderServiceTicketPrompt(resourceId, container, status) {469469 container.innerHTML = `470470 <p>This is a private TCP service. Mint a short-lived ticket, then connect it to a loopback port with the CLI.</p>471471 <button type="button" class="get-ticket">Get ticket</button>`;
@@ -491,12 +491,16 @@ function renderTicketPrompt(resourceId, container, status) {
491491 });492492 }493493 494-function openService(item) {495- $('viewer-title').textContent = item.owner_handle494+function showServiceTicketDialog(item) {495+ $('service-ticket-title').textContent = item.owner_handle496496 ? `${item.name} — @${item.owner_handle}`497497 : item.name;498- renderTicketPrompt(item.resource_id, $('viewer-body'), $('viewer-status'));499- openViewer();498+ renderServiceTicketPrompt(499+ item.resource_id,500+ $('service-ticket-body'),501+ $('service-ticket-status'),502+ );503+ openServiceTicketDialog();500504 }501505 502506 // -- setup views ---------------------------------------------------------------
@@ -1014,7 +1018,7 @@ async function route() {
10141018 <p><small>The CLI opens a loopback port and carries its byte stream through Iroh.</small></p>10151019 </article>`;10161020 if (me) {1017- renderTicketPrompt(serviceMatch[1], $('service-connect'), $('service-status'));1021+ renderServiceTicketPrompt(serviceMatch[1], $('service-connect'), $('service-status'));10181022 } else {10191023 $('service-connect').innerHTML = '<p>Sign in from the header to request access.</p>';10201024 }
@@ -1041,9 +1045,13 @@ async function showProfile(handle) {
10411045 }10421046 }10431047 1044-$('viewer-close').addEventListener('click', closeViewer);1045-$('viewer').addEventListener('click', (e) => { if (e.target === $('viewer')) closeViewer(); });1046-document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeViewer(); });1048+$('service-ticket-close').addEventListener('click', closeServiceTicketDialog);1049+$('service-ticket-dialog').addEventListener('click', (event) => {1050+ if (event.target === $('service-ticket-dialog')) closeServiceTicketDialog();1051+});1052+document.addEventListener('keydown', (event) => {1053+ if (event.key === 'Escape') closeServiceTicketDialog();1054+});10471055 10481056 updateLogoContrast();10491057 matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () =>
M web/index.html +5 -5
M web/site.css +1 -1