@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
177 lines (176 loc) • 6.95 kB
JavaScript
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})`);
}