@newhomestar/sdk 0.7.25 → 0.8.0
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 +62 -0
- package/dist/credentials.js +177 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/package.json +1 -1
package/dist/credentials.d.ts
CHANGED
|
@@ -89,3 +89,65 @@ export declare function integrationFetch(url: string, credentials: ResolvedCrede
|
|
|
89
89
|
* separate from credential resolution. Credential resolution uses HTTP (AUTH_ISSUER_BASE_URL).
|
|
90
90
|
*/
|
|
91
91
|
export declare function emitPlatformEvent(platformDB: SupabaseClient, topic: string, integrationSlug: string, payload: Record<string, unknown>): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Fetch integration credentials using a service token (NOVA_SERVICE_TOKEN).
|
|
94
|
+
* Same auth server endpoint as resolveCredentialsViaHttp, but authenticates
|
|
95
|
+
* with the service token instead of a user JWT. Intended for background
|
|
96
|
+
* processing loops that run outside the HTTP request context.
|
|
97
|
+
*
|
|
98
|
+
* @param authBaseUrl - Auth server base URL (AUTH_ISSUER_BASE_URL)
|
|
99
|
+
* @param slug - Integration slug (e.g., "adp", "bamboohr")
|
|
100
|
+
* @param serviceToken - NOVA_SERVICE_TOKEN value
|
|
101
|
+
*/
|
|
102
|
+
export declare function resolveCredentialsViaServiceToken(authBaseUrl: string, slug: string, serviceToken: string): Promise<ResolvedCredentials>;
|
|
103
|
+
/** The client object returned by createIntegrationClient() */
|
|
104
|
+
export interface IntegrationClient {
|
|
105
|
+
/**
|
|
106
|
+
* mTLS-aware fetch with auto-refreshing credentials.
|
|
107
|
+
* Automatically re-resolves credentials when they expire.
|
|
108
|
+
* On 401, forces a credential refresh and retries once.
|
|
109
|
+
*/
|
|
110
|
+
fetch: (url: string, options?: {
|
|
111
|
+
method?: string;
|
|
112
|
+
headers?: Record<string, string>;
|
|
113
|
+
body?: string;
|
|
114
|
+
}) => Promise<Response>;
|
|
115
|
+
/** Force credential re-resolution (e.g., after persistent 401s) */
|
|
116
|
+
refreshCredentials: () => Promise<ResolvedCredentials>;
|
|
117
|
+
/** Get current credentials (resolves if needed) */
|
|
118
|
+
ensureCredentials: () => Promise<ResolvedCredentials>;
|
|
119
|
+
/** The integration slug */
|
|
120
|
+
readonly slug: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Create a standalone integration client for background / headless contexts.
|
|
124
|
+
*
|
|
125
|
+
* Uses NOVA_SERVICE_TOKEN to resolve credentials from the auth server.
|
|
126
|
+
* Credentials are cached in memory and auto-refreshed when they expire
|
|
127
|
+
* (with a 60-second buffer before expiry).
|
|
128
|
+
*
|
|
129
|
+
* Designed for the two-phase ledger queue pattern:
|
|
130
|
+
* 1. Action handler seeds ledger rows (fast, no API calls)
|
|
131
|
+
* 2. Background loop processes ledger rows using this client
|
|
132
|
+
*
|
|
133
|
+
* @param slug - Integration slug (e.g., "adp", "bamboohr")
|
|
134
|
+
* @param options - Optional overrides for service token and auth URL
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* // In action handler (has JWT context):
|
|
139
|
+
* const client = createIntegrationClient("adp");
|
|
140
|
+
* setImmediate(() => processLedgerQueue(jobId, tenantId, client));
|
|
141
|
+
*
|
|
142
|
+
* // In background loop:
|
|
143
|
+
* const resp = await client.fetch("https://api.adp.com/hr/v2/workers/123");
|
|
144
|
+
* if (resp.status === 204) continue; // handled correctly — no crash
|
|
145
|
+
* const data = await resp.json();
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export declare function createIntegrationClient(slug: string, options?: {
|
|
149
|
+
/** Override service token (defaults to NOVA_SERVICE_TOKEN env var) */
|
|
150
|
+
serviceToken?: string;
|
|
151
|
+
/** Override auth server URL (defaults to AUTH_ISSUER_BASE_URL env var) */
|
|
152
|
+
authBaseUrl?: string;
|
|
153
|
+
}): IntegrationClient;
|
package/dist/credentials.js
CHANGED
|
@@ -354,9 +354,16 @@ export async function integrationFetch(url, credentials, options = {}) {
|
|
|
354
354
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
355
355
|
res.on("end", () => {
|
|
356
356
|
const bodyStr = Buffer.concat(chunks).toString("utf-8");
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
357
|
+
const statusCode = res.statusCode ?? 500;
|
|
358
|
+
console.log(`[nova-sdk] 📥 mTLS response: HTTP ${statusCode} for ${options.method ?? "GET"} ${url}`);
|
|
359
|
+
// Null-body status codes (204, 304) must NOT include a body per the
|
|
360
|
+
// Fetch spec. Node.js v20's Response constructor enforces this — passing
|
|
361
|
+
// a non-null body with status 204 throws a TypeError that escapes the
|
|
362
|
+
// Promise chain (the https callback is not connected to it), causing
|
|
363
|
+
// an uncaught exception AND a permanently-pending Promise (hang).
|
|
364
|
+
const isNullBodyStatus = statusCode === 204 || statusCode === 304;
|
|
365
|
+
resolve(new Response(isNullBodyStatus ? null : bodyStr, {
|
|
366
|
+
status: statusCode,
|
|
360
367
|
statusText: res.statusMessage ?? "Unknown",
|
|
361
368
|
headers: new Headers(res.headers),
|
|
362
369
|
}));
|
|
@@ -409,3 +416,170 @@ export async function emitPlatformEvent(platformDB, topic, integrationSlug, payl
|
|
|
409
416
|
console.log(`[nova-sdk] ✅ Event emitted: ${topic}`);
|
|
410
417
|
}
|
|
411
418
|
}
|
|
419
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
420
|
+
// Service-Token Credential Resolution (for background / headless contexts)
|
|
421
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
422
|
+
//
|
|
423
|
+
// Background sync loops (fire-and-forget via setImmediate) don't have an
|
|
424
|
+
// inbound JWT. They use NOVA_SERVICE_TOKEN to authenticate with the auth
|
|
425
|
+
// server's credential endpoint instead.
|
|
426
|
+
//
|
|
427
|
+
// The auth server already accepts this token as a Bearer (matched against
|
|
428
|
+
// INTERNAL_API_SECRET env var on the auth server side).
|
|
429
|
+
//
|
|
430
|
+
// For client_credentials / mTLS integrations (like ADP), no userId is needed.
|
|
431
|
+
// For standard auth, the service token path won't have user context — callers
|
|
432
|
+
// must pre-resolve user tokens in the handler while the JWT is available.
|
|
433
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
434
|
+
/**
|
|
435
|
+
* Fetch integration credentials using a service token (NOVA_SERVICE_TOKEN).
|
|
436
|
+
* Same auth server endpoint as resolveCredentialsViaHttp, but authenticates
|
|
437
|
+
* with the service token instead of a user JWT. Intended for background
|
|
438
|
+
* processing loops that run outside the HTTP request context.
|
|
439
|
+
*
|
|
440
|
+
* @param authBaseUrl - Auth server base URL (AUTH_ISSUER_BASE_URL)
|
|
441
|
+
* @param slug - Integration slug (e.g., "adp", "bamboohr")
|
|
442
|
+
* @param serviceToken - NOVA_SERVICE_TOKEN value
|
|
443
|
+
*/
|
|
444
|
+
export async function resolveCredentialsViaServiceToken(authBaseUrl, slug, serviceToken) {
|
|
445
|
+
console.log(`[nova-sdk] 🔑 resolveCredentialsViaServiceToken: slug="${slug}" authBaseUrl="${authBaseUrl}"`);
|
|
446
|
+
// Fetch credentials from auth server using service token as Bearer
|
|
447
|
+
const creds = await fetchCredentialsFromAuthServer(authBaseUrl, slug, serviceToken, false);
|
|
448
|
+
// ── Standard auth — not supported in service-token mode ───────────────
|
|
449
|
+
if (creds.authMode === "standard") {
|
|
450
|
+
// Standard auth needs a userId for user_app_connections lookup.
|
|
451
|
+
// The service token path doesn't carry user context.
|
|
452
|
+
// If the auth server returned a pre-resolved token (org-level), use it.
|
|
453
|
+
if (creds.accessToken) {
|
|
454
|
+
const expiresAt = creds.expiresAt
|
|
455
|
+
? new Date(creds.expiresAt)
|
|
456
|
+
: new Date(Date.now() + 3600 * 1000);
|
|
457
|
+
return {
|
|
458
|
+
accessToken: creds.accessToken,
|
|
459
|
+
expiresAt,
|
|
460
|
+
integrationId: `service:${slug}`,
|
|
461
|
+
authMode: "standard",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
throw new Error(`[nova-sdk] Standard auth integration "${slug}" requires user context (JWT). ` +
|
|
465
|
+
`Use ctx.resolveCredentials() in the handler, not resolveCredentialsViaServiceToken().`);
|
|
466
|
+
}
|
|
467
|
+
// ── Validate required fields (client_credentials / mTLS) ─────────────
|
|
468
|
+
if (!creds.clientId) {
|
|
469
|
+
throw new CredentialsNotConfiguredError(slug, "client_id is not set");
|
|
470
|
+
}
|
|
471
|
+
if (!creds.clientSecret) {
|
|
472
|
+
throw new CredentialsNotConfiguredError(slug, "client_secret not available");
|
|
473
|
+
}
|
|
474
|
+
if (!creds.tokenEndpoint) {
|
|
475
|
+
throw new CredentialsNotConfiguredError(slug, "token_endpoint is not set");
|
|
476
|
+
}
|
|
477
|
+
// ── Build mTLS agent if needed ────────────────────────────────────────
|
|
478
|
+
let httpsAgent;
|
|
479
|
+
if (creds.authMode === "mtls") {
|
|
480
|
+
if (!creds.mtlsCert || !creds.mtlsKey) {
|
|
481
|
+
throw new CredentialsNotConfiguredError(slug, "mTLS cert/key not returned by auth server");
|
|
482
|
+
}
|
|
483
|
+
httpsAgent = buildMtlsAgentFromPem(slug, creds.mtlsCert, creds.mtlsKey);
|
|
484
|
+
}
|
|
485
|
+
// ── Perform token exchange ────────────────────────────────────────────
|
|
486
|
+
console.log(`[nova-sdk] 🔄 Service-token: performing ${creds.authMode} token exchange for "${slug}"`);
|
|
487
|
+
const tokenResponse = await performTokenExchange(slug, {
|
|
488
|
+
tokenEndpoint: creds.tokenEndpoint,
|
|
489
|
+
clientId: creds.clientId,
|
|
490
|
+
clientSecret: creds.clientSecret,
|
|
491
|
+
httpsAgent,
|
|
492
|
+
});
|
|
493
|
+
const expiresAt = new Date(Date.now() + tokenResponse.expires_in * 1000);
|
|
494
|
+
console.log(`[nova-sdk] ✅ Service-token: token exchange successful for "${slug}" — expires ${expiresAt.toISOString()}`);
|
|
495
|
+
return {
|
|
496
|
+
accessToken: tokenResponse.access_token,
|
|
497
|
+
expiresAt,
|
|
498
|
+
integrationId: `service:${slug}`,
|
|
499
|
+
authMode: creds.authMode,
|
|
500
|
+
httpsAgent,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Create a standalone integration client for background / headless contexts.
|
|
505
|
+
*
|
|
506
|
+
* Uses NOVA_SERVICE_TOKEN to resolve credentials from the auth server.
|
|
507
|
+
* Credentials are cached in memory and auto-refreshed when they expire
|
|
508
|
+
* (with a 60-second buffer before expiry).
|
|
509
|
+
*
|
|
510
|
+
* Designed for the two-phase ledger queue pattern:
|
|
511
|
+
* 1. Action handler seeds ledger rows (fast, no API calls)
|
|
512
|
+
* 2. Background loop processes ledger rows using this client
|
|
513
|
+
*
|
|
514
|
+
* @param slug - Integration slug (e.g., "adp", "bamboohr")
|
|
515
|
+
* @param options - Optional overrides for service token and auth URL
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```typescript
|
|
519
|
+
* // In action handler (has JWT context):
|
|
520
|
+
* const client = createIntegrationClient("adp");
|
|
521
|
+
* setImmediate(() => processLedgerQueue(jobId, tenantId, client));
|
|
522
|
+
*
|
|
523
|
+
* // In background loop:
|
|
524
|
+
* const resp = await client.fetch("https://api.adp.com/hr/v2/workers/123");
|
|
525
|
+
* if (resp.status === 204) continue; // handled correctly — no crash
|
|
526
|
+
* const data = await resp.json();
|
|
527
|
+
* ```
|
|
528
|
+
*/
|
|
529
|
+
export function createIntegrationClient(slug, options) {
|
|
530
|
+
const serviceToken = options?.serviceToken ??
|
|
531
|
+
process.env.NOVA_SERVICE_TOKEN;
|
|
532
|
+
const authBaseUrl = options?.authBaseUrl ??
|
|
533
|
+
process.env.AUTH_ISSUER_BASE_URL;
|
|
534
|
+
if (!serviceToken) {
|
|
535
|
+
throw new Error(`[nova-sdk] createIntegrationClient("${slug}"): NOVA_SERVICE_TOKEN is required ` +
|
|
536
|
+
`for background credential resolution. Set it as an environment variable.`);
|
|
537
|
+
}
|
|
538
|
+
if (!authBaseUrl) {
|
|
539
|
+
throw new Error(`[nova-sdk] createIntegrationClient("${slug}"): AUTH_ISSUER_BASE_URL is required. ` +
|
|
540
|
+
`Set it as an environment variable.`);
|
|
541
|
+
}
|
|
542
|
+
let _creds = null;
|
|
543
|
+
/** Resolve credentials, using cached version if still valid (60s buffer) */
|
|
544
|
+
const ensureCredentials = async () => {
|
|
545
|
+
const now = Date.now();
|
|
546
|
+
// Re-resolve if no cached creds, or if they expire within 60 seconds
|
|
547
|
+
if (!_creds || _creds.expiresAt.getTime() < now + 60_000) {
|
|
548
|
+
console.log(`[nova-sdk] 🔄 IntegrationClient("${slug}"): ${_creds ? "refreshing expired" : "resolving initial"} credentials`);
|
|
549
|
+
_creds = await resolveCredentialsViaServiceToken(authBaseUrl, slug, serviceToken);
|
|
550
|
+
}
|
|
551
|
+
return _creds;
|
|
552
|
+
};
|
|
553
|
+
/** Force a fresh credential resolution (busts the in-memory cache) */
|
|
554
|
+
const refreshCredentials = async () => {
|
|
555
|
+
console.log(`[nova-sdk] 🔄 IntegrationClient("${slug}"): force-refreshing credentials`);
|
|
556
|
+
_creds = null;
|
|
557
|
+
return ensureCredentials();
|
|
558
|
+
};
|
|
559
|
+
/** mTLS-aware fetch with auto-refresh + 401 retry */
|
|
560
|
+
const clientFetch = async (url, fetchOptions) => {
|
|
561
|
+
const creds = await ensureCredentials();
|
|
562
|
+
const response = await integrationFetch(url, creds, fetchOptions);
|
|
563
|
+
// ── 401 auto-retry: refresh credentials and retry once ──────────────
|
|
564
|
+
if (response.status === 401) {
|
|
565
|
+
console.warn(`[nova-sdk] ⚠️ IntegrationClient("${slug}"): 401 from ${url} — refreshing credentials and retrying`);
|
|
566
|
+
const freshCreds = await refreshCredentials();
|
|
567
|
+
// Only retry if we got a different token (prevent infinite loops)
|
|
568
|
+
if (freshCreds.accessToken !== creds.accessToken) {
|
|
569
|
+
console.log(`[nova-sdk] 🔄 IntegrationClient("${slug}"): retrying with refreshed token`);
|
|
570
|
+
return integrationFetch(url, freshCreds, fetchOptions);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
console.warn(`[nova-sdk] ⚠️ IntegrationClient("${slug}"): refresh returned same token — not retrying`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return response;
|
|
577
|
+
};
|
|
578
|
+
console.log(`[nova-sdk] ✅ IntegrationClient created for "${slug}" (auth: ${authBaseUrl}, token: ${serviceToken.slice(0, 8)}...)`);
|
|
579
|
+
return {
|
|
580
|
+
fetch: clientFetch,
|
|
581
|
+
refreshCredentials,
|
|
582
|
+
ensureCredentials,
|
|
583
|
+
slug,
|
|
584
|
+
};
|
|
585
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -416,5 +416,5 @@ export type ScheduleTrigger = {
|
|
|
416
416
|
};
|
|
417
417
|
/** Union of all trigger variants */
|
|
418
418
|
export type Trigger = EventTrigger | ScheduleTrigger;
|
|
419
|
-
export { createPlatformClient, resolveCredentialsViaHttp, detectCredentialStrategy, clearTokenCache, integrationFetch, emitPlatformEvent, IntegrationNotFoundError, IntegrationDisabledError, CredentialsNotConfiguredError, ConnectionNotFoundError, TokenExchangeError, } from "./credentials.js";
|
|
420
|
-
export type { ResolvedCredentials, AuthMode, } from "./credentials.js";
|
|
419
|
+
export { createPlatformClient, resolveCredentialsViaHttp, resolveCredentialsViaServiceToken, createIntegrationClient, detectCredentialStrategy, clearTokenCache, integrationFetch, emitPlatformEvent, IntegrationNotFoundError, IntegrationDisabledError, CredentialsNotConfiguredError, ConnectionNotFoundError, TokenExchangeError, } from "./credentials.js";
|
|
420
|
+
export type { ResolvedCredentials, AuthMode, IntegrationClient, } from "./credentials.js";
|
package/dist/index.js
CHANGED
|
@@ -913,7 +913,11 @@ export { parseIntegrationSpec, IntegrationSpecSchema } from "./integrationSpec.j
|
|
|
913
913
|
// Every integration gets vault-backed token caching, mTLS, and OAuth for free.
|
|
914
914
|
export { createPlatformClient,
|
|
915
915
|
// resolveCredentials (DB-based strategy removed — use resolveCredentialsViaHttp)
|
|
916
|
-
resolveCredentialsViaHttp,
|
|
916
|
+
resolveCredentialsViaHttp,
|
|
917
|
+
// Service-token credential resolution (for background / headless sync loops)
|
|
918
|
+
resolveCredentialsViaServiceToken,
|
|
919
|
+
// Standalone integration client (auto-refresh, mTLS, 401 retry)
|
|
920
|
+
createIntegrationClient, detectCredentialStrategy, clearTokenCache, integrationFetch, emitPlatformEvent,
|
|
917
921
|
// Error classes
|
|
918
922
|
IntegrationNotFoundError, IntegrationDisabledError, CredentialsNotConfiguredError, ConnectionNotFoundError, TokenExchangeError, } from "./credentials.js";
|
|
919
923
|
// // Default export for compatibility
|
package/package.json
CHANGED