UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

177 lines (176 loc) 6.95 kB
import { clearContextConfig, clearCredentials, decodeJwtPayload, discoverConfigFromWellKnown, forceRefreshToken, getContextConfig, getValidToken, readAllContextConfigs, readAllCredentials, readCredentials, runLoginFlow, tokenStillValid, writeContextConfig, RESERVED_REGISTRY_CONTEXT, } from "../commands/login.js"; import { projectRoleKeys } from "./aws-auth.js"; import { firstPartyDomainFor } from "./first-party-contexts.js"; import { deriveRegistryBroker } from "./registry-broker.js"; import { MeshCliError } from "./errors.js"; import { logInfo } from "./log.js"; export const REGISTRY_ALIAS = "mesh.dev"; export const REGISTRY_DOMAIN = (() => { const domain = firstPartyDomainFor(REGISTRY_ALIAS); if (!domain) { throw new Error(`FIRST_PARTY_CONTEXTS has no entry for the registry alias "${REGISTRY_ALIAS}"`); } return domain; })(); export const REGISTRY_CREDENTIAL_KEY = RESERVED_REGISTRY_CONTEXT; export function isReservedRegistryContext(context) { return context === REGISTRY_CREDENTIAL_KEY; } export class RegistryIdentityError extends MeshCliError { constructor(detail) { super(`Could not resolve the Mesh package registry (${detail}).\n` + ` Discovery endpoint: https://cli.${REGISTRY_DOMAIN}/.well-known/mesh.json`, { remediation: { command: "mesh registry login # retry once you are online" } }); this.name = "RegistryIdentityError"; } } function toIdentity(cfg) { if (!cfg?.issuer || !cfg.clientId) return null; const registryBroker = cfg.registryBroker ?? deriveRegistryBroker(REGISTRY_CREDENTIAL_KEY, () => cfg); if (!registryBroker) return null; return { issuer: cfg.issuer, clientId: cfg.clientId, registryBroker, discoveredAt: cfg.discoveredAt ?? new Date(0).toISOString(), }; } export async function resolveRegistryIdentity(opts = {}) { if (!opts.refresh) { const cached = toIdentity(getContextConfig(REGISTRY_CREDENTIAL_KEY)); if (cached) return cached; } const discovered = await discoverConfigFromWellKnown(REGISTRY_DOMAIN, REGISTRY_CREDENTIAL_KEY, { quiet: true, }); if (!discovered) { throw new RegistryIdentityError(`discovery on ${REGISTRY_DOMAIN} failed — are you online?`); } const stored = { ...discovered, discoveredAt: new Date().toISOString() }; const identity = toIdentity(stored); if (!identity) { throw new RegistryIdentityError(`the discovery document names no registry broker`); } writeContextConfig(REGISTRY_CREDENTIAL_KEY, stored); return identity; } export function registrySessionCandidates(issuer, configs = readAllContextConfigs(), credentials = readAllCredentials()) { const names = issuer ? [REGISTRY_CREDENTIAL_KEY, REGISTRY_ALIAS, REGISTRY_DOMAIN] : [REGISTRY_CREDENTIAL_KEY]; return names.filter((name) => { if (!(name in credentials)) return false; const cfg = configs[name]; if (!cfg?.issuer) return false; return issuer ? cfg.issuer === issuer : true; }); } export function readRegistrySession() { const identity = toIdentity(getContextConfig(REGISTRY_CREDENTIAL_KEY)); for (const context of registrySessionCandidates(identity?.issuer ?? null)) { const creds = readCredentials(context); if (creds && tokenStillValid(creds.expiresAt)) { return { context, email: creds.email ?? "unknown", expiresAt: creds.expiresAt }; } } return null; } export async function ensureRegistrySession(opts) { let identity; try { identity = await resolveRegistryIdentity(); } catch (err) { return { ok: false, reason: "no-identity", detail: err.message }; } for (const context of registrySessionCandidates(identity.issuer)) { const token = await getValidToken(context); if (!token) continue; const creds = readCredentials(context); if (creds) return { ok: true, context, email: creds.email ?? "unknown", expiresAt: creds.expiresAt }; } if (!opts.interactive) { return { ok: false, reason: "no-tty", detail: "no registry session is cached and this run cannot open a sign-in", }; } const stored = getContextConfig(REGISTRY_CREDENTIAL_KEY); if (!stored) { writeContextConfig(REGISTRY_CREDENTIAL_KEY, { issuer: identity.issuer, clientId: identity.clientId, registryBroker: identity.registryBroker, }); } logInfo(opts.device ? "No registry session — sign in with the device code below…" : "No registry session — opening your browser to sign in… (headless? use --device)"); try { await runLoginFlow(REGISTRY_CREDENTIAL_KEY, identity, { device: opts.device }); } catch (err) { return { ok: false, reason: "login-failed", detail: err.message }; } const creds = readCredentials(REGISTRY_CREDENTIAL_KEY); if (!creds) { return { ok: false, reason: "login-failed", detail: "sign-in completed but no session was cached" }; } return { ok: true, context: REGISTRY_CREDENTIAL_KEY, email: creds.email ?? "unknown", expiresAt: creds.expiresAt, }; } export function registryTokenRoles(idToken) { if (!idToken) return []; try { return projectRoleKeys(decodeJwtPayload(idToken)).sort(); } catch { return []; } } export async function refreshRegistrySession(context) { const before = registryTokenRoles(readCredentials(context)?.idToken); const idToken = await forceRefreshToken(context); if (!idToken) return { reminted: false, rolesChanged: false }; const after = registryTokenRoles(idToken); const same = after.length === before.length && after.every((role, i) => role === before[i]); return { reminted: true, rolesChanged: !same }; } export function clearRegistrySession() { clearCredentials(REGISTRY_CREDENTIAL_KEY); clearContextConfig(REGISTRY_CREDENTIAL_KEY); } export function identityChanged(before, after) { return (before.issuer !== after.issuer || before.clientId !== after.clientId || before.registryBroker !== after.registryBroker); } export function describeSessionExpiry(expiresAt, now = Date.now()) { const ms = new Date(expiresAt).getTime() - now; if (!(ms > 0)) return "expired"; const totalMinutes = Math.floor(ms / 60_000); const h = Math.floor(totalMinutes / 60); const m = totalMinutes % 60; return h > 0 ? `${h}h ${m}m` : `${m}m`; } export function logRegistryIdentity(identity) { let host = identity.registryBroker; try { host = new URL(identity.registryBroker).host; } catch { } logInfo(`Mesh package registry: mesh-platform (broker ${host})`); }