feat(prometheus): real providers (Gemini/fal/Pollinations…) + secure key store
- Secure secrets: gitignored ~/pyre/.env (chmod 600) loaded into the API via
`node --env-file-if-exists`; keys never committed/logged/returned. .env.example
documents the vars. Free-first default (text=gemini, image=pollinations).
- @pyre/config: provider selection + key fields.
- @pyre/prometheus: real providers via fetch (no SDK deps) — Gemini/Anthropic/
OpenAI text, Pollinations(free)/fal/DeepInfra/Replicate image, OpenAI moderation;
`createProviders()` factory selects by config + key presence, falls back to stub.
29 tests.
- @pyre/api: /api/prometheus/generate builds providers from config; keys never logged.
Live-verified end-to-end: admin-gated generate returned a real Spawn ("Ashen
Golem"/$AGOL) with a Pollinations image on the $0 stub-text+free-image stack;
.env-loaded admin token enforced. typecheck 8/8, 150 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,22 +65,54 @@ import type {
|
||||
// Prometheus owns generation (built in parallel). We code to its published
|
||||
// signature: `generateSpawn(input, opts?): Promise<PrometheusGenerateResponse>`.
|
||||
// If the only typecheck error is this not-yet-exported symbol, that is expected.
|
||||
import { generateSpawn } from "@pyre/prometheus";
|
||||
import { generateSpawn, createProviders } from "@pyre/prometheus";
|
||||
import { getSellQuote, getShield } from "./jupiter.js";
|
||||
|
||||
/** The provider bundle returned by `@pyre/prometheus`'s `createProviders`. */
|
||||
type AiProviders = ReturnType<typeof createProviders>;
|
||||
|
||||
/**
|
||||
* Options accepted by `@pyre/prometheus`'s `generateSpawn`. Declared locally to
|
||||
* match the published signature without depending on its internal types.
|
||||
* match the published signature without depending on its internal types. The
|
||||
* provider overrides are typed off `createProviders`'s return so we never have
|
||||
* to spell out the providers' internal shapes.
|
||||
*/
|
||||
interface GenerateSpawnOpts {
|
||||
/** Chaos factor 0..1 controlling mutation strength. */
|
||||
chaos?: number;
|
||||
/** Optional manual operator theme seed. */
|
||||
operatorSeed?: string;
|
||||
/** Text provider override. */
|
||||
text?: AiProviders["text"];
|
||||
/** Image provider override. */
|
||||
image?: AiProviders["image"];
|
||||
/** Moderation provider override. */
|
||||
moderation?: AiProviders["moderation"];
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prometheus AI providers — built ONCE at module scope from operator config.
|
||||
//
|
||||
// SECURITY: the provider keys live only inside this bundle; they are NEVER
|
||||
// logged, never echoed in a response, and never included in an error body.
|
||||
// `createProviders` selects the configured text/image backends and wires the
|
||||
// API keys; an empty key simply means that backend is unconfigured.
|
||||
// ---------------------------------------------------------------------------
|
||||
const aiProviders = createProviders({
|
||||
textProvider: config.prometheusTextProvider,
|
||||
imageProvider: config.prometheusImageProvider,
|
||||
keys: {
|
||||
gemini: config.geminiApiKey,
|
||||
anthropic: config.anthropicApiKey,
|
||||
openai: config.openaiApiKey,
|
||||
fal: config.falKey,
|
||||
deepinfra: config.deepinfraApiKey,
|
||||
replicate: config.replicateApiToken,
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Admin gate (§16): admin endpoints require an `x-admin-token` header equal to
|
||||
// the configured `adminApiToken`. When the token is unset (empty), admin
|
||||
@@ -1223,7 +1255,12 @@ app.post<{ Body: GenerateBody }>(
|
||||
...(operatorSeed !== undefined ? { manualThemeSeed: operatorSeed } : {}),
|
||||
};
|
||||
|
||||
const opts: GenerateSpawnOpts = {};
|
||||
// Wire the real providers (built once at module scope) into every call.
|
||||
const opts: GenerateSpawnOpts = {
|
||||
text: aiProviders.text,
|
||||
image: aiProviders.image,
|
||||
moderation: aiProviders.moderation,
|
||||
};
|
||||
if (typeof chaos === "number") opts.chaos = chaos;
|
||||
if (operatorSeed !== undefined) opts.operatorSeed = operatorSeed;
|
||||
|
||||
@@ -1231,7 +1268,11 @@ app.post<{ Body: GenerateBody }>(
|
||||
try {
|
||||
result = await generateSpawn(input, opts);
|
||||
} catch (err) {
|
||||
request.log.error({ err }, "Prometheus generation failed");
|
||||
// SECURITY: log only the error MESSAGE server-side — never the opts (they
|
||||
// carry provider keys), never the request body, and never echo any detail
|
||||
// to the client (the response is a fixed 502 with no key material).
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
request.log.error({ detail }, "Prometheus generation failed");
|
||||
return reply.code(502).send({ error: "generation failed" });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user