@newhomestar/sdk 0.7.14 → 0.7.16
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/credentials.d.ts +9 -1
- package/dist/credentials.js +31 -17
- package/dist/index.d.ts +63 -0
- package/dist/index.js +41 -3
- package/package.json +1 -1
package/dist/credentials.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/credentials.js
CHANGED
|
@@ -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
|
|
617
|
-
* @param slug
|
|
618
|
-
* @param bearerToken
|
|
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
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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.d.ts
CHANGED
|
@@ -85,6 +85,45 @@ export interface ActionDef<I extends ZodTypeAny, O extends ZodTypeAny> {
|
|
|
85
85
|
eventTypes?: string[];
|
|
86
86
|
consumerGroup?: string;
|
|
87
87
|
}>;
|
|
88
|
+
/**
|
|
89
|
+
* Sync capability metadata — declares this action as a data sync endpoint.
|
|
90
|
+
* Read by `nova integrations build` to include sync config in nova-integration.yaml.
|
|
91
|
+
* Surfaced in the Odyssey UI DataSyncTab for each integration.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* sync: {
|
|
96
|
+
* entityType: "issue",
|
|
97
|
+
* direction: "to_nova",
|
|
98
|
+
* label: "Jira Issues",
|
|
99
|
+
* description: "Import all Jira issues into Nova",
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
sync?: {
|
|
104
|
+
/** Entity type identifier (e.g., "issue", "contact", "employee") */
|
|
105
|
+
entityType: string;
|
|
106
|
+
/** Direction of sync: to_nova = import from provider; from_nova = export to provider */
|
|
107
|
+
direction: 'to_nova' | 'from_nova' | 'bidirectional';
|
|
108
|
+
/** Human-readable label shown in the UI (e.g., "Jira Issues") */
|
|
109
|
+
label: string;
|
|
110
|
+
/** Optional description shown below the label in the DataSyncTab */
|
|
111
|
+
description?: string;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Trigger-based routing declarations.
|
|
115
|
+
* Used by the SSE worker mode to route incoming messages to this action's handler.
|
|
116
|
+
* Also read by `nova integrations build` to register event_subscriptions on push.
|
|
117
|
+
*/
|
|
118
|
+
triggers?: Array<{
|
|
119
|
+
type: 'event';
|
|
120
|
+
events: string[];
|
|
121
|
+
} | {
|
|
122
|
+
type: 'schedule';
|
|
123
|
+
cron: string;
|
|
124
|
+
timezone?: string;
|
|
125
|
+
description?: string;
|
|
126
|
+
}>;
|
|
88
127
|
}
|
|
89
128
|
/** Validated JWT claims from express-oauth2-jwt-bearer */
|
|
90
129
|
export interface JWTPayload {
|
|
@@ -208,6 +247,30 @@ export declare function action<I extends ZodTypeAny, O extends ZodTypeAny>(cfg:
|
|
|
208
247
|
eventTypes?: string[];
|
|
209
248
|
consumerGroup?: string;
|
|
210
249
|
}>;
|
|
250
|
+
/**
|
|
251
|
+
* Sync capability metadata — declares this action as a data sync endpoint.
|
|
252
|
+
* Surfaced in the Odyssey UI DataSyncTab and included in nova-integration.yaml
|
|
253
|
+
* by `nova integrations build`.
|
|
254
|
+
*/
|
|
255
|
+
sync?: {
|
|
256
|
+
entityType: string;
|
|
257
|
+
direction: 'to_nova' | 'from_nova' | 'bidirectional';
|
|
258
|
+
label: string;
|
|
259
|
+
description?: string;
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Trigger-based routing declarations for event/schedule triggers.
|
|
263
|
+
* Used by the SSE worker and CLI build pipeline.
|
|
264
|
+
*/
|
|
265
|
+
triggers?: Array<{
|
|
266
|
+
type: 'event';
|
|
267
|
+
events: string[];
|
|
268
|
+
} | {
|
|
269
|
+
type: 'schedule';
|
|
270
|
+
cron: string;
|
|
271
|
+
timezone?: string;
|
|
272
|
+
description?: string;
|
|
273
|
+
}>;
|
|
211
274
|
handler: (input: z.infer<I>, ctx: ActionCtx) => Promise<z.infer<O>>;
|
|
212
275
|
}): ActionDef<I, O>;
|
|
213
276
|
import type { NovaSpec } from './parseSpec.js';
|
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
|
-
|
|
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
|
}
|
|
@@ -732,7 +767,10 @@ export function runHttpServer(def, opts = {}) {
|
|
|
732
767
|
rawInput[key] = val === 'true' || val === '1';
|
|
733
768
|
}
|
|
734
769
|
// Auto-detect from Zod schema shape if no explicit uiType
|
|
735
|
-
|
|
770
|
+
// Guard: _def.shape must be a function (Zod v3 thunk) before calling it.
|
|
771
|
+
// Some Zod internals expose _def.shape as a plain object; calling a non-function
|
|
772
|
+
// throws "act.input._def.shape is not a function" at runtime.
|
|
773
|
+
else if (!uiType && typeof act.input?._def?.shape === 'function') {
|
|
736
774
|
const fieldDef = act.input._def.shape()?.[key];
|
|
737
775
|
const innerType = fieldDef?._def?.innerType?._def?.typeName ?? fieldDef?._def?.typeName;
|
|
738
776
|
if (innerType === 'ZodNumber') {
|
package/package.json
CHANGED