ident
All repositories: gitoria
8.0 KB
// selector.js — ident's IDENTITY SELECTOR for an app's page (piece 3 of 6, ticket #26;// CONCEPT.md "Flow 2: identity selector"). Served by ident at /selector.js. An app's page://// <script src="https://<ident>/selector.js"></script>// <ident-selector key="pk_…"></ident-selector> (the app's PUBLIC API key)//// The element (shadow DOM, ident's design) first says "choose ident". Opening it fetches// the identities of the ident user signed in in this browser from ident (credentials,// CORS: only from one of the app's registered origins with its key). Choosing one IS the// login: ident hands a one-time code, the element fires `ident-login` with// `event.detail.code`; the HOST'S SERVER trades the code with POST /api/exchange (key +// secret) for the app-specific identity id.// The host tells the element it is logged in: attribute `logged-in` (or `el.loggedIn =// true`). Logout on the website = the host resets it (remove the attribute, or// `el.loggedIn = false` / `el.reset()`). The element sets no cookie of its own.// Restyling: CSS custom properties --ident-* on the element, and ::part(button | panel |// identity | status | message | link) — see ident's README "How apps use ident".(() => {if (!window.customElements || customElements.get('ident-selector')) return;const script = document.currentScript;const IDENT = new URL(script ? script.src : location.href).origin;const CSS = `:host {--_accent: var(--ident-accent, #ce9178);--_accent-text: var(--ident-accent-text, rgb(15, 20, 25));--_bg: var(--ident-background, rgb(35, 40, 45));--_text: var(--ident-text, rgb(195, 200, 205));--_strong: var(--ident-text-strong, rgb(245, 250, 255));--_muted: var(--ident-text-muted, rgb(145, 150, 155));--_border: var(--ident-border, rgb(70, 75, 80));--_radius: var(--ident-radius, .4rem);display: inline-block;max-width: 100%;font: var(--ident-font, 600 16px/1.5 system-ui, sans-serif);color: var(--_text);}:host([hidden]) { display: none; }* { box-sizing: border-box; }button { font: inherit; cursor: pointer; }[part~="button"] {display: inline-flex; align-items: center; gap: .5rem; max-width: 100%;padding: .6rem 1.1rem; color: var(--_accent-text); background: var(--_accent);border: 1px solid var(--_accent); border-radius: var(--_radius);}[part~="button"]:hover { filter: brightness(1.1); }[part~="button"]:focus-visible, [part~="identity"]:focus-visible { outline: 2px solid var(--_strong); outline-offset: 2px; }.mark { font-weight: 800; letter-spacing: .01em; }.caret { font-size: .75em; }[part~="panel"] {margin-top: .4rem; width: 20rem; max-width: 100%; padding: .5rem;background: var(--_bg); border: 1px solid var(--_border); border-radius: calc(var(--_radius) * 1.5);font-weight: 400;}ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .3rem; }[part~="identity"] {display: block; width: 100%; text-align: left; padding: .55rem .8rem; overflow-wrap: anywhere;color: var(--_strong); background: transparent; border: 1px solid var(--_border); border-radius: var(--_radius);}[part~="identity"]:hover { border-color: var(--_accent); color: var(--_accent); }.head { margin: 0 0 .4rem; font-size: .85rem; color: var(--_muted); }[part~="message"] { margin: 0; padding: .3rem .2rem; font-size: .95rem; }[part~="link"] { color: var(--_accent); }[part~="status"] {display: inline-flex; align-items: center; gap: .5rem; max-width: 100%; overflow-wrap: anywhere;padding: .6rem 1.1rem; color: var(--_strong); background: var(--_bg);border: 1px solid var(--_border); border-radius: var(--_radius);}[part~="status"] .check { color: var(--_accent); }`;const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);class IdentSelector extends HTMLElement {static get observedAttributes() { return ['logged-in', 'key']; }constructor() {super();this.attachShadow({ mode: 'open' });this._open = false;this._view = 'idle'; // idle | loading | list | signedout | errorthis._items = [];this._error = '';this._chosen = '';this._render();}get key() { return this.getAttribute('key') || ''; }get loggedIn() { return this.hasAttribute('logged-in'); }set loggedIn(v) { this.toggleAttribute('logged-in', !!v); }reset() { this.loggedIn = false; }attributeChangedCallback(name) {if (name === 'logged-in' && !this.loggedIn) this._chosen = '';this._open = false;this._render();}async _toggle() {if (this._open) { this._open = false; this._render(); return; }this._open = true;this._view = 'loading';this._render();try {const r = await fetch(IDENT + '/api/selector/identities?key=' + encodeURIComponent(this.key), { credentials: 'include', cache: 'no-store' });if (!r.ok) throw new Error('HTTP ' + r.status);const j = await r.json();this._items = j.identities || [];this._view = j.signedIn ? 'list' : 'signedout';} catch (e) {// a refused origin/key has no CORS headers: the browser gives us nothing but thisthis._view = 'error';this._error = 'ident does not answer this site. Is it registered in ident with this API key?';}if (this._open) this._render();}async _choose(id, name) {this._view = 'loading';this._render();try {const r = await fetch(IDENT + '/api/selector/choose?key=' + encodeURIComponent(this.key), {method: 'POST', credentials: 'include', cache: 'no-store',headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identity: id }),});const j = await r.json().catch(() => ({}));if (!r.ok || !j.code) throw new Error(j.error || ('HTTP ' + r.status));this._chosen = name;this._open = false;this._render();// THE LOGIN: the host's server trades this code (POST /api/exchange, key + secret)this.dispatchEvent(new CustomEvent('ident-login', { detail: { code: j.code }, bubbles: true, composed: true }));} catch (e) {this._view = 'error';this._error = 'ident refused: ' + e.message;this._render();}}_render() {const root = this.shadowRoot;if (this.loggedIn) {root.innerHTML = `<style>${CSS}</style><span part="status" id="status"><span class="check" aria-hidden="true">✓</span>` +`<span>logged in with <span class="mark">ident</span>${this._chosen ? ' · ' + esc(this._chosen) : ''}</span></span>`;return;}let panel = '';if (this._open) {let body = '';if (this._view === 'loading') body = `<p part="message" id="message">…</p>`;else if (this._view === 'list') {body = `<p class="head">Choose the identity to log in with</p><ul id="list">` +this._items.map((it) => `<li><button type="button" part="identity" data-id="${esc(it.id)}">${esc(it.name)}</button></li>`).join('') + `</ul>`;} else if (this._view === 'signedout') {body = `<p part="message" id="message">You are not signed in to ident. <a part="link" id="signin" href="${esc(IDENT)}/" target="_blank" rel="noopener">Sign in to ident</a>, then choose again.</p>`;} else if (this._view === 'error') body = `<p part="message" id="message">${esc(this._error)}</p>`;panel = `<div part="panel" id="panel">${body}</div>`;}root.innerHTML = `<style>${CSS}</style><button type="button" part="button" id="choose" aria-expanded="${this._open}">` +`<span>choose <span class="mark">ident</span></span><span class="caret" aria-hidden="true">${this._open ? '▲' : '▼'}</span></button>${panel}`;root.getElementById('choose').addEventListener('click', () => this._toggle());for (const b of root.querySelectorAll('[part~="identity"]')) {b.addEventListener('click', () => this._choose(b.dataset.id, b.textContent));}}}customElements.define('ident-selector', IdentSelector);})();
Branches
- mainmain branch
Latest commits
- 91017164ident mission 009 (1/4): file moves — the root .hl files into lib/ (api.hl → lib/api-helpers.hl), styles.hl → components/styles.hl; imports adjusted, no other changemre
- f8bdcbc2ident: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); all gates greenmre
- ff78726cident: Hybriel master 190aa11d (fc838894 GC correctness, #127, #126 closure scopes); gates all greenmre
- a3a7d21aident: Hybriel master 8efba065 (#126 GC by bytes, #48 lambda params copy); session-writing lambdas take &sessionmre
- 98226b41antcolony#40: mission references point to the moved missionsmre
- ff805b9aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 51a7bcdfident: Hybriel master 73267707 (#122); /code uses the new page() signature; pending address passed as parameter; once-checksmre
- 836f644fident#24: installable app (manifest, service worker, data-free offline /start), own iconmre
- 8bebbbf2deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- cc063ea2deploy.sh: never send .git or .gitignore to Byrodinmre
- 81b15b7bState of 2026-09-27, before the move to gitoriamre