@agentproto/providers-store 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -89,6 +89,23 @@ declare function removeProviderKey(provider: string): Promise<boolean>;
89
89
  * satisfied too. Returns the list of provider names actually injected (for a
90
90
  * boot log that names providers, never values) — one entry per provider, not
91
91
  * per env name.
92
+ *
93
+ * Providers are visited in SORTED name order, never `providers.json` key
94
+ * order. Two providers can legitimately map to the same env name, and when
95
+ * their keys are DIFFERENT secrets only one of them can win: `opencode`
96
+ * (OpenCode Zen) and `opencode-go` both read `OPENCODE_API_KEY`, so whichever
97
+ * is injected first takes the var and the other's "explicit env wins" check
98
+ * sees it already set. Iterating the file's own key order made that outcome
99
+ * depend on which `auth provider set` the operator happened to run first —
100
+ * two hosts with the same two keys could inject different secrets. Sorting
101
+ * makes the winner a stable, documentable fact (`opencode` < `opencode-go`,
102
+ * so Zen wins). It does NOT make the collision harmless: a host that holds
103
+ * both keys can still only inject one, and the per-spawn auth profile
104
+ * (`accessProfile`, whose endpoint is `opencode` or `opencode-go`) is the
105
+ * real disambiguator — see the note on `PROVIDER_KEY_ENV` in
106
+ * `@agentproto/model-catalog`. The same-secret pairs this also covers
107
+ * (`openai`/`openai-realtime`, `google`/`gemini-live`) were never affected,
108
+ * because either order injects the same value.
92
109
  */
93
110
  declare function injectProviderKeysIntoEnv(env?: NodeJS.ProcessEnv): Promise<string[]>;
94
111
 
