@newhomestar/sdk 0.7.23 → 0.7.24

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.
@@ -37,26 +37,26 @@ export declare class TokenExchangeError extends Error {
37
37
  */
38
38
  export declare function createPlatformClient(): SupabaseClient;
39
39
  /**
40
- * Evict a cached token for the given integration slug and optional userId.
41
- * Call this when an API returns 401 so the next resolveCredentials() call
42
- * fetches a fresh token instead of re-using the revoked one.
40
+ * No-op token cache has been removed.
41
+ * Retained for backward compatibility with integration code that calls it on 401.
43
42
  */
44
43
  export declare function clearTokenCache(slug: string, userId?: string): void;
45
44
  /**
46
45
  * Resolve credentials for an integration using the HTTP callback strategy.
47
46
  *
48
- * Resolution order:
49
- * 1. In-memory cache (hot path no I/O)
50
- * 2. Client-side refresh_token grant (if refresh metadata is cached)
51
- * 3. Fetch decrypted credentials from the auth server via HTTP
52
- * 4. OAuth token exchange (client_credentials / mTLS)
53
- * 5. Cache result + refresh metadata in memory
47
+ * Token caching has been intentionally removed — every call fetches fresh
48
+ * credentials from the auth server to prevent stale/revoked tokens from
49
+ * being reused. The auth server and Vault maintain their own caching layer.
50
+ *
51
+ * On 401 from the integration API, call again with `{ forceRefresh: true }`
52
+ * to send X-Nova-Token-Invalid: true and force the auth server to discard
53
+ * any token it has stored for this integration.
54
54
  *
55
55
  * @param authBaseUrl - Auth server base URL (AUTH_ISSUER_BASE_URL)
56
- * @param slug - Integration slug (e.g., "jira", "adp")
57
- * @param bearerToken - JWT from the inbound request (to authenticate with auth server)
58
- * @param userId - Optional user ID for cache key differentiation (standard auth flows)
59
- * @param options - forceRefresh: true → skip cache + signal auth server to refresh
56
+ * @param slug - Integration slug (e.g., "jira", "bamboohr")
57
+ * @param bearerToken - JWT from the inbound request (forwarded to auth server)
58
+ * @param userId - Optional user ID (for standard/user-level OAuth flows)
59
+ * @param options - forceRefresh: true → send X-Nova-Token-Invalid header
60
60
  */
61
61
  export declare function resolveCredentialsViaHttp(authBaseUrl: string, slug: string, bearerToken: string, userId?: string, options?: {
62
62
  forceRefresh?: boolean;
@@ -7,14 +7,16 @@
7
7
  // - 'standard' → authorization_code (per-user OAuth redirect)
8
8
  //
9
9
  // Credential resolution strategy:
10
- // HTTP callback (AUTH_ISSUER_BASE_URL) — container calls auth server with JWT
10
+ // HTTP callback (AUTH_ISSUER_BASE_URL) — container calls auth server with JWT.
11
11
  // The container receives a JWT from the inbound request, forwards it to the
12
12
  // auth server, which decrypts credentials from Vault and returns them.
13
13
  //
14
- // Tokens are cached in-memory with client-side refresh_token support:
15
- // 1. In-memory Map (hot path no I/O)
16
- // 2. Client-side refresh_token grant (if refresh metadata is cached)
17
- // 3. Fresh credentials fetch from auth server + token exchange (cold path)
14
+ // NOTE: In-memory token caching has been intentionally removed.
15
+ // Every call to resolveCredentialsViaHttp() fetches fresh credentials from
16
+ // the auth server to prevent stale/revoked tokens from being reused.
17
+ // The auth server and Vault maintain their own caching layer.
18
+ // On a 401 from the integration API, ctx.fetch() automatically retries once
19
+ // with forceRefresh=true to bust the auth server's cached token too.
18
20
  //
19
21
  // Every integration gets this for free via ctx.resolveCredentials() / ctx.fetch().
20
22
  import { createClient } from "@supabase/supabase-js";
@@ -74,37 +76,23 @@ export function createPlatformClient() {
74
76
  }
75
77
  return _platformClient;
76
78
  }
77
- const _tokenCache = new Map();
78
- /** Token buffer — refresh tokens 5 minutes before actual expiry */
79
- const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
79
+ // ─── Token Cache (DISABLED) ─────────────────────────────────────────────────
80
+ //
81
+ // The in-memory token cache has been removed to prevent stale/revoked tokens
82
+ // from being re-used after a provider invalidates them server-side.
83
+ // Every call to resolveCredentialsViaHttp() now fetches fresh credentials from
84
+ // the auth server. The auth server (and Vault) provide their own caching layer.
85
+ //
86
+ // clearTokenCache() is retained as a no-op for backward compatibility with any
87
+ // integration code that calls it explicitly.
80
88
  /** System user UUID for client_credentials/mTLS connections (audit trail) */
81
89
  const SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
82
- function getCacheKey(integrationSlug, userId) {
83
- return `${integrationSlug}:${userId ?? SYSTEM_USER_ID}`;
84
- }
85
90
  /**
86
- * Evict a cached token for the given integration slug and optional userId.
87
- * Call this when an API returns 401 so the next resolveCredentials() call
88
- * fetches a fresh token instead of re-using the revoked one.
91
+ * No-op token cache has been removed.
92
+ * Retained for backward compatibility with integration code that calls it on 401.
89
93
  */
90
94
  export function clearTokenCache(slug, userId) {
91
- const key = getCacheKey(slug, userId);
92
- const deleted = _tokenCache.delete(key);
93
- console.log(`[nova-sdk] 🗑️ Cleared in-memory token cache for "${slug}" (key=${key}, existed=${deleted})`);
94
- }
95
- function getCachedToken(key) {
96
- const cached = _tokenCache.get(key);
97
- if (!cached) {
98
- console.log(`[nova-sdk] 💭 Cache miss for key="${key}"`);
99
- return null;
100
- }
101
- if (new Date().getTime() >= cached.expiresAt.getTime() - TOKEN_EXPIRY_BUFFER_MS) {
102
- console.log(`[nova-sdk] 🔄 In-memory cached token expired for key="${key}" (expired at ${cached.expiresAt.toISOString()})`);
103
- _tokenCache.delete(key);
104
- return null;
105
- }
106
- console.log(`[nova-sdk] ⚡ Cache hit for key="${key}" (expires ${cached.expiresAt.toISOString()})`);
107
- return cached;
95
+ console.log(`[nova-sdk] 🗑️ clearTokenCache("${slug}") called — cache is disabled, no-op`);
108
96
  }
109
97
  // ─── mTLS Agent Cache ───────────────────────────────────────────────────────
110
98
  // Cache mTLS agents per integration slug (cert/key don't change during container lifetime)
@@ -189,60 +177,6 @@ async function performTokenExchange(slug, params) {
189
177
  }
190
178
  return res.json();
191
179
  }
192
- // ─── Client-Side Refresh Grant ──────────────────────────────────────────────
193
- /**
194
- * Attempt a client-side refresh_token grant using cached refresh metadata.
195
- * Returns null if refresh metadata is not available (caller should fall through
196
- * to fetching fresh credentials from the auth server).
197
- *
198
- * This avoids a round-trip to the auth server when we already have a valid
199
- * refresh token — reducing latency and auth server load.
200
- */
201
- async function performRefreshGrant(slug, cacheKey, cachedEntry) {
202
- const { refreshToken, tokenEndpoint, clientId, clientSecret, httpsAgent, authMode } = cachedEntry;
203
- if (!refreshToken || !tokenEndpoint || !clientId || !clientSecret) {
204
- console.log(`[nova-sdk] 💭 No refresh metadata cached for "${slug}" — skipping client-side refresh`);
205
- return null;
206
- }
207
- console.log(`[nova-sdk] 🔄 Attempting client-side refresh_token grant for "${slug}" (endpoint: ${tokenEndpoint})`);
208
- try {
209
- const tokenResponse = await performTokenExchange(slug, {
210
- tokenEndpoint,
211
- clientId,
212
- clientSecret,
213
- httpsAgent,
214
- grantType: "refresh_token",
215
- refreshToken,
216
- });
217
- const expiresAt = new Date(Date.now() + tokenResponse.expires_in * 1000);
218
- const effectiveAuthMode = authMode ?? "client_credentials";
219
- console.log(`[nova-sdk] ✅ Client-side refresh successful for "${slug}" — expires ${expiresAt.toISOString()} (${tokenResponse.expires_in}s)`);
220
- // Update cache with new access token (and new refresh token if rotated)
221
- _tokenCache.set(cacheKey, {
222
- accessToken: tokenResponse.access_token,
223
- expiresAt,
224
- httpsAgent,
225
- authMode: effectiveAuthMode,
226
- refreshToken: tokenResponse.refresh_token ?? refreshToken, // use new RT if provider rotated it
227
- tokenEndpoint,
228
- clientId,
229
- clientSecret,
230
- });
231
- return {
232
- accessToken: tokenResponse.access_token,
233
- expiresAt,
234
- integrationId: `http:${slug}`,
235
- authMode: effectiveAuthMode,
236
- httpsAgent,
237
- };
238
- }
239
- catch (err) {
240
- console.warn(`[nova-sdk] ⚠️ Client-side refresh failed for "${slug}": ${err.message} — will re-fetch from auth server`);
241
- // Clear stale cached entry so we fall through to auth server fetch
242
- _tokenCache.delete(cacheKey);
243
- return null;
244
- }
245
- }
246
180
  /**
247
181
  * Fetch integration credentials from the auth server over HTTP.
248
182
  * Requires a valid JWT Bearer token (the same one that authenticated the
@@ -285,105 +219,34 @@ async function fetchCredentialsFromAuthServer(authBaseUrl, slug, bearerToken, fo
285
219
  /**
286
220
  * Resolve credentials for an integration using the HTTP callback strategy.
287
221
  *
288
- * Resolution order:
289
- * 1. In-memory cache (hot path no I/O)
290
- * 2. Client-side refresh_token grant (if refresh metadata is cached)
291
- * 3. Fetch decrypted credentials from the auth server via HTTP
292
- * 4. OAuth token exchange (client_credentials / mTLS)
293
- * 5. Cache result + refresh metadata in memory
222
+ * Token caching has been intentionally removed — every call fetches fresh
223
+ * credentials from the auth server to prevent stale/revoked tokens from
224
+ * being reused. The auth server and Vault maintain their own caching layer.
225
+ *
226
+ * On 401 from the integration API, call again with `{ forceRefresh: true }`
227
+ * to send X-Nova-Token-Invalid: true and force the auth server to discard
228
+ * any token it has stored for this integration.
294
229
  *
295
230
  * @param authBaseUrl - Auth server base URL (AUTH_ISSUER_BASE_URL)
296
- * @param slug - Integration slug (e.g., "jira", "adp")
297
- * @param bearerToken - JWT from the inbound request (to authenticate with auth server)
298
- * @param userId - Optional user ID for cache key differentiation (standard auth flows)
299
- * @param options - forceRefresh: true → skip cache + signal auth server to refresh
231
+ * @param slug - Integration slug (e.g., "jira", "bamboohr")
232
+ * @param bearerToken - JWT from the inbound request (forwarded to auth server)
233
+ * @param userId - Optional user ID (for standard/user-level OAuth flows)
234
+ * @param options - forceRefresh: true → send X-Nova-Token-Invalid header
300
235
  */
301
236
  export async function resolveCredentialsViaHttp(authBaseUrl, slug, bearerToken, userId, options) {
302
237
  const effectiveUserId = userId ?? SYSTEM_USER_ID;
303
- const cacheKey = getCacheKey(slug, effectiveUserId);
304
238
  const forceRefresh = options?.forceRefresh ?? false;
305
239
  console.log(`[nova-sdk] 🔑 resolveCredentialsViaHttp: slug="${slug}" userId="${effectiveUserId}" forceRefresh=${forceRefresh} authBaseUrl="${authBaseUrl}"`);
306
- // ── Step 1: Check in-memory cache (skip on forceRefresh) ─────────────
307
- if (!forceRefresh) {
308
- const cached = getCachedToken(cacheKey);
309
- if (cached) {
310
- const cachedAuthMode = cached.authMode ?? (cached.httpsAgent ? "mtls" : "client_credentials");
311
- console.log(`[nova-sdk] ⚡ Cache hit for "${slug}" — returning cached token (authMode: ${cachedAuthMode}, expires: ${cached.expiresAt.toISOString()})`);
312
- return {
313
- accessToken: cached.accessToken,
314
- expiresAt: cached.expiresAt,
315
- integrationId: `http:${slug}`,
316
- authMode: cachedAuthMode,
317
- httpsAgent: cached.httpsAgent,
318
- };
319
- }
320
- // ── Step 2: Try client-side refresh_token grant ───────────────────
321
- // The cached entry was evicted (expired), but it may have left refresh metadata
322
- // behind if we stored it before eviction. Check the map directly (bypassing
323
- // the expiry check) to get refresh metadata from the evicted entry.
324
- // Actually, since getCachedToken deletes the entry on expiry, we need to
325
- // try a refresh using data we stored before the eviction. If the map no
326
- // longer has the entry, we fall through to the auth server.
327
- //
328
- // Alternative: keep an expired entry temporarily for refresh metadata.
329
- // For simplicity, we store a "refresh-only" entry under a separate key.
330
- const refreshKey = `${cacheKey}:refresh`;
331
- const refreshEntry = _tokenCache.get(refreshKey);
332
- if (refreshEntry) {
333
- console.log(`[nova-sdk] 🔄 Found refresh metadata for "${slug}" — attempting client-side refresh`);
334
- const refreshed = await performRefreshGrant(slug, cacheKey, refreshEntry);
335
- if (refreshed) {
336
- // Store updated refresh metadata under the refresh key too
337
- const updatedEntry = _tokenCache.get(cacheKey);
338
- if (updatedEntry) {
339
- _tokenCache.set(refreshKey, {
340
- ...updatedEntry,
341
- // Extend expiry of refresh key far out (refresh tokens are long-lived)
342
- expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
343
- });
344
- }
345
- return refreshed;
346
- }
347
- // Refresh failed — fall through to auth server
348
- _tokenCache.delete(refreshKey);
349
- }
350
- }
351
- else {
352
- // Force refresh — clear cache and refresh entry
353
- _tokenCache.delete(cacheKey);
354
- _tokenCache.delete(`${cacheKey}:refresh`);
355
- console.log(`[nova-sdk] 🗑️ Force refresh: cleared cache for "${slug}" (key=${cacheKey})`);
356
- }
357
- // ── Step 3: Fetch credentials from auth server ────────────────────────
240
+ // ── Fetch credentials from auth server (always fresh — no local cache) ──
358
241
  console.log(`[nova-sdk] 📡 Fetching fresh credentials from auth server for "${slug}"`);
359
242
  const creds = await fetchCredentialsFromAuthServer(authBaseUrl, slug, bearerToken, forceRefresh);
360
- // ── Step 3b: Standard auth — use pre-resolved access token ───────────
243
+ // ── Standard auth — use pre-resolved access token from auth server ────
361
244
  if (creds.authMode === "standard") {
362
245
  if (creds.accessToken) {
363
246
  const expiresAt = creds.expiresAt
364
247
  ? new Date(creds.expiresAt)
365
248
  : new Date(Date.now() + 3600 * 1000); // default 1h if not provided
366
- console.log(`[nova-sdk] ✅ Standard auth: using pre-resolved access token for "${slug}" — expires ${expiresAt.toISOString()}`);
367
- // Cache access token
368
- const cacheEntry = {
369
- accessToken: creds.accessToken,
370
- expiresAt,
371
- authMode: "standard",
372
- // Cache refresh metadata if provided — enables client-side refresh next time
373
- refreshToken: creds.refreshToken ?? undefined,
374
- tokenEndpoint: creds.tokenEndpoint ?? undefined,
375
- clientId: creds.clientId ?? undefined,
376
- clientSecret: creds.clientSecret ?? undefined,
377
- };
378
- _tokenCache.set(cacheKey, cacheEntry);
379
- // Also store refresh metadata in the long-lived refresh key
380
- if (creds.refreshToken && creds.tokenEndpoint) {
381
- _tokenCache.set(`${cacheKey}:refresh`, {
382
- ...cacheEntry,
383
- expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
384
- });
385
- console.log(`[nova-sdk] 💾 Cached refresh metadata for "${slug}" (standard auth)`);
386
- }
249
+ console.log(`[nova-sdk] ✅ Standard auth: received access token for "${slug}" — expires ${expiresAt.toISOString()}`);
387
250
  return {
388
251
  accessToken: creds.accessToken,
389
252
  expiresAt,
@@ -391,11 +254,11 @@ export async function resolveCredentialsViaHttp(authBaseUrl, slug, bearerToken,
391
254
  authMode: "standard",
392
255
  };
393
256
  }
394
- // Standard auth but no access token returned — user hasn't authorized yet
257
+ // Standard auth but no access token — user hasn't authorized yet
395
258
  console.error(`[nova-sdk] ❌ Standard auth: no access token returned for "${slug}" — user must authorize via OAuth popup`);
396
259
  throw new ConnectionNotFoundError(slug, `${effectiveUserId} (no stored access token — user must authorize via OAuth popup first)`);
397
260
  }
398
- // ── Step 4: Validate required fields (client_credentials / mTLS only) ──
261
+ // ── Validate required fields (client_credentials / mTLS) ─────────────
399
262
  if (!creds.clientId) {
400
263
  throw new CredentialsNotConfiguredError(slug, "client_id is not set");
401
264
  }
@@ -406,7 +269,7 @@ export async function resolveCredentialsViaHttp(authBaseUrl, slug, bearerToken,
406
269
  throw new CredentialsNotConfiguredError(slug, "token_endpoint is not set");
407
270
  }
408
271
  console.log(`[nova-sdk] 🔧 Credentials received: authMode=${creds.authMode} clientId=${creds.clientId?.slice(0, 8)}... tokenEndpoint=${creds.tokenEndpoint}`);
409
- // ── Step 5: Build mTLS agent if needed ────────────────────────────────
272
+ // ── Build mTLS agent if needed ────────────────────────────────────────
410
273
  let httpsAgent;
411
274
  if (creds.authMode === "mtls") {
412
275
  if (!creds.mtlsCert || !creds.mtlsKey) {
@@ -414,7 +277,7 @@ export async function resolveCredentialsViaHttp(authBaseUrl, slug, bearerToken,
414
277
  }
415
278
  httpsAgent = buildMtlsAgentFromPem(slug, creds.mtlsCert, creds.mtlsKey);
416
279
  }
417
- // ── Step 6: Perform token exchange (client_credentials / mTLS) ────────
280
+ // ── Perform token exchange (client_credentials / mTLS) ───────────────
418
281
  console.log(`[nova-sdk] 🔄 Performing ${creds.authMode} token exchange at ${creds.tokenEndpoint} for "${slug}"`);
419
282
  const tokenResponse = await performTokenExchange(slug, {
420
283
  tokenEndpoint: creds.tokenEndpoint,
@@ -423,28 +286,7 @@ export async function resolveCredentialsViaHttp(authBaseUrl, slug, bearerToken,
423
286
  httpsAgent,
424
287
  });
425
288
  const expiresAt = new Date(Date.now() + tokenResponse.expires_in * 1000);
426
- console.log(`[nova-sdk] ✅ Token exchange successful for "${slug}" — expires ${expiresAt.toISOString()} (${tokenResponse.expires_in}s, hasRefreshToken: ${!!tokenResponse.refresh_token})`);
427
- // ── Step 7: Cache in memory with refresh metadata ─────────────────────
428
- const cacheEntry = {
429
- accessToken: tokenResponse.access_token,
430
- expiresAt,
431
- httpsAgent,
432
- authMode: creds.authMode,
433
- // Store refresh metadata for client-side refresh next time
434
- refreshToken: tokenResponse.refresh_token ?? undefined,
435
- tokenEndpoint: creds.tokenEndpoint,
436
- clientId: creds.clientId,
437
- clientSecret: creds.clientSecret,
438
- };
439
- _tokenCache.set(cacheKey, cacheEntry);
440
- // Also persist refresh metadata in a long-lived key (survives access token expiry)
441
- if (tokenResponse.refresh_token) {
442
- _tokenCache.set(`${cacheKey}:refresh`, {
443
- ...cacheEntry,
444
- expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
445
- });
446
- console.log(`[nova-sdk] 💾 Cached refresh metadata for "${slug}"`);
447
- }
289
+ console.log(`[nova-sdk] ✅ Token exchange successful for "${slug}" — expires ${expiresAt.toISOString()} (${tokenResponse.expires_in}s)`);
448
290
  return {
449
291
  accessToken: tokenResponse.access_token,
450
292
  expiresAt,
package/dist/index.d.ts CHANGED
@@ -170,8 +170,9 @@ export interface ActionCtx {
170
170
  auth?: JWTPayload;
171
171
  /**
172
172
  * Resolve OAuth credentials for the current integration (or a named slug).
173
- * Handles all auth modes (mtls, client_credentials, standard) with
174
- * 3-tier caching: in-memory DB (vault-encrypted) fresh token exchange.
173
+ * Handles all auth modes (mtls, client_credentials, standard).
174
+ * Always fetches fresh credentials from the auth server — no local token cache.
175
+ * On 401, use ctx.fetch() which automatically retries with forceRefresh=true.
175
176
  *
176
177
  * @param slug - Integration slug (defaults to the current worker's name)
177
178
  * @param userId - User ID for standard OAuth; omit for server flows
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { createServer } from "node:http";
6
6
  import { os } from "@orpc/server";
7
7
  import { RPCHandler } from "@orpc/server/node";
8
8
  import { CORSPlugin } from "@orpc/server/plugins";
9
- import { resolveCredentialsViaHttp as _resolveCredentialsViaHttp, detectCredentialStrategy, integrationFetch as _integrationFetch, clearTokenCache as _clearTokenCache, } from "./credentials.js";
9
+ import { resolveCredentialsViaHttp as _resolveCredentialsViaHttp, detectCredentialStrategy, integrationFetch as _integrationFetch, } from "./credentials.js";
10
10
  if (!process.env.RUNTIME_SUPABASE_URL) {
11
11
  // local dev – read .env.local
12
12
  dotenv.config({ path: ".env.local", override: true });
@@ -545,7 +545,10 @@ export async function generateOpenAPISpec(def) {
545
545
  * AUTH_ISSUER_BASE_URL → calls the auth server with the inbound JWT to
546
546
  * retrieve credentials, then performs the OAuth token exchange locally.
547
547
  *
548
- * Token cache order: in-memory client-side refresh_token grant auth server fetch
548
+ * No local token cache every resolveCredentials() call fetches fresh from
549
+ * the auth server. A per-request _lastCreds variable avoids duplicate calls
550
+ * within the same handler invocation.
551
+ * On 401, ctx.fetch() retries once with forceRefresh=true (X-Nova-Token-Invalid).
549
552
  *
550
553
  * @param defaultSlug - Integration slug (e.g., "jira", "bamboohr")
551
554
  * @param authToken - JWT Bearer token from the inbound request (forwarded to auth server)
@@ -577,14 +580,14 @@ function buildCredentialCtx(defaultSlug, authToken) {
577
580
  const response = await _integrationFetch(url, creds, options);
578
581
  // ── 401 auto-retry ───────────────────────────────────────────────────────
579
582
  // If the API returns 401 AND the caller did NOT supply explicit credentials
580
- // (3rd argument), the token may be stale. Clear the cache and force-refresh
581
- // from the auth server, then retry the request once.
583
+ // (3rd argument), force-refresh from the auth server (sends
584
+ // X-Nova-Token-Invalid: true so the auth server also discards its token),
585
+ // then retry the request once.
582
586
  //
583
587
  // If credentials were passed explicitly (3rd arg), skip retry — the caller
584
588
  // is managing their own token lifecycle.
585
589
  if (response.status === 401 && !credentials) {
586
- console.warn(`[nova-sdk] ⚠️ 401 Unauthorized from ${url} — clearing cache and force-refreshing credentials for "${defaultSlug}"`);
587
- _clearTokenCache(defaultSlug);
590
+ console.warn(`[nova-sdk] ⚠️ 401 Unauthorized from ${url} — force-refreshing credentials for "${defaultSlug}" (X-Nova-Token-Invalid)`);
588
591
  _lastCreds = null;
589
592
  if (strategy !== "http") {
590
593
  console.error(`[nova-sdk] ❌ Cannot force-refresh: strategy is "${strategy}" (not "http")`);
@@ -595,7 +598,7 @@ function buildCredentialCtx(defaultSlug, authToken) {
595
598
  console.error("[nova-sdk] ❌ Cannot force-refresh: no authToken available in request context");
596
599
  return response; // return original 401
597
600
  }
598
- console.log(`[nova-sdk] 🔄 Force-refreshing token for "${defaultSlug}" via auth server`);
601
+ console.log(`[nova-sdk] 🔄 Force-refreshing token for "${defaultSlug}" via auth server (forceRefresh=true)`);
599
602
  const freshCreds = await _resolveCredentialsViaHttp(authBaseUrl, defaultSlug, authToken, undefined, { forceRefresh: true });
600
603
  _lastCreds = freshCreds;
601
604
  // Only retry if we actually got a different token — guards against infinite loop
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newhomestar/sdk",
3
- "version": "0.7.23",
3
+ "version": "0.7.24",
4
4
  "description": "Type-safe SDK for building Nova pipelines (workers & functions)",
5
5
  "homepage": "https://github.com/newhomestar/nova-node-sdk#readme",
6
6
  "bugs": {