clone

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

main
devsite / web / services.ts
services.ts 3.8K
1import { api, element, errorMessage, query } from './shared';2import { renderSecretCommand } from './secrets';3import { clearTheme } from './theme';45interface ServicePrompt {6 resource_id: string;7 name: string;8 owner_handle?: string;9}1011function openServiceTicketDialog() {12 document.documentElement.classList.add('modal-is-open', 'modal-is-opening');13 element('service-ticket-dialog').setAttribute('open', '');14 setTimeout(() => document.documentElement.classList.remove('modal-is-opening'), 400);15}1617function closeServiceTicketDialog() {18 document.documentElement.classList.remove('modal-is-open', 'modal-is-opening');19 element('service-ticket-dialog').removeAttribute('open');20 element('service-ticket-body').replaceChildren();21}2223export function renderServiceTicketPrompt(24 resourceId: string,25 container: HTMLElement,26 status: HTMLElement,27) {28 container.innerHTML = `29 <p>This is a private TCP service. Mint a short-lived ticket, then connect it to a loopback port with the CLI.</p>30 <button type="button" class="get-ticket">Get ticket</button>`;31 status.textContent = '';3233 query<HTMLButtonElement>('.get-ticket', container).addEventListener('click', async (event) => {34 const button = event.currentTarget as HTMLButtonElement;35 button.setAttribute('aria-busy', 'true');36 status.textContent = '';37 try {38 const result = await api<{ ticket: string }>(39 `/api/services/${encodeURIComponent(resourceId)}/ticket`,40 { method: 'POST', body: '{}' },41 );42 container.innerHTML = `43 <p>This ticket can be redeemed once and expires shortly.</p>44 <div class="secret-command-slot"></div>`;45 renderSecretCommand(query('.secret-command-slot', container), 'connect', result.ticket);46 } catch (error) {47 button.removeAttribute('aria-busy');48 status.textContent = errorMessage(error);49 }50 });51}5253function showServiceTicketDialog(item: ServicePrompt) {54 element('service-ticket-title').textContent = item.owner_handle55 ? `${item.name} - @${item.owner_handle}`56 : item.name;57 renderServiceTicketPrompt(58 item.resource_id,59 element('service-ticket-body'),60 element('service-ticket-status'),61 );62 openServiceTicketDialog();63}6465export function showServiceRoute(resourceId: string, signedIn: boolean) {66 clearTheme();67 const main = element('main');68 main.innerHTML = `69 <article>70 <hgroup>71 <h1>Private TCP service</h1>72 <p>Access is checked for each connection.</p>73 </hgroup>74 <div id="service-connect"></div>75 <p id="service-status"></p>76 <p><small>The CLI opens a loopback port and carries its byte stream through Iroh.</small></p>77 </article>`;78 if (signedIn) {79 renderServiceTicketPrompt(resourceId, element('service-connect'), element('service-status'));80 } else {81 element('service-connect').innerHTML = '<p>Sign in from the header to request access.</p>';82 }83}8485export function bindServiceDialogs(main: HTMLElement) {86 main.addEventListener('click', (event) => {87 const target = event.target;88 if (!(target instanceof Element)) return;89 const button = target.closest<HTMLButtonElement>('button[data-service-id]');90 if (!button?.dataset.serviceId || !button.dataset.serviceName) return;91 showServiceTicketDialog({92 resource_id: button.dataset.serviceId,93 name: button.dataset.serviceName,94 ...(button.dataset.serviceOwner ? { owner_handle: button.dataset.serviceOwner } : {}),95 });96 });9798 element('service-ticket-close').addEventListener('click', closeServiceTicketDialog);99 element('service-ticket-dialog').addEventListener('click', (event) => {100 if (event.target === element('service-ticket-dialog')) closeServiceTicketDialog();101 });102 document.addEventListener('keydown', (event) => {103 if (event.key === 'Escape') closeServiceTicketDialog();104 });105}