Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 7x 7x 7x 2x 2x 2x 2x 2x 2x 2x 2x 13x 13x 13x 5x 5x 8x 8x 13x 13x 13x 13x 13x 13x 2x 2x 6x 6x 2x 2x 2x 2x 2x 2x 2x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 4x 5x 1x 1x 3x 3x | /**
* Pairing with a THH print service (Abacus Studio Phase 2a, #8.2).
*
* The service's admin UI hands the user a one-shot artifact `CODE@host`
* (single-use, ~10-minute TTL). We exchange it server-side for a durable
* bearer token via the unauthenticated POST /pair, then seal the token at
* rest. Codes and tokens are credentials: never logged, never echoed back
* to the browser.
*/
import { PrintServiceError, ensureOk, printServiceFetch } from './print-service-fetch'
export interface PairingArtifact {
code: string
/** Normalized origin with scheme, no trailing slash */
origin: string
}
/** Malformed `CODE@host` input. Messages are user-safe (never contain the code). */
export class PairingArtifactError extends Error {
constructor(message: string) {
super(message)
this.name = 'PairingArtifactError'
}
}
/**
* Parse a `CODE@host` pairing artifact. Split on the LAST `@` — codes are
* opaque and could themselves contain `@`. The host part may omit the scheme
* (defaults to https). Throws {@link PairingArtifactError} on malformed input.
*/
export function parsePairingArtifact(artifact: string): PairingArtifact {
const trimmed = artifact.trim()
const at = trimmed.lastIndexOf('@')
if (at <= 0 || at === trimmed.length - 1) {
throw new PairingArtifactError('Pairing code must look like CODE@host')
}
const code = trimmed.slice(0, at)
const host = trimmed.slice(at + 1)
const withScheme = /^https?:\/\//i.test(host) ? host : `https://${host}`
let url: URL
try {
url = new URL(withScheme)
} catch {
throw new PairingArtifactError('Pairing code has an unrecognizable host part')
}
if (url.pathname !== '/' || url.search || url.hash) {
throw new PairingArtifactError('Pairing host must be a bare origin (no path)')
}
return { code, origin: url.origin }
}
/**
* Exchange a pairing code for a durable bearer token.
* Returns the PLAINTEXT token — the caller seals it before persisting.
* Throws {@link PrintServiceError} on any upstream failure (invalid/expired
* code, unreachable service, malformed response).
*/
export async function pairWithService(input: {
origin: string
code: string
/** Client name shown in the service's admin (e.g. "abaci.one (user@example.com)") */
clientName: string
}): Promise<{ token: string }> {
const res = await printServiceFetch({ origin: input.origin }, '/pair', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: input.code, name: input.clientName }),
})
await ensureOk(res.clone())
const body = (await res.json().catch(() => null)) as { token?: unknown } | null
if (!body || typeof body.token !== 'string' || !body.token) {
throw new PrintServiceError(res.status, 'bad_response', 'pair succeeded but no token returned')
}
return { token: body.token }
}
|