ident
All repositories: gitoria
5.1 KB
// selector.hl — THE IDENTITY SELECTOR's server side (piece 3 of 6, ticket #26; CONCEPT.md// "Flow 2: identity selector" and "Decided 2026-09-24"). Statics only; the routes are in// project.hl, the browser half is selector.js (served at /selector.js).//// GET /api/selector/identities?key=<api key> → { signedIn, identities: [{ id, name }] }// POST /api/selector/choose?key=<api key> { identity: <id from the list> } → { code }// OPTIONS /api/selector/choose?key=… the CORS preflight of the POST//// CORS: ident answers ONLY a request whose Origin header is one of the app's registered// origins AND whose key is that app's API key. Then (and only then) the answer carries// `Access-Control-Allow-Origin: <that exact origin>` and `…-Allow-Credentials: true`// (never `*`). Anything else is a 403 without those headers: the browser hands the page// nothing, and nothing is written.// The ident user is the one of the `identsid` cookie the browser sends along (credentials;// SameSite=Lax, so it only travels from the SAME SITE — our apps; other ports of the same// host count). Function routes get no session: it is resolved from the cookie (hybriel #18).//// THE LIST'S ids are not ident's identity ids: each is sha256(app's secret hash : identity)// cut to 32 hex — stable for one app, different for every app, not guessable, and it// means nothing outside this selector. The CODE of a choice follows the login button's// rules exactly (apps.hl issueCode: single use, 60 s, this app only); the host's server// trades it with POST /api/exchange (key + secret) for the app-specific identity id.import { Response } from 'hl:http1'import { sha256 } from 'hl:crypto'import { appByKey, originsOf, issueCode } from './apps.hl'import { identityRecords, labelOf, accountOfSession } from './store.hl'import { strictBody } from './api.hl'static jsonType = 'application/json; charset=utf-8'// the refusal: no CORS headers → the browser gives the page nothingstatic refuse = (status, message) => {return new Response(JSON.stringify({ error = message }), { status = status headers = { 'Content-Type' = jsonType 'Cache-Control' = 'no-store' 'Vary' = 'Origin' } })}static corsHeaders = (origin) => {return { 'Content-Type' = jsonType 'Cache-Control' = 'no-store' 'Vary' = 'Origin' 'Access-Control-Allow-Origin' = origin 'Access-Control-Allow-Credentials' = 'true' }}static answer = (origin, status, value) => {return new Response(JSON.stringify(value), { status = status headers = corsHeaders(origin) })}// WHO ASKS: answers { app (record), origin } or { status, error }static caller = (req) => {let origin = req.headers['origin']if (origin == null || origin == '') { return { status = 403 error = 'the selector answers browsers only (no Origin header)' } }let q = req.query != null ? req.query : {}let a = appByKey(q.key)if (a == null) { return { status = 403 error = 'no app has this API key' } }if (!originsOf(a).includes(origin)) { return { status = 403 error = 'this origin is not registered for the app' } }return { app = a origin = origin }}// (since mission 009 the identity's id is its UUID, so every pick id changed once then)static pickOf = (appRec, identityRec) => { return sha256(appRec.secretHash + ':' + identityRec.id).slice(0, 32) }// the account's identities for the app's page: the id to pick it by and the identity// name (CONCEPT.md: "that one is shown in the selector"; blank → the list's fallback)static selectorList = (appRec, accountId) => {let out = []let n = 1for (r of identityRecords(accountId)) {out.push({ id = pickOf(appRec, r) name = labelOf(r, n) })n = n + 1}return out}// GET /api/selector/identities?key=static selectorIdentities = (req, session) => {let c = caller(req)if (c.error != null) { return refuse(c.status, c.error) }if (req.method != 'GET') { return answer(c.origin, 405, { error = 'GET only' }) }let me = accountOfSession(session)if (me == null) { return answer(c.origin, 200, { signedIn = false identities = [] }) }return answer(c.origin, 200, { signedIn = true identities = selectorList(c.app, me.id) })}// POST /api/selector/choose?key= { identity } (+ its OPTIONS preflight)static selectorChoose = (req, session) => {let c = caller(req)if (c.error != null) { return refuse(c.status, c.error) }if (req.method == 'OPTIONS') {let h = corsHeaders(c.origin)h['Access-Control-Allow-Methods'] = 'POST'h['Access-Control-Allow-Headers'] = 'Content-Type'h['Access-Control-Max-Age'] = '600'return new Response('', { status = 204 headers = h })}if (req.method != 'POST') { return answer(c.origin, 405, { error = 'POST only' }) }let b = strictBody(req, { identity = { type = 'String' required = true } })if (b.error != null) { return answer(c.origin, 400, { error = b.error }) }let me = accountOfSession(session)if (me == null) { return answer(c.origin, 401, { error = 'you are not signed in to ident' }) }for (r of identityRecords(me.id)) {if (pickOf(c.app, r) == b.body.identity) {return answer(c.origin, 200, { code = issueCode(c.app.id, r.id) })}}return answer(c.origin, 400, { error = 'no such identity' })}
Branches
- mainmain branch