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>
| M | README.md | 31 | +++++++++--------- |
| M | crates/ | 22 | ++++++------ |
| M | crates/ | 19 | +++++------ |
| M | crates/ | 10 | +++--- |
| M | crates/ | 2 | +- |
| M | crates/ | 32 | +++++++++--------- |
| M | crates/ | 4 | +- |
| M | crates/ | 3 | +- |
| M | docs/ | 39 | +++++++++++------------ |
| M | web/ | 52 | +++++++++++++++++------------- |
| M | web/ | 10 | +++--- |
| M | web/ | 2 | +- |
@@ -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.@@ -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 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(@@ -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.@@ -1,8 +1,8 @@
1-//! The daemon: an Iroh endpoint that proxies *authorized* requests to local services.1+//! The daemon: an Iroh endpoint that forwards authorized streams to fixed local services.22 //!3-//! It has no inbound port. It reaches the world through a relay, and it decides for itself4-//! whether to honour a request by verifying a capability signed by the control plane —5-//! the control plane never touches this traffic.3+//! It exposes no application TCP port. Iroh establishes direct or relayed connectivity,4+//! and the daemon decides for itself whether to honour a request by verifying a capability5+//! signed by the control plane — the control plane never touches this traffic.66 77 pub mod config;88 pub mod verify;@@ -39,7 +39,7 @@ pub struct Daemon {
3939 }4040 4141 impl Daemon {42- /// Bind the endpoint and wait until a relay will carry traffic for us.42+ /// Bind the endpoint and wait until Iroh has established network connectivity.4343 ///4444 /// Fails if no control-plane key has been pinned: a daemon that cannot verify4545 /// capabilities must refuse to start rather than run in some permissive mode.@@ -200,7 +200,7 @@ mod tests {
200200 201201 fn a_denied_request_does_not_consume_the_nonce() {202202 // If a wrong-peer attempt burned the nonce, an attacker who merely observed a203- // capability could deny the legitimate viewer their one use of it.203+ // capability could deny the legitimate client its one use of it.204204 let key = SigningKey::from_bytes(&[9; 32]);205205 let mut replay = ReplayGuard::default();206206 let claims = claims_with([1; 16], resource());@@ -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@@ -1,7 +1,7 @@
11 //! Short-lived, signed grants issued by the control plane and verified by a daemon.22 //!33 //! A capability is the only thing that persuades a daemon to talk to a local service. It4-//! is deliberately narrow: one viewer, one resource, one daemon, one client key, one4+//! is deliberately narrow: one account, one resource, one daemon, one client key, one55 //! permission, a few minutes.66 //!77 //! Public keys are carried as raw bytes rather than iroh types so this crate stays free of@@ -17,7 +17,7 @@ use crate::{AccountId, ResourceId};
1717 pub type KeyBytes = [u8; 32];1818 1919 /// How long a freshly issued capability remains valid. Short enough that a leaked token is20-/// of little use, long enough to survive a slow relay connection.20+/// of little use, long enough to survive slow network setup.2121 pub const DEFAULT_LIFETIME_SECS: u64 = 180;2222 2323 /// Tolerance for clock skew between the control plane and a daemon.@@ -9,6 +9,5 @@ pub use capability::{CapabilityClaims, CapabilityError, Permission, SignedCapabi
99 pub use id::{AccountId, IdParseError, ResourceId};1010 pub use wire::{ConnectRequest, ErrorCode, Request, Response};1111 12-/// ALPN for the dev.site stream protocol. Version 1 replaced the early one-document13-/// HTTP fetch with an authorized bidirectional TCP stream.12+/// ALPN for the authorized bidirectional TCP stream protocol.1413 pub const ALPN: &[u8] = b"devsite/tcp/1";@@ -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 </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@@ -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', () =>@@ -39,14 +39,14 @@
3939 4040 <footer class="container" id="page-footer" hidden></footer>4141 42-<dialog id="viewer">42+<dialog id="service-ticket-dialog">4343 <article>4444 <header>45- <button aria-label="Close" rel="prev" id="viewer-close"></button>46- <strong id="viewer-title"></strong>45+ <button aria-label="Close" rel="prev" id="service-ticket-close"></button>46+ <strong id="service-ticket-title"></strong>4747 </header>48- <div id="viewer-body"></div>49- <p id="viewer-status"></p>48+ <div id="service-ticket-body"></div>49+ <p id="service-ticket-status"></p>5050 </article>5151 </dialog>5252 @@ -231,7 +231,7 @@ details:not(.dropdown)[open] > summary::after {
231231 }232232 }233233 234-/* -- the viewer ------------------------------------------------------------ */234+/* -- CLI commands and service tickets -------------------------------------- */235235 236236 .secret-command {237237 display: flex;