mr-magic-mcp-server 0.2.6 → 0.3.1

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.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Zero-dependency KV store abstraction.
3
+ *
4
+ * Auto-detects the backend from environment variables. Both backends call
5
+ * their respective REST APIs using the built-in `fetch()` — no extra packages
6
+ * required.
7
+ *
8
+ * Supported backends (in priority order — first configured wins):
9
+ *
10
+ * 1. Upstash Redis (recommended for ephemeral/serverless deployments)
11
+ * UPSTASH_REDIS_REST_URL — e.g. https://xxxxx.upstash.io
12
+ * UPSTASH_REDIS_REST_TOKEN — Upstash REST bearer token
13
+ * → Also used by the export backend; set once, used everywhere.
14
+ * → Takes precedence over Cloudflare KV if both are configured.
15
+ *
16
+ * 2. Cloudflare KV
17
+ * CF_API_TOKEN — Cloudflare API token with KV:Edit permission
18
+ * CF_ACCOUNT_ID — Cloudflare account ID
19
+ * CF_KV_NAMESPACE_ID — KV namespace ID
20
+ *
21
+ * If neither backend is configured, all operations are silent no-ops.
22
+ * If both are configured, Upstash Redis is used and Cloudflare KV is ignored.
23
+ */
24
+
25
+ // ─── Backend detection ────────────────────────────────────────────────────────
26
+
27
+ function getUpstashConfig() {
28
+ const url = (process.env.UPSTASH_REDIS_REST_URL || '').replace(/\/$/, '');
29
+ const token = process.env.UPSTASH_REDIS_REST_TOKEN;
30
+ if (!url || !token) return null;
31
+ return { url, token };
32
+ }
33
+
34
+ function getCfConfig() {
35
+ const apiToken = process.env.CF_API_TOKEN;
36
+ const accountId = process.env.CF_ACCOUNT_ID;
37
+ const namespaceId = process.env.CF_KV_NAMESPACE_ID;
38
+ if (!apiToken || !accountId || !namespaceId) return null;
39
+ return { apiToken, accountId, namespaceId };
40
+ }
41
+
42
+ export function isKvConfigured() {
43
+ return Boolean(getUpstashConfig() || getCfConfig());
44
+ }
45
+
46
+ /** Returns a human-readable label for the active KV backend. */
47
+ export function describeKvBackend() {
48
+ if (getUpstashConfig()) return 'upstash-redis';
49
+ if (getCfConfig()) return 'cloudflare-kv';
50
+ return 'none';
51
+ }
52
+
53
+ // ─── Public API ───────────────────────────────────────────────────────────────
54
+
55
+ /**
56
+ * Get a value from the KV store.
57
+ * Returns the stored string, or `null` if the key is missing, not configured,
58
+ * or an error occured.
59
+ * @param {string} key
60
+ * @returns {Promise<string|null>}
61
+ */
62
+ export async function kvGet(key) {
63
+ const upstash = getUpstashConfig();
64
+ if (upstash) return upstashGet(upstash, key);
65
+
66
+ const cf = getCfConfig();
67
+ if (cf) return cfGet(cf, key);
68
+
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Store a string value in the KV store. No-op if no backend is configured.
74
+ * @param {string} key
75
+ * @param {string} value
76
+ * @param {number} [ttlSeconds] — optional TTL; defaults to no expiry if omitted
77
+ * @returns {Promise<void>}
78
+ */
79
+ export async function kvSet(key, value, ttlSeconds) {
80
+ const upstash = getUpstashConfig();
81
+ if (upstash) return upstashSet(upstash, key, value, ttlSeconds);
82
+
83
+ const cf = getCfConfig();
84
+ if (cf) return cfSet(cf, key, value, ttlSeconds);
85
+ }
86
+
87
+ // ─── Upstash Redis backend ────────────────────────────────────────────────────
88
+ // Uses the Upstash Redis REST API (POST / with a Redis command array).
89
+
90
+ async function upstashCmd({ url, token }, command) {
91
+ const res = await fetch(url, {
92
+ method: 'POST',
93
+ headers: {
94
+ Authorization: `Bearer ${token}`,
95
+ 'Content-Type': 'application/json'
96
+ },
97
+ body: JSON.stringify(command)
98
+ });
99
+ if (!res.ok) {
100
+ const text = await res.text().catch(() => '');
101
+ throw new Error(`Upstash Redis ${command[0]} failed (${res.status}): ${text}`);
102
+ }
103
+ return res.json();
104
+ }
105
+
106
+ async function upstashGet(cfg, key) {
107
+ try {
108
+ const json = await upstashCmd(cfg, ['GET', key]);
109
+ return json?.result ?? null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ async function upstashSet(cfg, key, value, ttlSeconds) {
116
+ const cmd = ttlSeconds
117
+ ? ['SET', key, value, 'EX', String(Math.round(ttlSeconds))]
118
+ : ['SET', key, value];
119
+ await upstashCmd(cfg, cmd);
120
+ }
121
+
122
+ // ─── Cloudflare KV backend ────────────────────────────────────────────────────
123
+ // Uses the Cloudflare Workers KV REST API.
124
+
125
+ function cfValuesUrl({ accountId, namespaceId }, key) {
126
+ return `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${encodeURIComponent(key)}`;
127
+ }
128
+
129
+ async function cfGet(cfg, key) {
130
+ try {
131
+ const res = await fetch(cfValuesUrl(cfg, key), {
132
+ headers: { Authorization: `Bearer ${cfg.apiToken}` }
133
+ });
134
+ if (res.status === 404) return null;
135
+ if (!res.ok) return null;
136
+ return res.text();
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ async function cfSet(cfg, key, value, ttlSeconds) {
143
+ const url =
144
+ cfValuesUrl(cfg, key) + (ttlSeconds ? `?expiration_ttl=${Math.round(ttlSeconds)}` : '');
145
+ const res = await fetch(url, {
146
+ method: 'PUT',
147
+ headers: {
148
+ Authorization: `Bearer ${cfg.apiToken}`,
149
+ 'Content-Type': 'text/plain'
150
+ },
151
+ body: value
152
+ });
153
+ if (!res.ok) {
154
+ const text = await res.text().catch(() => '');
155
+ throw new Error(`Cloudflare KV PUT failed (${res.status}): ${text}`);
156
+ }
157
+ }
@@ -5,23 +5,32 @@ import axios from 'axios';
5
5
 
6
6
  import { getEnvValue, getProjectRoot } from '../config.js';
7
7
  import { createLogger } from '../logger.js';
8
+ import { describeKvBackend, isKvConfigured, kvGet, kvSet } from '../kv-store.js';
8
9
 
9
10
  const GENIUS_TOKEN_ENDPOINT = 'https://api.genius.com/oauth/token';
10
11
  const logger = createLogger('genius-token-manager');
11
12
 
12
13
  // Token source terminology used throughout this module:
13
- // • Auto-refresh — GENIUS_CLIENT_ID + GENIUS_CLIENT_SECRET env vars.
14
- // The server calls the Genius OAuth client_credentials endpoint
15
- // at runtime and keeps the token refreshed in memory automatically.
16
- // This is the recommended approach for all deployments: no disk,
17
- // no scripts, and no manual token copying needed.
18
- // Fallback token GENIUS_ACCESS_TOKEN env var. A static bearer token set directly
19
- // in the environment. Does not auto-refresh; redeploy when expired.
20
- // Use this only when client_credentials are unavailable.
21
- // Cache token — on-disk .cache/genius-token.json written by the fetch script.
22
- // Only reliable when a persistent, writable filesystem is available
23
- // (i.e. local development). Ephemeral hosts (Render free tier, etc.)
24
- // should use the auto-refresh or fallback token paths instead.
14
+ // • Auto-refresh — GENIUS_CLIENT_ID + GENIUS_CLIENT_SECRET env vars.
15
+ // The server calls the Genius OAuth client_credentials endpoint
16
+ // at runtime and keeps the token refreshed in memory automatically.
17
+ // This is the recommended approach for all deployments: no disk,
18
+ // no scripts, and no manual token copying needed.
19
+ // On success the token is also persisted to KV + disk cache.
20
+ // Direct token — GENIUS_DIRECT_TOKEN env var. A static bearer token set directly
21
+ // in the environment. Does not auto-refresh; redeploy when expired.
22
+ // Use this only when client_credentials are unavailable.
23
+ // KV token — stored in a remote KV store (Upstash Redis or Cloudflare KV).
24
+ // Written automatically when client_credentials refresh succeeds.
25
+ // Ideal for ephemeral/serverless deployments and npx installs.
26
+ // • Cache token — on-disk .cache/genius-token.json written by the fetch script or
27
+ // by a successful client_credentials refresh. Only reliable when
28
+ // a persistent, writable filesystem is available (local dev).
29
+ // Ephemeral hosts should use auto-refresh, direct token, or KV.
30
+
31
+ // KV key and TTL — configurable via env vars.
32
+ const KV_KEY = process.env.GENIUS_TOKEN_KV_KEY || 'mr-magic:genius-token';
33
+ const KV_TTL_SECONDS = parseInt(process.env.GENIUS_TOKEN_KV_TTL_SECONDS || '3600', 10); // 1 hour
25
34
 
26
35
  // Token cache path — must match the path used by src/scripts/fetch_genius_token.mjs.
27
36
  const TOKEN_CACHE_PATH =
@@ -32,7 +41,7 @@ let cachedExpiry = 0;
32
41
  let lastAuthMode = 'unknown';
33
42
 
34
43
  function getFallbackToken() {
35
- return getEnvValue('GENIUS_ACCESS_TOKEN');
44
+ return getEnvValue('GENIUS_DIRECT_TOKEN');
36
45
  }
37
46
 
38
47
  function tokenExpired() {
@@ -56,6 +65,59 @@ async function readCachedToken() {
56
65
  }
57
66
  }
58
67
 
68
+ async function writeCachedToken(accessToken, expiresIn) {
69
+ if (!accessToken) return;
70
+ try {
71
+ await fs.mkdir(path.dirname(TOKEN_CACHE_PATH), { recursive: true });
72
+ await fs.writeFile(
73
+ TOKEN_CACHE_PATH,
74
+ JSON.stringify({
75
+ access_token: accessToken,
76
+ expires_at: Date.now() + (Number(expiresIn) || 3600) * 1000
77
+ }),
78
+ 'utf8'
79
+ );
80
+ } catch (error) {
81
+ logger.warn('Failed to persist Genius token cache', { error: error?.message });
82
+ }
83
+ }
84
+
85
+ // ─── KV store helpers ─────────────────────────────────────────────────────────
86
+
87
+ async function readKvToken() {
88
+ if (!isKvConfigured()) return null;
89
+ try {
90
+ const raw = await kvGet(KV_KEY);
91
+ if (!raw) return null;
92
+ const parsed = JSON.parse(raw);
93
+ if (parsed?.access_token) {
94
+ cachedToken = parsed.access_token;
95
+ cachedExpiry = parsed.expires_at ?? Date.now() + 3_600_000;
96
+ lastAuthMode = `kv:${describeKvBackend()}`;
97
+ return cachedToken;
98
+ }
99
+ } catch {
100
+ // KV read error — not fatal; fall through to disk cache
101
+ }
102
+ return null;
103
+ }
104
+
105
+ async function writeKvToken(accessToken, expiresIn) {
106
+ if (!isKvConfigured() || !accessToken) return;
107
+ try {
108
+ const payload = JSON.stringify({
109
+ access_token: accessToken,
110
+ expires_at: Date.now() + (Number(expiresIn) || 3600) * 1000
111
+ });
112
+ await kvSet(KV_KEY, payload, KV_TTL_SECONDS);
113
+ } catch (error) {
114
+ logger.warn('Failed to persist Genius token to KV store', {
115
+ backend: describeKvBackend(),
116
+ error: error?.message
117
+ });
118
+ }
119
+ }
120
+
59
121
  async function fetchClientCredentialsToken() {
60
122
  const clientId = getEnvValue('GENIUS_CLIENT_ID');
61
123
  const clientSecret = getEnvValue('GENIUS_CLIENT_SECRET');
@@ -82,6 +144,8 @@ async function fetchClientCredentialsToken() {
82
144
  cachedExpiry = Date.now() + ttl * 1000;
83
145
  lastAuthMode = 'client_credentials';
84
146
  logger.info('Genius token refreshed', { ttlSeconds: ttl });
147
+ // Persist to durable backends in parallel so ephemeral hosts survive restarts.
148
+ await Promise.allSettled([writeCachedToken(accessToken, ttl), writeKvToken(accessToken, ttl)]);
85
149
  return cachedToken;
86
150
  } catch (error) {
87
151
  logger.error('Failed to refresh Genius token', {
@@ -95,8 +159,10 @@ async function fetchClientCredentialsToken() {
95
159
  * Resolve the Genius token using the following priority order:
96
160
  * 1. In-memory runtime cache (already resolved this session)
97
161
  * 2. Auto-refresh via GENIUS_CLIENT_ID + GENIUS_CLIENT_SECRET (client_credentials)
98
- * 3. GENIUS_ACCESS_TOKEN env var fallback token
99
- * 4. On-disk .cache/genius-token.jsoncache token, local dev only
162
+ * on success, writes to KV store + disk cache
163
+ * 3. GENIUS_DIRECT_TOKEN env var static bearer token override
164
+ * 4. KV store — Upstash Redis or Cloudflare KV (ephemeral/npx)
165
+ * 5. On-disk .cache/genius-token.json — cache token, local dev only
100
166
  */
101
167
  export async function getGeniusToken({ forceRefresh = false } = {}) {
102
168
  if (!forceRefresh && !tokenExpired()) {
@@ -109,17 +175,21 @@ export async function getGeniusToken({ forceRefresh = false } = {}) {
109
175
  return token;
110
176
  }
111
177
 
112
- // 3. Fallback token from env var (static, no auto-refresh)
178
+ // 3. Direct token from env var (static override, no auto-refresh)
113
179
  const fallback = getFallbackToken();
114
180
  if (fallback && fallback !== cachedToken) {
115
- logger.warn('Using Genius fallback token from GENIUS_ACCESS_TOKEN env var');
181
+ logger.warn('Using Genius direct token from GENIUS_DIRECT_TOKEN env var');
116
182
  cachedToken = fallback;
117
183
  cachedExpiry = Date.now() + 86_400_000; // 1 day placeholder
118
- lastAuthMode = 'env_access_token';
184
+ lastAuthMode = 'env_direct_token';
119
185
  return cachedToken;
120
186
  }
121
187
 
122
- // 4. Cache token from disk (local dev convenience only)
188
+ // 4. KV store ideal for ephemeral deployments and npx installs
189
+ const kvToken = await readKvToken();
190
+ if (kvToken) return kvToken;
191
+
192
+ // 5. Cache token from disk (local dev convenience only)
123
193
  const cached = await readCachedToken();
124
194
  if (cached) {
125
195
  logger.info('Using Genius cache token from disk', { cachePath: TOKEN_CACHE_PATH });
@@ -156,7 +226,10 @@ export function describeGeniusAuthMode() {
156
226
  return 'client_credentials';
157
227
  }
158
228
  if (getFallbackToken()) {
159
- return 'env_access_token';
229
+ return 'env_direct_token';
230
+ }
231
+ if (isKvConfigured()) {
232
+ return 'kv_store';
160
233
  }
161
234
  return 'none';
162
235
  }
@@ -164,12 +237,14 @@ export function describeGeniusAuthMode() {
164
237
  export async function getGeniusDiagnostics() {
165
238
  const clientId = getEnvValue('GENIUS_CLIENT_ID');
166
239
  const clientSecret = getEnvValue('GENIUS_CLIENT_SECRET');
167
- const fallback = getFallbackToken();
240
+ const directToken = getFallbackToken();
168
241
  const ttlMs = Math.max(cachedExpiry - Date.now(), 0);
169
242
 
170
243
  const diagnostics = {
171
244
  clientCredentialsPresent: Boolean(clientId && clientSecret),
172
- fallbackTokenPresent: Boolean(fallback),
245
+ directTokenPresent: Boolean(directToken),
246
+ kvConfigured: isKvConfigured(),
247
+ kvBackend: describeKvBackend(),
173
248
  runtimeTokenCached: Boolean(cachedToken),
174
249
  runtimeTokenExpiresInMs: cachedToken ? ttlMs : 0,
175
250
  lastAuthMode: describeGeniusAuthMode(),
@@ -3,20 +3,28 @@ import path from 'node:path';
3
3
 
4
4
  import { getEnvValue, getProjectRoot } from '../config.js';
5
5
  import { createLogger } from '../logger.js';
6
+ import { describeKvBackend, isKvConfigured, kvGet, kvSet } from '../kv-store.js';
6
7
 
7
8
  const logger = createLogger('musixmatch-token-manager');
8
9
 
9
10
  // Token source terminology used throughout this module:
10
- // • Cache token — loaded from the on-disk cache file written by the fetch script.
11
- // Only reliable when a persistent, writable filesystem is available
12
- // (i.e. local development). Ephemeral hosts (Render free tier, etc.)
13
- // may not have a writable FS, so the cache token is unavailable there.
14
- // • Fallback token the token value supplied directly via MUSIXMATCH_FALLBACK_TOKEN or
15
- // MUSIXMATCH_ALT_USER_TOKEN environment variables. This is the recommended
16
- // approach for production and remote deployments where the filesystem
17
- // cannot be relied upon for persistence.
11
+ // • Direct token — MUSIXMATCH_DIRECT_TOKEN env var. A static bearer token set
12
+ // directly in the environment. Recommended for production and
13
+ // remote deployments where the filesystem cannot be relied upon
14
+ // for persistence. Highest priority after in-memory cache.
15
+ // • KV token stored in a remote KV store (Upstash Redis or Cloudflare KV).
16
+ // Ideal for ephemeral/serverless deployments and npx installs
17
+ // where there is no local filesystem at all.
18
+ // Cache token — loaded from the on-disk cache file written by the fetch script.
19
+ // Only reliable when a persistent, writable filesystem is available
20
+ // (i.e. local development). Ephemeral hosts (Render free tier, etc.)
21
+ // may not have a writable FS, so the cache token is unavailable there.
22
+
23
+ // KV key and TTL — configurable via env vars.
24
+ const KV_KEY = process.env.MUSIXMATCH_TOKEN_KV_KEY || 'mr-magic:musixmatch-token';
25
+ const KV_TTL_SECONDS = parseInt(process.env.MUSIXMATCH_TOKEN_KV_TTL_SECONDS || '2592000', 10); // 30 days
18
26
  const TOKEN_CACHE_PATH =
19
- process.env.MUSIXMATCH_ALT_USER_TOKEN_CACHE ||
27
+ process.env.MUSIXMATCH_TOKEN_CACHE ||
20
28
  path.join(getProjectRoot(), '.cache', 'musixmatch-token.json');
21
29
 
22
30
  let cachedToken = null;
@@ -60,7 +68,7 @@ async function writeCachedToken(token, desktopCookie) {
60
68
  if (!dirOk) {
61
69
  logger.warn(
62
70
  'Musixmatch token cache directory unavailable (read-only or restricted filesystem). ' +
63
- 'Token was NOT persisted to disk. Set MUSIXMATCH_FALLBACK_TOKEN as an environment variable ' +
71
+ 'Token was NOT persisted to disk. Set MUSIXMATCH_DIRECT_TOKEN as an environment variable ' +
64
72
  'to ensure the token survives restarts in remote/ephemeral deployments.',
65
73
  { cachePath: TOKEN_CACHE_PATH }
66
74
  );
@@ -75,36 +83,65 @@ async function writeCachedToken(token, desktopCookie) {
75
83
  }
76
84
  }
77
85
 
86
+ // ─── KV store helpers ─────────────────────────────────────────────────────────
87
+
88
+ async function readKvToken() {
89
+ if (!isKvConfigured()) return null;
90
+ try {
91
+ const raw = await kvGet(KV_KEY);
92
+ if (!raw) return null;
93
+ const parsed = JSON.parse(raw);
94
+ if (parsed?.token) {
95
+ cachedToken = parsed.token;
96
+ cachedDesktopCookie = parsed.desktopCookie || null;
97
+ lastLoadedFrom = `kv:${describeKvBackend()}`;
98
+ return cachedToken;
99
+ }
100
+ } catch {
101
+ // KV read error — not fatal; fall through to disk cache
102
+ }
103
+ return null;
104
+ }
105
+
106
+ async function writeKvToken(token, desktopCookie) {
107
+ if (!isKvConfigured() || !token) return;
108
+ try {
109
+ const payload = JSON.stringify({ token, ...(desktopCookie ? { desktopCookie } : {}) });
110
+ await kvSet(KV_KEY, payload, KV_TTL_SECONDS);
111
+ } catch (error) {
112
+ logger.warn('Failed to persist Musixmatch token to KV store', {
113
+ backend: describeKvBackend(),
114
+ error: error?.message
115
+ });
116
+ }
117
+ }
118
+
78
119
  /**
79
120
  * Resolve the Musixmatch token using the following priority order:
80
121
  * 1. In-memory runtime cache (already resolved this session)
81
- * 2. MUSIXMATCH_FALLBACK_TOKEN env var fallback token, first-priority env source
82
- * 3. MUSIXMATCH_ALT_USER_TOKEN env var fallback token, second-priority env source
83
- * 4. On-disk cache file cache token, local dev only
122
+ * 2. MUSIXMATCH_DIRECT_TOKEN env var direct/static bearer token (highest env priority)
123
+ * 3. KV store Upstash Redis or Cloudflare KV (ephemeral/npx)
124
+ * 4. On-disk cache file — local dev / persistent server only
84
125
  */
85
126
  export async function getMusixmatchToken() {
86
127
  if (cachedToken) {
87
128
  return cachedToken;
88
129
  }
89
130
 
90
- // Prioritize env varsthese survive restarts on ephemeral hosts
91
- const userToken = getEnvValue('MUSIXMATCH_FALLBACK_TOKEN');
92
- if (userToken) {
93
- cachedToken = userToken;
94
- lastLoadedFrom = 'env:MUSIXMATCH_FALLBACK_TOKEN';
131
+ // 2. Direct token from env varsurvives restarts on ephemeral hosts without any external service
132
+ const directToken = getEnvValue('MUSIXMATCH_DIRECT_TOKEN');
133
+ if (directToken) {
134
+ cachedToken = directToken;
135
+ lastLoadedFrom = 'env:MUSIXMATCH_DIRECT_TOKEN';
95
136
  cachedDesktopCookie = null;
96
137
  return cachedToken;
97
138
  }
98
139
 
99
- const envToken = getEnvValue('MUSIXMATCH_ALT_USER_TOKEN');
100
- if (envToken) {
101
- cachedToken = envToken;
102
- lastLoadedFrom = 'env:MUSIXMATCH_ALT_USER_TOKEN';
103
- cachedDesktopCookie = null;
104
- return cachedToken;
105
- }
140
+ // 3. KV store — ideal for ephemeral deployments and npx installs with no local filesystem
141
+ const kvToken = await readKvToken();
142
+ if (kvToken) return kvToken;
106
143
 
107
- // Fall back to disk cache for local development
144
+ // 4. On-disk cache local dev / persistent hosts with a writable filesystem
108
145
  return readCachedToken();
109
146
  }
110
147
 
@@ -113,10 +150,16 @@ export async function setMusixmatchToken(token, { desktopCookie } = {}) {
113
150
  cachedToken = token;
114
151
  lastLoadedFrom = 'runtime';
115
152
  cachedDesktopCookie = desktopCookie || null;
116
- await writeCachedToken(token, desktopCookie);
153
+ // Write to both storage backends in parallel; failures are logged, not thrown.
154
+ await Promise.allSettled([
155
+ writeCachedToken(token, desktopCookie),
156
+ writeKvToken(token, desktopCookie)
157
+ ]);
117
158
  logger.info('Musixmatch token updated', {
118
159
  source: 'runtime',
119
- desktopCookiePresent: Boolean(desktopCookie)
160
+ desktopCookiePresent: Boolean(desktopCookie),
161
+ kvConfigured: isKvConfigured(),
162
+ kvBackend: describeKvBackend()
120
163
  });
121
164
  }
122
165
 
@@ -129,8 +172,7 @@ export function describeMusixmatchTokenSource() {
129
172
  }
130
173
 
131
174
  export async function getMusixmatchTokenDiagnostics() {
132
- const userEnvToken = getEnvValue('MUSIXMATCH_FALLBACK_TOKEN');
133
- const envToken = getEnvValue('MUSIXMATCH_ALT_USER_TOKEN');
175
+ const directEnvToken = getEnvValue('MUSIXMATCH_DIRECT_TOKEN');
134
176
 
135
177
  const diagnostics = {
136
178
  cachePath: TOKEN_CACHE_PATH,
@@ -140,8 +182,9 @@ export async function getMusixmatchTokenDiagnostics() {
140
182
  cacheBytes: 0,
141
183
  cacheTokenPresent: false,
142
184
  cacheError: null,
143
- userEnvPresent: Boolean(userEnvToken),
144
- envPresent: Boolean(envToken),
185
+ directEnvPresent: Boolean(directEnvToken),
186
+ kvConfigured: isKvConfigured(),
187
+ kvBackend: describeKvBackend(),
145
188
  runtimeTokenCached: Boolean(cachedToken),
146
189
  lastLoadedFrom,
147
190
  resolvedSource: 'none'
@@ -160,10 +203,8 @@ export async function getMusixmatchTokenDiagnostics() {
160
203
 
161
204
  if (cachedToken) {
162
205
  diagnostics.resolvedSource = lastLoadedFrom;
163
- } else if (userEnvToken) {
164
- diagnostics.resolvedSource = 'env:MUSIXMATCH_FALLBACK_TOKEN';
165
- } else if (envToken) {
166
- diagnostics.resolvedSource = 'env:MUSIXMATCH_ALT_USER_TOKEN';
206
+ } else if (directEnvToken) {
207
+ diagnostics.resolvedSource = 'env:MUSIXMATCH_DIRECT_TOKEN';
167
208
  } else if (diagnostics.cacheTokenPresent) {
168
209
  diagnostics.resolvedSource = 'cache';
169
210
  } else {