package/dist/index.mjs CHANGED
@@ -81,7 +81,8 @@ async function removeProviderKey(provider) {
81
81
  async function injectProviderKeysIntoEnv(env = process.env) {
82
82
  const file = await loadProviders();
83
83
  const injected = [];
84
- for (const [provider, entry] of Object.entries(file.providers)) {
84
+ for (const provider of Object.keys(file.providers).sort()) {
85
+ const entry = file.providers[provider];
85
86
  if (!entry?.apiKey) continue;
86
87
  const names = /* @__PURE__ */ new Set([providerEnvVar(provider), ...providerEnvAliases(provider)]);
87
88
  let didInject = false;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * `~/.agentproto/providers.json` — the provider API-key store (mode 0600).\n *\n * Distinct from `credentials.json` (host-binding OAuth tokens for\n * `serve --connect`) and from per-adapter `setup[]` tokens. This file holds\n * the LLM/model **provider** keys (anthropic, openrouter, openai, …) that the\n * model gateways in spawned agents read from the environment.\n *\n * Why a store at all: before this, the only way to give the daemon a provider\n * key was an ambient `export FOO_API_KEY=…` in whatever shell launched\n * `serve`. That's invisible, easy to forget, and per-shell. Storing the keys\n * here (0600, same-user-only) lets `serve` inject them at boot — set once,\n * works for every daemon. Explicit env still wins (see injectProviderKeysIntoEnv).\n *\n * Keys never leave this file except into the daemon's own process env; they\n * are never logged. A browser-loaded localhost page can't read a 0600 file —\n * the same defence the runtime.json bearer token relies on.\n */\n\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\"\nimport { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\nimport { PROVIDER_KEY_ENV, PROVIDER_KEY_ENV_ALIASES } from \"@agentproto/model-catalog\"\n\n/**\n * Canonical provider → environment-variable name. The spawned model gateways\n * (Mastra in mastra-agent, hermes/opencode's routers) read the provider key\n * from these env names. Aligned with the adapter manifests' `models.env`\n * maps + the common SDK conventions.\n *\n * DERIVED from `@agentproto/model-catalog`'s {@link PROVIDER_KEY_ENV} (the\n * single source of truth, co-located with the provider enum — DECISION 1) so\n * the two maps that used to restate the same fact can't drift, plus the few\n * NON-catalog gateway providers this store also fronts (`groq`,\n * `vercel-ai-gateway`) which aren't LLM-catalog `provider` values but still\n * carry a key here. Adding a catalog provider forces adding its key env in the\n * catalog (the `Record<CatalogProvider, string>` is exhaustive), and it flows\n * through to here for free.\n */\nexport const PROVIDER_ENV_VARS: Readonly<Record<string, string>> = {\n ...PROVIDER_KEY_ENV,\n // ── Non-catalog gateway providers (not LLM-catalog `provider` enum values,\n // so absent from PROVIDER_KEY_ENV, but still keyed through this store) ──\n groq: \"GROQ_API_KEY\",\n xai: \"XAI_API_KEY\",\n // Vercel AI Gateway — a gateway provider, like openrouter, that fronts many\n // upstream model families behind one key.\n \"vercel-ai-gateway\": \"AI_GATEWAY_API_KEY\",\n}\n\n/**\n * Provider → ADDITIONAL env-var names its stored key also satisfies, beyond the\n * canonical {@link PROVIDER_ENV_VARS} name. DERIVED from the catalog's\n * {@link PROVIDER_KEY_ENV_ALIASES} (the single source of truth, same as\n * `PROVIDER_ENV_VARS` derives from `PROVIDER_KEY_ENV`) so the alias facts live\n * in one place. `injectProviderKeysIntoEnv` sets the canonical name AND every\n * alias here, letting one `auth provider set` satisfy a consumer that reads a\n * different env name (e.g. mastracode reads `GOOGLE_API_KEY`, not our canonical\n * `GOOGLE_GENERATIVE_AI_API_KEY`). No non-catalog gateway provider has a known\n * alias today; add them alongside the spread if one ever does.\n */\nexport const PROVIDER_ENV_ALIASES: Readonly<Record<string, readonly string[]>> = {\n ...PROVIDER_KEY_ENV_ALIASES,\n}\n\nexport type KnownProvider = keyof typeof PROVIDER_ENV_VARS\n\nexport interface ProviderEntry {\n /** The provider API key (or gateway key). */\n apiKey: string\n /** Optional custom base URL (self-hosted / proxy). */\n baseUrl?: string\n /** Wall-clock ISO timestamp the key was last set. */\n updatedAt: string\n}\n\nexport interface ProvidersFile {\n version: 1\n providers: Record<string, ProviderEntry>\n}\n\n/** Fresh empty file each call — never share the `providers` object, or a\n * later `setProviderKey` mutation leaks into every other load in-process. */\nfunction emptyFile(): ProvidersFile {\n return { version: 1, providers: {} }\n}\n\nexport function providersPath(): string {\n return resolve(homedir(), \".agentproto\", \"providers.json\")\n}\n\n/** Resolve the env-var name for a provider (canonical map, else\n * `<PROVIDER>_API_KEY` upper-snake fallback so new providers still work). */\nexport function providerEnvVar(provider: string): string {\n return (\n PROVIDER_ENV_VARS[provider] ??\n `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, \"_\")}_API_KEY`\n )\n}\n\n/** Additional env-var names a provider's stored key also satisfies, beyond\n * {@link providerEnvVar}. Empty for providers with no verified alias. */\nexport function providerEnvAliases(provider: string): readonly string[] {\n return PROVIDER_ENV_ALIASES[provider] ?? []\n}\n\nexport async function loadProviders(): Promise<ProvidersFile> {\n try {\n const raw = await readFile(providersPath(), \"utf8\")\n const parsed = JSON.parse(raw) as unknown\n if (\n !parsed ||\n typeof parsed !== \"object\" ||\n !(\"providers\" in parsed) ||\n typeof (parsed as Record<string, unknown>).providers !== \"object\" ||\n (parsed as Record<string, unknown>).providers === null\n ) {\n return emptyFile()\n }\n return {\n version: 1,\n providers: (parsed as ProvidersFile).providers,\n }\n } catch {\n return emptyFile() // ENOENT / malformed → empty\n }\n}\n\n/**\n * Read a single provider's stored api key from `providers.json`, or undefined\n * when the provider has no entry (or the file doesn't exist). The EXPLICIT,\n * verifiable credential source for the billing-auth resolver's api-key mode\n * (`agentproto auth provider set <provider> <key>` writes it) — distinct from\n * the ambient shell env, which the resolver never reads. Returns the raw key;\n * only a non-secret fingerprint of it is ever surfaced back to a caller.\n */\nexport async function getProviderKey(provider: string): Promise<string | undefined> {\n const file = await loadProviders()\n const entry = file.providers[provider]\n return entry?.apiKey || undefined\n}\n\nasync function writeProviders(file: ProvidersFile): Promise<void> {\n const dir = join(homedir(), \".agentproto\")\n await mkdir(dir, { recursive: true })\n // mode 0600 so other local users can't read the keys.\n await writeFile(providersPath(), JSON.stringify(file, null, 2) + \"\\n\", {\n encoding: \"utf8\",\n mode: 0o600,\n })\n}\n\n/** Set (or replace) a provider's key. Returns the env-var it maps to. */\nexport async function setProviderKey(\n provider: string,\n apiKey: string,\n baseUrl?: string,\n): Promise<string> {\n const file = await loadProviders()\n file.providers[provider] = {\n apiKey,\n ...(baseUrl ? { baseUrl } : {}),\n updatedAt: new Date().toISOString(),\n }\n await writeProviders(file)\n return providerEnvVar(provider)\n}\n\n/** Remove a provider's key. Returns true if it existed. */\nexport async function removeProviderKey(provider: string): Promise<boolean> {\n const file = await loadProviders()\n if (!(provider in file.providers)) return false\n delete file.providers[provider]\n await writeProviders(file)\n return true\n}\n\n/**\n * Inject stored provider keys into a target env (default `process.env`).\n * **Explicit env always wins** — a var already set is never overwritten, so a\n * one-off `FOO_API_KEY=… serve` or a CI secret takes precedence over the\n * store. One provider key is written to its canonical env name AND every\n * verified alias ({@link providerEnvAliases}) — so a consumer that reads a\n * different env name for the same provider (e.g. mastracode reads\n * `GOOGLE_API_KEY`, not our canonical `GOOGLE_GENERATIVE_AI_API_KEY`) is\n * satisfied too. Returns the list of provider names actually injected (for a\n * boot log that names providers, never values) — one entry per provider, not\n * per env name.\n */\nexport async function injectProviderKeysIntoEnv(\n env: NodeJS.ProcessEnv = process.env,\n): Promise<string[]> {\n const file = await loadProviders()\n const injected: string[] = []\n for (const [provider, entry] of Object.entries(file.providers)) {\n if (!entry?.apiKey) continue\n // Canonical name first, then any verified aliases the same key satisfies.\n // De-dupe defends against an alias that equals the canonical name.\n const names = new Set([providerEnvVar(provider), ...providerEnvAliases(provider)])\n let didInject = false\n for (const name of names) {\n if (env[name]) continue // explicit env wins — evaluated per env name\n env[name] = entry.apiKey\n didInject = true\n }\n if (!didInject) continue // every name for this provider is already set\n if (entry.baseUrl) env[`${provider.toUpperCase().replace(/[^A-Z0-9]+/g, \"_\")}_BASE_URL`] = entry.baseUrl\n injected.push(provider)\n }\n return injected\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * `~/.agentproto/providers.json` — the provider API-key store (mode 0600).\n *\n * Distinct from `credentials.json` (host-binding OAuth tokens for\n * `serve --connect`) and from per-adapter `setup[]` tokens. This file holds\n * the LLM/model **provider** keys (anthropic, openrouter, openai, …) that the\n * model gateways in spawned agents read from the environment.\n *\n * Why a store at all: before this, the only way to give the daemon a provider\n * key was an ambient `export FOO_API_KEY=…` in whatever shell launched\n * `serve`. That's invisible, easy to forget, and per-shell. Storing the keys\n * here (0600, same-user-only) lets `serve` inject them at boot — set once,\n * works for every daemon. Explicit env still wins (see injectProviderKeysIntoEnv).\n *\n * Keys never leave this file except into the daemon's own process env; they\n * are never logged. A browser-loaded localhost page can't read a 0600 file —\n * the same defence the runtime.json bearer token relies on.\n */\n\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\"\nimport { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\nimport { PROVIDER_KEY_ENV, PROVIDER_KEY_ENV_ALIASES } from \"@agentproto/model-catalog\"\n\n/**\n * Canonical provider → environment-variable name. The spawned model gateways\n * (Mastra in mastra-agent, hermes/opencode's routers) read the provider key\n * from these env names. Aligned with the adapter manifests' `models.env`\n * maps + the common SDK conventions.\n *\n * DERIVED from `@agentproto/model-catalog`'s {@link PROVIDER_KEY_ENV} (the\n * single source of truth, co-located with the provider enum — DECISION 1) so\n * the two maps that used to restate the same fact can't drift, plus the few\n * NON-catalog gateway providers this store also fronts (`groq`,\n * `vercel-ai-gateway`) which aren't LLM-catalog `provider` values but still\n * carry a key here. Adding a catalog provider forces adding its key env in the\n * catalog (the `Record<CatalogProvider, string>` is exhaustive), and it flows\n * through to here for free.\n */\nexport const PROVIDER_ENV_VARS: Readonly<Record<string, string>> = {\n ...PROVIDER_KEY_ENV,\n // ── Non-catalog gateway providers (not LLM-catalog `provider` enum values,\n // so absent from PROVIDER_KEY_ENV, but still keyed through this store) ──\n groq: \"GROQ_API_KEY\",\n xai: \"XAI_API_KEY\",\n // Vercel AI Gateway — a gateway provider, like openrouter, that fronts many\n // upstream model families behind one key.\n \"vercel-ai-gateway\": \"AI_GATEWAY_API_KEY\",\n}\n\n/**\n * Provider → ADDITIONAL env-var names its stored key also satisfies, beyond the\n * canonical {@link PROVIDER_ENV_VARS} name. DERIVED from the catalog's\n * {@link PROVIDER_KEY_ENV_ALIASES} (the single source of truth, same as\n * `PROVIDER_ENV_VARS` derives from `PROVIDER_KEY_ENV`) so the alias facts live\n * in one place. `injectProviderKeysIntoEnv` sets the canonical name AND every\n * alias here, letting one `auth provider set` satisfy a consumer that reads a\n * different env name (e.g. mastracode reads `GOOGLE_API_KEY`, not our canonical\n * `GOOGLE_GENERATIVE_AI_API_KEY`). No non-catalog gateway provider has a known\n * alias today; add them alongside the spread if one ever does.\n */\nexport const PROVIDER_ENV_ALIASES: Readonly<Record<string, readonly string[]>> = {\n ...PROVIDER_KEY_ENV_ALIASES,\n}\n\nexport type KnownProvider = keyof typeof PROVIDER_ENV_VARS\n\nexport interface ProviderEntry {\n /** The provider API key (or gateway key). */\n apiKey: string\n /** Optional custom base URL (self-hosted / proxy). */\n baseUrl?: string\n /** Wall-clock ISO timestamp the key was last set. */\n updatedAt: string\n}\n\nexport interface ProvidersFile {\n version: 1\n providers: Record<string, ProviderEntry>\n}\n\n/** Fresh empty file each call — never share the `providers` object, or a\n * later `setProviderKey` mutation leaks into every other load in-process. */\nfunction emptyFile(): ProvidersFile {\n return { version: 1, providers: {} }\n}\n\nexport function providersPath(): string {\n return resolve(homedir(), \".agentproto\", \"providers.json\")\n}\n\n/** Resolve the env-var name for a provider (canonical map, else\n * `<PROVIDER>_API_KEY` upper-snake fallback so new providers still work). */\nexport function providerEnvVar(provider: string): string {\n return (\n PROVIDER_ENV_VARS[provider] ??\n `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, \"_\")}_API_KEY`\n )\n}\n\n/** Additional env-var names a provider's stored key also satisfies, beyond\n * {@link providerEnvVar}. Empty for providers with no verified alias. */\nexport function providerEnvAliases(provider: string): readonly string[] {\n return PROVIDER_ENV_ALIASES[provider] ?? []\n}\n\nexport async function loadProviders(): Promise<ProvidersFile> {\n try {\n const raw = await readFile(providersPath(), \"utf8\")\n const parsed = JSON.parse(raw) as unknown\n if (\n !parsed ||\n typeof parsed !== \"object\" ||\n !(\"providers\" in parsed) ||\n typeof (parsed as Record<string, unknown>).providers !== \"object\" ||\n (parsed as Record<string, unknown>).providers === null\n ) {\n return emptyFile()\n }\n return {\n version: 1,\n providers: (parsed as ProvidersFile).providers,\n }\n } catch {\n return emptyFile() // ENOENT / malformed → empty\n }\n}\n\n/**\n * Read a single provider's stored api key from `providers.json`, or undefined\n * when the provider has no entry (or the file doesn't exist). The EXPLICIT,\n * verifiable credential source for the billing-auth resolver's api-key mode\n * (`agentproto auth provider set <provider> <key>` writes it) — distinct from\n * the ambient shell env, which the resolver never reads. Returns the raw key;\n * only a non-secret fingerprint of it is ever surfaced back to a caller.\n */\nexport async function getProviderKey(provider: string): Promise<string | undefined> {\n const file = await loadProviders()\n const entry = file.providers[provider]\n return entry?.apiKey || undefined\n}\n\nasync function writeProviders(file: ProvidersFile): Promise<void> {\n const dir = join(homedir(), \".agentproto\")\n await mkdir(dir, { recursive: true })\n // mode 0600 so other local users can't read the keys.\n await writeFile(providersPath(), JSON.stringify(file, null, 2) + \"\\n\", {\n encoding: \"utf8\",\n mode: 0o600,\n })\n}\n\n/** Set (or replace) a provider's key. Returns the env-var it maps to. */\nexport async function setProviderKey(\n provider: string,\n apiKey: string,\n baseUrl?: string,\n): Promise<string> {\n const file = await loadProviders()\n file.providers[provider] = {\n apiKey,\n ...(baseUrl ? { baseUrl } : {}),\n updatedAt: new Date().toISOString(),\n }\n await writeProviders(file)\n return providerEnvVar(provider)\n}\n\n/** Remove a provider's key. Returns true if it existed. */\nexport async function removeProviderKey(provider: string): Promise<boolean> {\n const file = await loadProviders()\n if (!(provider in file.providers)) return false\n delete file.providers[provider]\n await writeProviders(file)\n return true\n}\n\n/**\n * Inject stored provider keys into a target env (default `process.env`).\n * **Explicit env always wins** — a var already set is never overwritten, so a\n * one-off `FOO_API_KEY=… serve` or a CI secret takes precedence over the\n * store. One provider key is written to its canonical env name AND every\n * verified alias ({@link providerEnvAliases}) — so a consumer that reads a\n * different env name for the same provider (e.g. mastracode reads\n * `GOOGLE_API_KEY`, not our canonical `GOOGLE_GENERATIVE_AI_API_KEY`) is\n * satisfied too. Returns the list of provider names actually injected (for a\n * boot log that names providers, never values) — one entry per provider, not\n * per env name.\n *\n * Providers are visited in SORTED name order, never `providers.json` key\n * order. Two providers can legitimately map to the same env name, and when\n * their keys are DIFFERENT secrets only one of them can win: `opencode`\n * (OpenCode Zen) and `opencode-go` both read `OPENCODE_API_KEY`, so whichever\n * is injected first takes the var and the other's \"explicit env wins\" check\n * sees it already set. Iterating the file's own key order made that outcome\n * depend on which `auth provider set` the operator happened to run first —\n * two hosts with the same two keys could inject different secrets. Sorting\n * makes the winner a stable, documentable fact (`opencode` < `opencode-go`,\n * so Zen wins). It does NOT make the collision harmless: a host that holds\n * both keys can still only inject one, and the per-spawn auth profile\n * (`accessProfile`, whose endpoint is `opencode` or `opencode-go`) is the\n * real disambiguator — see the note on `PROVIDER_KEY_ENV` in\n * `@agentproto/model-catalog`. The same-secret pairs this also covers\n * (`openai`/`openai-realtime`, `google`/`gemini-live`) were never affected,\n * because either order injects the same value.\n */\nexport async function injectProviderKeysIntoEnv(\n env: NodeJS.ProcessEnv = process.env,\n): Promise<string[]> {\n const file = await loadProviders()\n const injected: string[] = []\n for (const provider of Object.keys(file.providers).sort()) {\n const entry = file.providers[provider]\n if (!entry?.apiKey) continue\n // Canonical name first, then any verified aliases the same key satisfies.\n // De-dupe defends against an alias that equals the canonical name.\n const names = new Set([providerEnvVar(provider), ...providerEnvAliases(provider)])\n let didInject = false\n for (const name of names) {\n if (env[name]) continue // explicit env wins — evaluated per env name\n env[name] = entry.apiKey\n didInject = true\n }\n if (!didInject) continue // every name for this provider is already set\n if (entry.baseUrl) env[`${provider.toUpperCase().replace(/[^A-Z0-9]+/g, \"_\")}_BASE_URL`] = entry.baseUrl\n injected.push(provider)\n }\n return injected\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/providers-store",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "@agentproto/providers-store — the `~/.agentproto/providers.json` LLM provider API-key store (mode 0600) and the `injectProviderKeysIntoEnv` helper that lets any process (daemon or standalone gateway) boot with keys set via `agentproto auth provider set`, without ever logging a value. Explicit env always wins.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@agentproto/model-catalog": "0.9.4"
44
+ "@agentproto/model-catalog": "0.10.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^25.9.5",