mcp --http: --broker-profiles (store keys from the broker vault, memory only), --read-only, project by folder name under the roots

This commit is contained in:
CRTkafa 2026-10-29 18:38:11 +07:00
parent 164ba390dd
commit a108aae53f
6 changed files with 121 additions and 10 deletions

View file

@ -325,3 +325,35 @@ export async function playPreflight(command: string, root: string, pkg: string |
if (o.dry) { for (const g of gaps) o.warn(`${g.message}. Fix: ${g.fix}`); return gaps; }
throw new Error(`${command} stopped before any call to Google Play:\n${gapsText(gaps)}`);
}
export type StoreKind = 'asc-api-key' | 'play-service-account';
/**
* A store credential of one profile from the broker's vault (GET /api/store-credentials/<profile>/<kind>, a key with
* the store-credentials scope): { secret, metadata }. For a long-running server (`mcp --http --broker-profiles`) that
* keeps no key files; null when the broker has none for that profile. Never logged.
*/
export async function brokerStoreCredential(cfg: BrokerConfig, profile: string, kind: StoreKind): Promise<{ secret: string; metadata: Record<string, unknown> } | null> {
const a = await request(cfg, 'GET', `/api/store-credentials/${encodeURIComponent(profile)}/${kind}`);
if (a.status === 404) return null;
if (a.status === 401 || a.status === 403) throw new BrokerRefused(`credential broker ${host(cfg)} refused store credentials for ${profile} (${a.status} ${errOf(a)}): the key needs the store-credentials scope and the profile`, a.status);
const j = a.json as { secret?: unknown; metadata?: unknown } | null;
if (a.status !== 200 || !j || typeof j.secret !== 'string') throw new BrokerUnavailable(`credential broker ${host(cfg)} -> ${a.status} for ${profile}/${kind}`);
return { secret: j.secret, metadata: j.metadata && typeof j.metadata === 'object' ? (j.metadata as Record<string, unknown>) : {} };
}
/** The env a child process needs to use these profiles from the broker (STOREBOUND_PROFILE_<NAME>_<KEY>, inline PEM /
* JSON; see credentials.ts). Profiles the broker has nothing for are left out. */
export async function brokerProfileEnv(cfg: BrokerConfig, profiles: string[]): Promise<Record<string, string>> {
const env: Record<string, string> = {};
const name = (p: string, k: string) => `STOREBOUND_PROFILE_${p.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_${k}`;
await Promise.all(profiles.map(async p => {
const [asc, play] = await Promise.all([brokerStoreCredential(cfg, p, 'asc-api-key'), brokerStoreCredential(cfg, p, 'play-service-account')]);
if (asc) {
const { issuerId, keyId } = asc.metadata;
if (typeof issuerId === 'string' && typeof keyId === 'string') Object.assign(env, { [name(p, 'ASC_P8')]: asc.secret, [name(p, 'ASC_KEY_ID')]: keyId, [name(p, 'ASC_ISSUER_ID')]: issuerId });
}
if (play) env[name(p, 'PLAY_SERVICE_ACCOUNT')] = play.secret;
}));
return env;
}