@newhomestar/sdk 0.7.14 → 0.7.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.
@@ -45,6 +45,12 @@ export declare class TokenExchangeError extends Error {
45
45
  * Reads from PLATFORM_SUPABASE_URL + PLATFORM_SUPABASE_SERVICE_ROLE_KEY.
46
46
  */
47
47
  export declare function createPlatformClient(): SupabaseClient;
48
+ /**
49
+ * Evict a cached token for the given integration slug and optional userId.
50
+ * Call this when an API returns 401 so the next resolveCredentials() call
51
+ * fetches a fresh token instead of re-using the revoked one.
52
+ */
53
+ export declare function clearTokenCache(slug: string, userId?: string): void;
48
54
  /**
49
55
  * Resolves an access token for an integration, handling all auth modes.
50
56
  *
@@ -90,7 +96,9 @@ export declare function emitPlatformEvent(platformDB: SupabaseClient, topic: str
90
96
  * @param bearerToken - JWT to authenticate with the auth server
91
97
  * @param userId - Optional user ID (for cache key differentiation)
92
98
  */
93
- export declare function resolveCredentialsViaHttp(authBaseUrl: string, slug: string, bearerToken: string, userId?: string): Promise<ResolvedCredentials>;
99
+ export declare function resolveCredentialsViaHttp(authBaseUrl: string, slug: string, bearerToken: string, userId?: string, options?: {
100
+ forceRefresh?: boolean;
101
+ }): Promise<ResolvedCredentials>;
94
102
  /**
95
103
  * Detect which credential resolution strategy to use.
96
104
  * Returns 'db' if PLATFORM_SUPABASE_* are available, 'http' if AUTH_ISSUER_BASE_URL
@@ -78,6 +78,16 @@ const SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
78
78
  function getCacheKey(integrationSlug, userId) {
79
79
  return `${integrationSlug}:${userId ?? SYSTEM_USER_ID}`;
80
80
  }
81
+ /**
82
+ * Evict a cached token for the given integration slug and optional userId.
83
+ * Call this when an API returns 401 so the next resolveCredentials() call
84
+ * fetches a fresh token instead of re-using the revoked one.
85
+ */
86
+ export function clearTokenCache(slug, userId) {
87
+ const key = getCacheKey(slug, userId);
88
+ const deleted = _tokenCache.delete(key);
89
+ console.log(`[nova-sdk] 🗑️ Cleared in-memory token cache for "${slug}" (key=${key}, existed=${deleted})`);
90
+ }
81
91
  function getCachedToken(key) {
82
92
  const cached = _tokenCache.get(key);
83
93
  if (!cached)
@@ -613,20 +623,23 @@ function buildMtlsAgentFromPem(slug, certPem, keyPem) {
613
623
  * Requires a valid JWT Bearer token (the same one that authenticated the
614
624
  * inbound request to this container).
615
625
  *
616
- * @param authBaseUrl - Auth server base URL (e.g., "http://localhost:3001")
617
- * @param slug - Integration slug (e.g., "adp")
618
- * @param bearerToken - JWT to forward as Authorization header
626
+ * @param authBaseUrl - Auth server base URL (e.g., "http://localhost:3001")
627
+ * @param slug - Integration slug (e.g., "adp")
628
+ * @param bearerToken - JWT to forward as Authorization header
629
+ * @param forceRefresh - When true, sends X-Nova-Token-Invalid: true to trigger a forced refresh
619
630
  */
620
- async function fetchCredentialsFromAuthServer(authBaseUrl, slug, bearerToken) {
631
+ async function fetchCredentialsFromAuthServer(authBaseUrl, slug, bearerToken, forceRefresh = false) {
621
632
  const url = `${authBaseUrl}/api/integrations/${encodeURIComponent(slug)}/credentials`;
622
- console.log(`[nova-sdk] 🌐 Fetching credentials via HTTP: GET ${url}`);
623
- const res = await fetch(url, {
624
- method: "GET",
625
- headers: {
626
- Authorization: `Bearer ${bearerToken}`,
627
- Accept: "application/json",
628
- },
629
- });
633
+ console.log(`[nova-sdk] 🌐 Fetching credentials via HTTP: GET ${url}${forceRefresh ? " (force-refresh)" : ""}`);
634
+ const headers = {
635
+ Authorization: `Bearer ${bearerToken}`,
636
+ Accept: "application/json",
637
+ };
638
+ // Signal the auth server to discard its stored token and force a refresh
639
+ if (forceRefresh) {
640
+ headers["X-Nova-Token-Invalid"] = "true";
641
+ }
642
+ const res = await fetch(url, { method: "GET", headers });
630
643
  if (!res.ok) {
631
644
  const errBody = await res.text().catch(() => "");
632
645
  if (res.status === 401) {
@@ -656,12 +669,13 @@ async function fetchCredentialsFromAuthServer(authBaseUrl, slug, bearerToken) {
656
669
  * @param bearerToken - JWT to authenticate with the auth server
657
670
  * @param userId - Optional user ID (for cache key differentiation)
658
671
  */
659
- export async function resolveCredentialsViaHttp(authBaseUrl, slug, bearerToken, userId) {
672
+ export async function resolveCredentialsViaHttp(authBaseUrl, slug, bearerToken, userId, options) {
660
673
  const effectiveUserId = userId ?? SYSTEM_USER_ID;
661
674
  const cacheKey = getCacheKey(slug, effectiveUserId);
662
- console.log(`[nova-sdk] 🔑 Resolving credentials for "${slug}" via HTTP (userId: ${effectiveUserId})`);
663
- // ── Step 1: Check in-memory cache ─────────────────────────────────────
664
- const cached = getCachedToken(cacheKey);
675
+ const forceRefresh = options?.forceRefresh ?? false;
676
+ console.log(`[nova-sdk] 🔑 Resolving credentials for "${slug}" via HTTP (userId: ${effectiveUserId}${forceRefresh ? ", forceRefresh=true" : ""})`);
677
+ // ── Step 1: Check in-memory cache (skip if forcing refresh) ──────────
678
+ const cached = forceRefresh ? null : getCachedToken(cacheKey);
665
679
  if (cached) {
666
680
  // Derive authMode: use stored value if available, otherwise infer from httpsAgent
667
681
  const cachedAuthMode = cached.authMode ?? (cached.httpsAgent ? "mtls" : "client_credentials");
@@ -675,7 +689,7 @@ export async function resolveCredentialsViaHttp(authBaseUrl, slug, bearerToken,
675
689
  };
676
690
  }
677
691
  // ── Step 2: Fetch credentials from auth server ────────────────────────
678
- const creds = await fetchCredentialsFromAuthServer(authBaseUrl, slug, bearerToken);
692
+ const creds = await fetchCredentialsFromAuthServer(authBaseUrl, slug, bearerToken, forceRefresh);
679
693
  // ── Step 2b: Standard auth — use pre-resolved access token if available ──
680
694
  if (creds.authMode === "standard") {
681
695
  if (creds.accessToken) {
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 { createPlatformClient, resolveCredentials as _resolveCredentials, resolveCredentialsViaHttp as _resolveCredentialsViaHttp, detectCredentialStrategy, integrationFetch as _integrationFetch, } from "./credentials.js";
9
+ import { createPlatformClient, resolveCredentials as _resolveCredentials, resolveCredentialsViaHttp as _resolveCredentialsViaHttp, detectCredentialStrategy, integrationFetch as _integrationFetch, clearTokenCache as _clearTokenCache, } 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 });
@@ -580,7 +580,42 @@ function buildCredentialCtx(defaultSlug, authToken) {
580
580
  };
581
581
  const ctxFetch = async (url, options, credentials) => {
582
582
  const creds = credentials ?? _lastCreds ?? await ctxResolveCredentials();
583
- return _integrationFetch(url, creds, options);
583
+ const response = await _integrationFetch(url, creds, options);
584
+ // ── 401 auto-retry: token may have been revoked ──────────────────────────
585
+ // If the API returns 401 AND the caller didn't supply explicit credentials
586
+ // (i.e., we used the cached/resolved ones), clear the in-memory cache and
587
+ // force-refresh from the auth server, then retry once.
588
+ // This handles the "user re-authenticated but container still holds the old
589
+ // revoked token" scenario.
590
+ if (response.status === 401 && !credentials) {
591
+ console.warn(`[nova-sdk] ⚠️ 401 received from ${url} — clearing cache and force-refreshing credentials for "${defaultSlug}"`);
592
+ _clearTokenCache(defaultSlug);
593
+ _lastCreds = null;
594
+ let freshCreds;
595
+ if (strategy === "http") {
596
+ // HTTP strategy: signal auth server to bypass its stored token and refresh
597
+ const authBaseUrl = process.env.AUTH_ISSUER_BASE_URL;
598
+ if (!authToken) {
599
+ console.error("[nova-sdk] ❌ Cannot force-refresh: no authToken available");
600
+ return response; // return original 401
601
+ }
602
+ freshCreds = await _resolveCredentialsViaHttp(authBaseUrl, defaultSlug, authToken, undefined, { forceRefresh: true });
603
+ }
604
+ else {
605
+ // DB strategy: just re-resolve (DB always reads latest from vault)
606
+ freshCreds = await ctxResolveCredentials();
607
+ }
608
+ _lastCreds = freshCreds;
609
+ // Only retry if we got a different (fresh) token — avoid an infinite loop
610
+ if (freshCreds.accessToken !== creds.accessToken) {
611
+ console.log(`[nova-sdk] 🔄 Retrying request with refreshed token for "${defaultSlug}"`);
612
+ return _integrationFetch(url, freshCreds, options);
613
+ }
614
+ else {
615
+ console.warn(`[nova-sdk] ⚠️ Force-refresh returned the same token for "${defaultSlug}" — not retrying`);
616
+ }
617
+ }
618
+ return response;
584
619
  };
585
620
  return { resolveCredentials: ctxResolveCredentials, fetch: ctxFetch };
586
621
  }
package/dist/next.d.ts CHANGED
@@ -303,9 +303,23 @@ export declare function buildPrismaPage(input: CursorPageInputType): {
303
303
  id: 'desc';
304
304
  }>;
305
305
  };
306
+ /**
307
+ * Convert a snake_case string to camelCase.
308
+ * Used internally to normalise sort column names from API query params
309
+ * (snake_case) to Prisma model field names (camelCase).
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * snakeToCamel('modified_at') // → 'modifiedAt'
314
+ * snakeToCamel('createdAt') // → 'createdAt' (no-op)
315
+ * ```
316
+ */
317
+ export declare function snakeToCamel(s: string): string;
306
318
  /**
307
319
  * Build Prisma `skip`, `take`, and `orderBy` from an `OffsetPageInput`.
308
320
  * Handles offset pagination and sort direction.
321
+ * Sort column names are automatically converted from snake_case (API convention)
322
+ * to camelCase (Prisma convention) via `snakeToCamel()`.
309
323
  * Does NOT handle `search` or `filters` — apply those to the `where` clause manually.
310
324
  *
311
325
  * @example
package/dist/next.js CHANGED
@@ -191,9 +191,25 @@ export function buildPrismaPage(input) {
191
191
  orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
192
192
  };
193
193
  }
194
+ /**
195
+ * Convert a snake_case string to camelCase.
196
+ * Used internally to normalise sort column names from API query params
197
+ * (snake_case) to Prisma model field names (camelCase).
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * snakeToCamel('modified_at') // → 'modifiedAt'
202
+ * snakeToCamel('createdAt') // → 'createdAt' (no-op)
203
+ * ```
204
+ */
205
+ export function snakeToCamel(s) {
206
+ return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
207
+ }
194
208
  /**
195
209
  * Build Prisma `skip`, `take`, and `orderBy` from an `OffsetPageInput`.
196
210
  * Handles offset pagination and sort direction.
211
+ * Sort column names are automatically converted from snake_case (API convention)
212
+ * to camelCase (Prisma convention) via `snakeToCamel()`.
197
213
  * Does NOT handle `search` or `filters` — apply those to the `where` clause manually.
198
214
  *
199
215
  * @example
@@ -203,7 +219,7 @@ export function buildPrismaPage(input) {
203
219
  * ```
204
220
  */
205
221
  export function buildPrismaOffsetPage(input, defaultSort = 'createdAt') {
206
- const col = input.sort ?? defaultSort;
222
+ const col = snakeToCamel(input.sort ?? defaultSort);
207
223
  return {
208
224
  skip: input.offset,
209
225
  take: input.limit,
package/package.json CHANGED
@@ -1,58 +1,58 @@
1
- {
2
- "name": "@newhomestar/sdk",
3
- "version": "0.7.14",
4
- "description": "Type-safe SDK for building Nova pipelines (workers & functions)",
5
- "homepage": "https://github.com/newhomestar/nova-node-sdk#readme",
6
- "bugs": {
7
- "url": "https://github.com/newhomestar/nova-node-sdk/issues"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/newhomestar/nova-node-sdk.git"
12
- },
13
- "license": "ISC",
14
- "author": "Christian Gomez",
15
- "type": "module",
16
- "main": "dist/index.js",
17
- "types": "dist/index.d.ts",
18
- "exports": {
19
- ".": {
20
- "import": "./dist/index.js",
21
- "types": "./dist/index.d.ts"
22
- },
23
- "./next": {
24
- "import": "./dist/next.js",
25
- "types": "./dist/next.d.ts"
26
- },
27
- "./events": {
28
- "import": "./dist/events.js",
29
- "types": "./dist/events.d.ts"
30
- }
31
- },
32
- "files": [
33
- "dist"
34
- ],
35
- "scripts": {
36
- "build": "tsc"
37
- },
38
- "dependencies": {
39
- "@openfga/sdk": "^0.9.0",
40
- "@orpc/openapi": "1.7.4",
41
- "@orpc/server": "1.7.4",
42
- "@supabase/supabase-js": "^2.39.0",
43
- "body-parser": "^1.20.2",
44
- "dotenv": "^16.4.3",
45
- "express": "^4.18.2",
46
- "express-oauth2-jwt-bearer": "^1.7.4",
47
- "undici": "^7.24.4",
48
- "yaml": "^2.7.1"
49
- },
50
- "peerDependencies": {
51
- "zod": ">=4.0.0"
52
- },
53
- "devDependencies": {
54
- "@types/node": "^20.11.17",
55
- "typescript": "^5.4.4",
56
- "zod": "^4.3.0"
57
- }
58
- }
1
+ {
2
+ "name": "@newhomestar/sdk",
3
+ "version": "0.7.15",
4
+ "description": "Type-safe SDK for building Nova pipelines (workers & functions)",
5
+ "homepage": "https://github.com/newhomestar/nova-node-sdk#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/newhomestar/nova-node-sdk/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/newhomestar/nova-node-sdk.git"
12
+ },
13
+ "license": "ISC",
14
+ "author": "Christian Gomez",
15
+ "type": "module",
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "import": "./dist/index.js",
21
+ "types": "./dist/index.d.ts"
22
+ },
23
+ "./next": {
24
+ "import": "./dist/next.js",
25
+ "types": "./dist/next.d.ts"
26
+ },
27
+ "./events": {
28
+ "import": "./dist/events.js",
29
+ "types": "./dist/events.d.ts"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc"
37
+ },
38
+ "dependencies": {
39
+ "@openfga/sdk": "^0.9.0",
40
+ "@orpc/openapi": "1.7.4",
41
+ "@orpc/server": "1.7.4",
42
+ "@supabase/supabase-js": "^2.39.0",
43
+ "body-parser": "^1.20.2",
44
+ "dotenv": "^16.4.3",
45
+ "express": "^4.18.2",
46
+ "express-oauth2-jwt-bearer": "^1.7.4",
47
+ "undici": "^7.24.4",
48
+ "yaml": "^2.7.1"
49
+ },
50
+ "peerDependencies": {
51
+ "zod": ">=4.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^20.11.17",
55
+ "typescript": "^5.4.4",
56
+ "zod": "^4.3.0"
57
+ }
58
+ }