158 lines
5.3 KiB
TypeScript
158 lines
5.3 KiB
TypeScript
import { loadSession, validateSession, signIn, saveSession } from '../../auth.js';
|
|
import { readMeta, writeMeta } from './daemon.js';
|
|
import { getGatewayUrl, promptLine, promptSecret } from './login.js';
|
|
|
|
interface MintedToken {
|
|
id: string;
|
|
label: string;
|
|
plaintext: string;
|
|
}
|
|
|
|
/**
|
|
* Call POST /api/admin/tokens with the session cookie and return the minted token.
|
|
* Exits the process on network or auth errors.
|
|
*/
|
|
export async function mintAdminToken(
|
|
gatewayUrl: string,
|
|
cookie: string,
|
|
label: string,
|
|
): Promise<MintedToken> {
|
|
let res: Response;
|
|
try {
|
|
res = await fetch(`${gatewayUrl}/api/admin/tokens`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Cookie: cookie,
|
|
Origin: gatewayUrl,
|
|
},
|
|
body: JSON.stringify({ label, scope: 'admin' }),
|
|
});
|
|
} catch (err) {
|
|
console.error(
|
|
`Could not reach gateway at ${gatewayUrl}: ${err instanceof Error ? err.message : String(err)}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (res.status === 401 || res.status === 403) {
|
|
console.error(
|
|
`Session rejected by the gateway (${res.status.toString()}) — your session may be expired.`,
|
|
);
|
|
console.error('Run: mosaic gateway login');
|
|
process.exit(2);
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => '');
|
|
console.error(
|
|
`Gateway rejected token creation (${res.status.toString()}): ${body.slice(0, 200)}`,
|
|
);
|
|
process.exit(3);
|
|
}
|
|
|
|
const data = (await res.json()) as { id: string; label: string; plaintext: string };
|
|
return { id: data.id, label: data.label, plaintext: data.plaintext };
|
|
}
|
|
|
|
/**
|
|
* Persist the new token into meta.json and print the confirmation banner.
|
|
*
|
|
* Emits a warning when the target gateway differs from the locally installed one,
|
|
* so operators are aware that meta.json may not reflect the intended gateway.
|
|
*/
|
|
export function persistToken(gatewayUrl: string, minted: MintedToken): void {
|
|
const meta = readMeta() ?? {
|
|
version: 'unknown',
|
|
installedAt: new Date().toISOString(),
|
|
entryPoint: '',
|
|
host: new URL(gatewayUrl).hostname,
|
|
port: parseInt(new URL(gatewayUrl).port || '14242', 10),
|
|
};
|
|
|
|
// Warn when the target gateway does not match the locally installed one
|
|
const targetHost = new URL(gatewayUrl).hostname;
|
|
if (targetHost !== meta.host) {
|
|
console.warn(
|
|
`Warning: token was minted against ${gatewayUrl} but is being saved to the local` +
|
|
` meta.json (host: ${meta.host}). Copy the token manually if targeting a remote gateway.`,
|
|
);
|
|
}
|
|
|
|
writeMeta({ ...meta, adminToken: minted.plaintext });
|
|
|
|
const preview = `${minted.plaintext.slice(0, 8)}...`;
|
|
console.log();
|
|
console.log(`Token minted: ${minted.label}`);
|
|
console.log(`Preview: ${preview}`);
|
|
console.log('Token saved to meta.json. Use it with admin endpoints.');
|
|
}
|
|
|
|
/**
|
|
* Require a valid session for the given gateway URL.
|
|
* Returns the session cookie or exits if not authenticated.
|
|
*/
|
|
export async function requireSession(gatewayUrl: string): Promise<string> {
|
|
const session = loadSession(gatewayUrl);
|
|
if (session) {
|
|
const valid = await validateSession(gatewayUrl, session.cookie);
|
|
if (valid) return session.cookie;
|
|
}
|
|
console.error('Not signed in or session expired.');
|
|
console.error('Run: mosaic gateway login');
|
|
process.exit(2);
|
|
}
|
|
|
|
/**
|
|
* Ensure a valid session for the gateway, prompting for credentials if needed.
|
|
* On sign-in failure, prints the error and exits non-zero.
|
|
* Returns the session cookie.
|
|
*/
|
|
export async function ensureSession(gatewayUrl: string): Promise<string> {
|
|
// Try the stored session first
|
|
const session = loadSession(gatewayUrl);
|
|
if (session) {
|
|
const valid = await validateSession(gatewayUrl, session.cookie);
|
|
if (valid) return session.cookie;
|
|
console.log('Stored session is invalid or expired. Please sign in again.');
|
|
} else {
|
|
console.log(`No session found for ${gatewayUrl}. Please sign in.`);
|
|
}
|
|
|
|
// Prompt for credentials — password must not be echoed to the terminal
|
|
const email = await promptLine('Email: ');
|
|
// Do not trim password — it may contain intentional leading/trailing whitespace
|
|
const password = await promptSecret('Password: ');
|
|
|
|
const auth = await signIn(gatewayUrl, email, password).catch((err: unknown) => {
|
|
console.error(err instanceof Error ? err.message : String(err));
|
|
process.exit(2);
|
|
});
|
|
|
|
saveSession(gatewayUrl, auth);
|
|
console.log(`Signed in as ${auth.email}`);
|
|
return auth.cookie;
|
|
}
|
|
|
|
/**
|
|
* `mosaic gateway config rotate-token` — requires an existing valid session.
|
|
*/
|
|
export async function runRotateToken(gatewayUrl?: string): Promise<void> {
|
|
const url = getGatewayUrl(gatewayUrl);
|
|
const cookie = await requireSession(url);
|
|
const label = `CLI rotated token (${new Date().toISOString().slice(0, 10)})`;
|
|
const minted = await mintAdminToken(url, cookie, label);
|
|
persistToken(url, minted);
|
|
}
|
|
|
|
/**
|
|
* `mosaic gateway config recover-token` — prompts for login if no session exists.
|
|
*/
|
|
export async function runRecoverToken(gatewayUrl?: string): Promise<void> {
|
|
const url = getGatewayUrl(gatewayUrl);
|
|
const cookie = await ensureSession(url);
|
|
const label = `CLI recovery token (${new Date().toISOString().slice(0, 16).replace('T', ' ')})`;
|
|
const minted = await mintAdminToken(url, cookie, label);
|
|
persistToken(url, minted);
|
|
}
|