@newhomestar/sdk 0.8.3 → 0.8.5
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/README.md +1187 -0
- package/dist/credentials.d.ts +12 -0
- package/dist/credentials.js +8 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +84 -6
- package/dist/integration.d.ts +282 -0
- package/dist/integration.js +205 -0
- package/package.json +58 -58
package/dist/credentials.d.ts
CHANGED
|
@@ -12,6 +12,18 @@ export interface ResolvedCredentials {
|
|
|
12
12
|
authMode: AuthMode;
|
|
13
13
|
/** mTLS agent (if auth_mode = 'mtls') — use for ALL API requests to that provider */
|
|
14
14
|
httpsAgent?: https.Agent;
|
|
15
|
+
/**
|
|
16
|
+
* Per-connection configuration values filled in by the user during setup.
|
|
17
|
+
* Populated from `app_integration_configs.config` via the auth server's
|
|
18
|
+
* credential resolution response. Empty `{}` if no config exists.
|
|
19
|
+
*/
|
|
20
|
+
config?: Record<string, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the user has completed all required configuration fields.
|
|
23
|
+
* `true` when every required configField has a value; `false` otherwise.
|
|
24
|
+
* `undefined` if the integration has no configFields.
|
|
25
|
+
*/
|
|
26
|
+
configComplete?: boolean;
|
|
15
27
|
}
|
|
16
28
|
export declare class IntegrationNotFoundError extends Error {
|
|
17
29
|
constructor(slug: string);
|
package/dist/credentials.js
CHANGED
|
@@ -262,6 +262,8 @@ export async function resolveCredentialsViaHttp(authBaseUrl, slug, bearerToken,
|
|
|
262
262
|
expiresAt,
|
|
263
263
|
integrationId: `http:${slug}`,
|
|
264
264
|
authMode: "standard",
|
|
265
|
+
config: creds.config ?? {},
|
|
266
|
+
configComplete: creds.configComplete ?? undefined,
|
|
265
267
|
};
|
|
266
268
|
}
|
|
267
269
|
// Standard auth but no access token — user hasn't authorized yet
|
|
@@ -303,6 +305,8 @@ export async function resolveCredentialsViaHttp(authBaseUrl, slug, bearerToken,
|
|
|
303
305
|
integrationId: `http:${slug}`,
|
|
304
306
|
authMode: creds.authMode,
|
|
305
307
|
httpsAgent,
|
|
308
|
+
config: creds.config ?? {},
|
|
309
|
+
configComplete: creds.configComplete ?? undefined,
|
|
306
310
|
};
|
|
307
311
|
}
|
|
308
312
|
/**
|
|
@@ -459,6 +463,8 @@ export async function resolveCredentialsViaServiceToken(authBaseUrl, slug, servi
|
|
|
459
463
|
expiresAt,
|
|
460
464
|
integrationId: `service:${slug}`,
|
|
461
465
|
authMode: "standard",
|
|
466
|
+
config: creds.config ?? {},
|
|
467
|
+
configComplete: creds.configComplete ?? undefined,
|
|
462
468
|
};
|
|
463
469
|
}
|
|
464
470
|
throw new Error(`[nova-sdk] Standard auth integration "${slug}" requires user context (JWT). ` +
|
|
@@ -498,6 +504,8 @@ export async function resolveCredentialsViaServiceToken(authBaseUrl, slug, servi
|
|
|
498
504
|
integrationId: `service:${slug}`,
|
|
499
505
|
authMode: creds.authMode,
|
|
500
506
|
httpsAgent,
|
|
507
|
+
config: creds.config ?? {},
|
|
508
|
+
configComplete: creds.configComplete ?? undefined,
|
|
501
509
|
};
|
|
502
510
|
}
|
|
503
511
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -168,6 +168,25 @@ export interface ActionCtx {
|
|
|
168
168
|
authToken?: string;
|
|
169
169
|
/** Validated JWT payload from JWKS verification (HTTP mode only) */
|
|
170
170
|
auth?: JWTPayload;
|
|
171
|
+
/**
|
|
172
|
+
* Per-connection configuration values filled in by the user during setup.
|
|
173
|
+
* Available in every action handler. Values come from the auth server's
|
|
174
|
+
* credential resolution response (`app_integration_configs` table).
|
|
175
|
+
*
|
|
176
|
+
* Empty object `{}` if the integration has no `configFields` or the
|
|
177
|
+
* connection has no saved configuration.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* handler: async (input, ctx) => {
|
|
182
|
+
* const domain = ctx.config.companyDomain as string;
|
|
183
|
+
* const baseUrl = `https://api.bamboohr.com/api/gateway.php/${domain}`;
|
|
184
|
+
* const res = await ctx.fetch(`${baseUrl}/v1/employees/directory`);
|
|
185
|
+
* // ...
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
config: Record<string, unknown>;
|
|
171
190
|
/**
|
|
172
191
|
* Resolve OAuth credentials for the current integration (or a named slug).
|
|
173
192
|
* Handles all auth modes (mtls, client_credentials, standard).
|
|
@@ -362,8 +381,8 @@ export type { QueueEventPayload, LogEventPayload, OutboxRelayOptions, ServiceEmi
|
|
|
362
381
|
export type { ZodTypeAny as SchemaAny, ZodTypeAny };
|
|
363
382
|
export { parseNovaSpec } from "./parseSpec.js";
|
|
364
383
|
export type { NovaSpec } from "./parseSpec.js";
|
|
365
|
-
export { defineIntegration, validateIntegration, integrationSchema, integrationEvent, integrationFunction, schema, event, IntegrationDefSchema, } from "./integration.js";
|
|
366
|
-
export type { IntegrationDef, IntegrationSchemaDef, IntegrationEventDef, IntegrationFunctionDef, ValidationResult, SchemaType, ParamMeta, ParamIn, ParamUiType, SyncMappingDef, SyncMappingFieldDef, WebhookConfig, WebhookTypeDef, WebhookFieldDef, } from "./integration.js";
|
|
384
|
+
export { defineIntegration, validateIntegration, integrationSchema, integrationEvent, integrationFunction, schema, event, IntegrationDefSchema, ConfigPhase, ConfigPhaseSchema, ConfigWidget, ConfigWidgetSchema, } from "./integration.js";
|
|
385
|
+
export type { IntegrationDef, IntegrationSchemaDef, IntegrationEventDef, IntegrationFunctionDef, ValidationResult, SchemaType, ParamMeta, ParamIn, ParamUiType, SyncMappingDef, SyncMappingFieldDef, WebhookConfig, WebhookTypeDef, WebhookFieldDef, ConfigFieldDef, OptionsFetcher, OptionsContext, } from "./integration.js";
|
|
367
386
|
export { parseIntegrationSpec, IntegrationSpecSchema } from "./integrationSpec.js";
|
|
368
387
|
export type { IntegrationSpec } from "./integrationSpec.js";
|
|
369
388
|
export type WebhookCapability = {
|
package/dist/index.js
CHANGED
|
@@ -65,10 +65,12 @@ export function createORPCRouter(def) {
|
|
|
65
65
|
const credCtx = buildCredentialCtx(def.name);
|
|
66
66
|
const ctx = {
|
|
67
67
|
jobId: context?.jobId || `orpc-${Date.now()}`,
|
|
68
|
+
config: credCtx._config,
|
|
68
69
|
progress: (percent, meta) => {
|
|
69
70
|
console.log(`[${actionName}] Progress: ${percent}%`, meta);
|
|
70
71
|
},
|
|
71
|
-
|
|
72
|
+
resolveCredentials: credCtx.resolveCredentials,
|
|
73
|
+
fetch: credCtx.fetch,
|
|
72
74
|
};
|
|
73
75
|
return await actionDef.handler(input, ctx);
|
|
74
76
|
})
|
|
@@ -302,8 +304,10 @@ export async function runWorker(def) {
|
|
|
302
304
|
const credCtx = buildCredentialCtx(def.name);
|
|
303
305
|
const ctx = {
|
|
304
306
|
jobId,
|
|
307
|
+
config: credCtx._config,
|
|
305
308
|
progress: (percent, meta) => runtime.from("job_events").insert({ job_id: jobId, percent, meta }),
|
|
306
|
-
|
|
309
|
+
resolveCredentials: credCtx.resolveCredentials,
|
|
310
|
+
fetch: credCtx.fetch,
|
|
307
311
|
};
|
|
308
312
|
const result = await act.handler(parsedInput, ctx);
|
|
309
313
|
act.output.parse(result);
|
|
@@ -461,12 +465,14 @@ async function runWorkerSSE(def) {
|
|
|
461
465
|
const credCtx = buildCredentialCtx(def.name);
|
|
462
466
|
const ctx = {
|
|
463
467
|
jobId: `sse-${msg_id}`,
|
|
468
|
+
config: credCtx._config,
|
|
464
469
|
read_ct: read_ct ?? 0,
|
|
465
470
|
progress: (percent, meta) => {
|
|
466
471
|
console.log(`[nova] progress ${percent}%`, meta ?? '');
|
|
467
472
|
},
|
|
468
473
|
heartbeat: (extendBy = 30) => _heartbeatSSE(def.queue, msg_id, extendBy),
|
|
469
|
-
|
|
474
|
+
resolveCredentials: credCtx.resolveCredentials,
|
|
475
|
+
fetch: credCtx.fetch,
|
|
470
476
|
};
|
|
471
477
|
const result = await actionDef.handler(parsedInput, ctx);
|
|
472
478
|
actionDef.output.parse(result);
|
|
@@ -550,12 +556,20 @@ export async function generateOpenAPISpec(def) {
|
|
|
550
556
|
* within the same handler invocation.
|
|
551
557
|
* On 401, ctx.fetch() retries once with forceRefresh=true (X-Nova-Token-Invalid).
|
|
552
558
|
*
|
|
559
|
+
* Returns `_config` — a shared mutable object that gets populated with
|
|
560
|
+
* config values from the auth server response when resolveCredentials() is
|
|
561
|
+
* called. Assign `ctx.config = credCtx._config` so that config values
|
|
562
|
+
* become available to the handler after credential resolution.
|
|
563
|
+
*
|
|
553
564
|
* @param defaultSlug - Integration slug (e.g., "jira", "bamboohr")
|
|
554
565
|
* @param authToken - JWT Bearer token from the inbound request (forwarded to auth server)
|
|
555
566
|
*/
|
|
556
567
|
function buildCredentialCtx(defaultSlug, authToken) {
|
|
557
568
|
// Per-request credentials cache — avoids duplicate round-trips within a single handler
|
|
558
569
|
let _lastCreds = null;
|
|
570
|
+
// Shared config ref — populated when resolveCredentials() returns config
|
|
571
|
+
// from the auth server's credential resolution response.
|
|
572
|
+
const _config = {};
|
|
559
573
|
const strategy = detectCredentialStrategy();
|
|
560
574
|
const ctxResolveCredentials = async (slug, userId) => {
|
|
561
575
|
const targetSlug = slug ?? defaultSlug;
|
|
@@ -569,6 +583,13 @@ function buildCredentialCtx(defaultSlug, authToken) {
|
|
|
569
583
|
}
|
|
570
584
|
const creds = await _resolveCredentialsViaHttp(authBaseUrl, targetSlug, authToken, userId);
|
|
571
585
|
_lastCreds = creds;
|
|
586
|
+
// ── Populate shared config ref from credential resolution response ──
|
|
587
|
+
// The auth server includes config + configComplete in its response.
|
|
588
|
+
// We merge into _config (mutates in place) so ctx.config is updated.
|
|
589
|
+
if (creds.config && Object.keys(creds.config).length > 0) {
|
|
590
|
+
Object.assign(_config, creds.config);
|
|
591
|
+
console.log(`[nova-sdk] 📋 ctx.config populated with ${Object.keys(creds.config).length} field(s) from credential resolution`);
|
|
592
|
+
}
|
|
572
593
|
return creds;
|
|
573
594
|
}
|
|
574
595
|
// strategy === "none"
|
|
@@ -612,7 +633,7 @@ function buildCredentialCtx(defaultSlug, authToken) {
|
|
|
612
633
|
}
|
|
613
634
|
return response;
|
|
614
635
|
};
|
|
615
|
-
return { resolveCredentials: ctxResolveCredentials, fetch: ctxFetch };
|
|
636
|
+
return { resolveCredentials: ctxResolveCredentials, fetch: ctxFetch, _config };
|
|
616
637
|
}
|
|
617
638
|
/*──────────────── HTTP Server Harness (Enhanced) ───────────────*/
|
|
618
639
|
import express from "express";
|
|
@@ -788,13 +809,15 @@ export function runHttpServer(def, opts = {}) {
|
|
|
788
809
|
const credCtx = buildCredentialCtx(def.name, authToken);
|
|
789
810
|
const ctx = {
|
|
790
811
|
jobId,
|
|
812
|
+
config: credCtx._config, // populated from credential resolution when resolveCredentials() is called
|
|
791
813
|
progress: (percent, meta) => {
|
|
792
814
|
console.log(`[nova][${actionName}] Progress: ${percent}%`, meta ?? '');
|
|
793
815
|
},
|
|
794
816
|
headers: req.headers,
|
|
795
817
|
authToken,
|
|
796
818
|
auth: jwtPayload,
|
|
797
|
-
|
|
819
|
+
resolveCredentials: credCtx.resolveCredentials,
|
|
820
|
+
fetch: credCtx.fetch,
|
|
798
821
|
};
|
|
799
822
|
const authLabel = jwtPayload?.sub
|
|
800
823
|
? ` [auth ✓ sub=${jwtPayload.sub}]`
|
|
@@ -847,6 +870,59 @@ export function runHttpServer(def, opts = {}) {
|
|
|
847
870
|
app.get('/health', handler);
|
|
848
871
|
}
|
|
849
872
|
}
|
|
873
|
+
// ── Auto-register optionsFetcher routes for configFields ──
|
|
874
|
+
// When an integration has configFields with optionsFetcher handlers,
|
|
875
|
+
// the SDK automatically registers POST /config-options/:fieldKey routes.
|
|
876
|
+
// This is transparent to the integration author — they just define the handler.
|
|
877
|
+
const integrationDef = def;
|
|
878
|
+
if (integrationDef.configFields) {
|
|
879
|
+
for (const field of integrationDef.configFields) {
|
|
880
|
+
if (field.optionsFetcher) {
|
|
881
|
+
const optionsRoute = `/config-options/${field.key}`;
|
|
882
|
+
app.post(optionsRoute, async (req, res) => {
|
|
883
|
+
try {
|
|
884
|
+
const authHeader = req.headers['authorization'];
|
|
885
|
+
const authToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : undefined;
|
|
886
|
+
if (!authToken) {
|
|
887
|
+
return res.status(401).json({
|
|
888
|
+
error: 'Authorization required',
|
|
889
|
+
message: 'optionsFetcher requires a valid Bearer token',
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
// Resolve credentials (same flow as action handlers)
|
|
893
|
+
const credCtx = buildCredentialCtx(def.name, authToken);
|
|
894
|
+
const credentials = await credCtx.resolveCredentials();
|
|
895
|
+
// Build OptionsContext for the handler
|
|
896
|
+
const optionsCtx = {
|
|
897
|
+
fetch: credCtx.fetch,
|
|
898
|
+
config: req.body?.config ?? {},
|
|
899
|
+
credentials,
|
|
900
|
+
tenantId: req.auth?.sub ?? 'unknown',
|
|
901
|
+
};
|
|
902
|
+
// Run the optionsFetcher handler
|
|
903
|
+
console.log(`[nova] 🔧 Running optionsFetcher for config field "${field.key}"`);
|
|
904
|
+
const rawOptions = await field.optionsFetcher.handler(optionsCtx);
|
|
905
|
+
// Validate each option against the declared schema
|
|
906
|
+
const validated = rawOptions.map((item, i) => {
|
|
907
|
+
const result = field.optionsFetcher.schema.safeParse(item);
|
|
908
|
+
if (!result.success) {
|
|
909
|
+
throw new Error(`optionsFetcher "${field.key}" returned invalid item at index ${i}: ` +
|
|
910
|
+
JSON.stringify(result.error.issues ?? result.error.message));
|
|
911
|
+
}
|
|
912
|
+
return result.data;
|
|
913
|
+
});
|
|
914
|
+
console.log(`[nova] ✅ optionsFetcher "${field.key}" returned ${validated.length} options`);
|
|
915
|
+
res.json({ options: validated });
|
|
916
|
+
}
|
|
917
|
+
catch (err) {
|
|
918
|
+
console.error(`[nova] ❌ optionsFetcher "${field.key}" failed:`, err.message);
|
|
919
|
+
res.status(500).json({ error: err.message });
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
console.log(`[nova] 🔧 POST ${optionsRoute} -> optionsFetcher("${field.key}") 🔐`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
850
926
|
// ── Log credential resolution strategy ──
|
|
851
927
|
const credStrategy = detectCredentialStrategy();
|
|
852
928
|
const strategyLabels = {
|
|
@@ -905,7 +981,9 @@ export { defineIntegration, validateIntegration,
|
|
|
905
981
|
// Verbose helpers (backward-compatible)
|
|
906
982
|
integrationSchema, integrationEvent, integrationFunction,
|
|
907
983
|
// Lean helpers (recommended)
|
|
908
|
-
schema, event, IntegrationDefSchema,
|
|
984
|
+
schema, event, IntegrationDefSchema,
|
|
985
|
+
// Config field enums + Zod schemas (Phase 1: SDK Foundation)
|
|
986
|
+
ConfigPhase, ConfigPhaseSchema, ConfigWidget, ConfigWidgetSchema, } from "./integration.js";
|
|
909
987
|
// Integration spec parsing utility
|
|
910
988
|
export { parseIntegrationSpec, IntegrationSpecSchema } from "./integrationSpec.js";
|
|
911
989
|
/*──────────────── Credential Resolution (re-exports) ───────────────*/
|
package/dist/integration.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z, type ZodTypeAny } from "zod";
|
|
2
|
+
import type { ResolvedCredentials } from "./credentials.js";
|
|
2
3
|
/** Where the parameter is sent in the HTTP request */
|
|
3
4
|
export type ParamIn = 'path' | 'query' | 'body' | 'header';
|
|
4
5
|
/** UI input widget type hint for the admin dashboard form builder */
|
|
@@ -54,6 +55,217 @@ export interface ParamMeta {
|
|
|
54
55
|
/** Group name for visual grouping in the form (e.g., "Identity", "Options") */
|
|
55
56
|
group?: string;
|
|
56
57
|
}
|
|
58
|
+
/** Zod schema for config field lifecycle phase validation */
|
|
59
|
+
export declare const ConfigPhaseSchema: z.ZodEnum<{
|
|
60
|
+
before_auth: "before_auth";
|
|
61
|
+
after_auth: "after_auth";
|
|
62
|
+
}>;
|
|
63
|
+
/** Config field lifecycle phase type — `"before_auth" | "after_auth"` */
|
|
64
|
+
export type ConfigPhase = z.infer<typeof ConfigPhaseSchema>;
|
|
65
|
+
/**
|
|
66
|
+
* Named constants for config field lifecycle phases.
|
|
67
|
+
* Use these instead of raw strings for IDE autocomplete and compile-time safety.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { ConfigPhase } from "@newhomestar/sdk";
|
|
72
|
+
*
|
|
73
|
+
* { when: ConfigPhase.BeforeAuth } // ✅ autocomplete + type-safe
|
|
74
|
+
* { when: "before_auth" } // ✅ also works (same type)
|
|
75
|
+
* { when: "beforeAuth" } // ❌ TypeScript error
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare const ConfigPhase: {
|
|
79
|
+
/** Collected before OAuth redirect (e.g., company domain needed to build auth URL) */
|
|
80
|
+
readonly BeforeAuth: "before_auth";
|
|
81
|
+
/** Collected after OAuth completes (e.g., preferences that need API access) */
|
|
82
|
+
readonly AfterAuth: "after_auth";
|
|
83
|
+
};
|
|
84
|
+
/** Zod schema for config field widget type validation */
|
|
85
|
+
export declare const ConfigWidgetSchema: z.ZodEnum<{
|
|
86
|
+
number: "number";
|
|
87
|
+
boolean: "boolean";
|
|
88
|
+
date: "date";
|
|
89
|
+
text: "text";
|
|
90
|
+
textarea: "textarea";
|
|
91
|
+
select: "select";
|
|
92
|
+
password: "password";
|
|
93
|
+
}>;
|
|
94
|
+
/** Config field widget type — UI input hint */
|
|
95
|
+
export type ConfigWidget = z.infer<typeof ConfigWidgetSchema>;
|
|
96
|
+
/**
|
|
97
|
+
* Named constants for config field widget types.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* import { ConfigWidget } from "@newhomestar/sdk";
|
|
102
|
+
*
|
|
103
|
+
* { widget: ConfigWidget.Select } // ✅
|
|
104
|
+
* { widget: "select" } // ✅ also works
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export declare const ConfigWidget: {
|
|
108
|
+
readonly Text: "text";
|
|
109
|
+
readonly Select: "select";
|
|
110
|
+
readonly Boolean: "boolean";
|
|
111
|
+
readonly Number: "number";
|
|
112
|
+
readonly Textarea: "textarea";
|
|
113
|
+
readonly Password: "password";
|
|
114
|
+
readonly Date: "date";
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Context available inside `optionsFetcher.handler`.
|
|
118
|
+
* Same authenticated environment as action handlers — token-injected fetch,
|
|
119
|
+
* previously-saved config values, and full resolved credentials.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* optionsFetcher: {
|
|
124
|
+
* schema: z.object({ label: z.string(), value: z.string() }),
|
|
125
|
+
* handler: async (ctx) => {
|
|
126
|
+
* // ctx.fetch has the OAuth token injected automatically
|
|
127
|
+
* const res = await ctx.fetch("https://api.atlassian.com/oauth/token/accessible-resources");
|
|
128
|
+
* const sites = await res.json();
|
|
129
|
+
* return sites.map(s => ({ label: s.name, value: s.id }));
|
|
130
|
+
* },
|
|
131
|
+
* }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export interface OptionsContext {
|
|
135
|
+
/** Token-injected fetch — same as ctx.fetch in action handlers */
|
|
136
|
+
fetch: (url: string, options?: {
|
|
137
|
+
method?: string;
|
|
138
|
+
headers?: Record<string, string>;
|
|
139
|
+
body?: string;
|
|
140
|
+
}, credentials?: ResolvedCredentials) => Promise<Response>;
|
|
141
|
+
/** Previously-saved config values for this connection (so field B can depend on field A) */
|
|
142
|
+
config: Record<string, unknown>;
|
|
143
|
+
/** Full resolved credentials (accessToken, refreshToken, etc.) */
|
|
144
|
+
credentials: ResolvedCredentials;
|
|
145
|
+
/** Tenant ID for calling internal Nova platform services */
|
|
146
|
+
tenantId: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A handler function that fetches select options dynamically.
|
|
150
|
+
* Runs inside the integration container (not in the UI or auth server).
|
|
151
|
+
*
|
|
152
|
+
* Can make multiple API calls, call internal services, transform data, etc.
|
|
153
|
+
* Output is validated against the provided Zod schema at runtime.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* optionsFetcher: {
|
|
158
|
+
* schema: z.object({ label: z.string(), value: z.string() }),
|
|
159
|
+
* handler: async (ctx) => {
|
|
160
|
+
* const res = await ctx.fetch("https://api.atlassian.com/oauth/token/accessible-resources");
|
|
161
|
+
* const sites = await res.json();
|
|
162
|
+
* return sites.map(s => ({ label: s.name, value: s.id }));
|
|
163
|
+
* },
|
|
164
|
+
* }
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export interface OptionsFetcher<TItem extends {
|
|
168
|
+
label: string;
|
|
169
|
+
value: string;
|
|
170
|
+
} = {
|
|
171
|
+
label: string;
|
|
172
|
+
value: string;
|
|
173
|
+
}> {
|
|
174
|
+
/** Zod schema for each returned option item — enforces shape at runtime */
|
|
175
|
+
schema: z.ZodType<TItem>;
|
|
176
|
+
/**
|
|
177
|
+
* Handler function that fetches and returns the options array.
|
|
178
|
+
* Has access to authenticated fetch, saved config, and credentials.
|
|
179
|
+
*/
|
|
180
|
+
handler: (ctx: OptionsContext) => Promise<TItem[]>;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* A single configurable field declared by an integration.
|
|
184
|
+
* Reuses the same ParamMeta rendering system used for action parameters.
|
|
185
|
+
*
|
|
186
|
+
* Values are stored per connection and available at runtime via `ctx.config`.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* import { ConfigPhase, ConfigWidget } from "@newhomestar/sdk";
|
|
191
|
+
*
|
|
192
|
+
* configFields: [
|
|
193
|
+
* {
|
|
194
|
+
* key: "companyDomain",
|
|
195
|
+
* label: "Company Domain",
|
|
196
|
+
* schema: z.string().min(1).regex(/^[a-z0-9-]+$/i),
|
|
197
|
+
* when: ConfigPhase.BeforeAuth,
|
|
198
|
+
* required: true,
|
|
199
|
+
* helpText: "Your BambooHR subdomain (the 'acme' part of acme.bamboohr.com)",
|
|
200
|
+
* placeholder: "acme",
|
|
201
|
+
* widget: ConfigWidget.Text,
|
|
202
|
+
* },
|
|
203
|
+
* {
|
|
204
|
+
* key: "cloudId",
|
|
205
|
+
* label: "Jira Site",
|
|
206
|
+
* schema: z.string(),
|
|
207
|
+
* when: ConfigPhase.AfterAuth,
|
|
208
|
+
* widget: ConfigWidget.Select,
|
|
209
|
+
* optionsFetcher: {
|
|
210
|
+
* schema: z.object({ label: z.string(), value: z.string() }),
|
|
211
|
+
* handler: async (ctx) => {
|
|
212
|
+
* const res = await ctx.fetch("https://api.atlassian.com/...");
|
|
213
|
+
* return (await res.json()).map(s => ({ label: s.name, value: s.id }));
|
|
214
|
+
* },
|
|
215
|
+
* },
|
|
216
|
+
* },
|
|
217
|
+
* ]
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
export interface ConfigFieldDef<T extends z.ZodType = z.ZodType> {
|
|
221
|
+
/** Unique key for this field (used in ctx.config[key]) — must be a valid JS identifier */
|
|
222
|
+
key: string;
|
|
223
|
+
/** Human-readable label for the UI */
|
|
224
|
+
label: string;
|
|
225
|
+
/** Zod schema for validation (converted to JSON Schema at build time) */
|
|
226
|
+
schema: T;
|
|
227
|
+
/** When to collect this field in the connect flow */
|
|
228
|
+
when: ConfigPhase;
|
|
229
|
+
/** Whether this field is required (default: true) */
|
|
230
|
+
required?: boolean;
|
|
231
|
+
/** Default value if not provided by user */
|
|
232
|
+
defaultValue?: z.infer<T>;
|
|
233
|
+
/** Help text shown below the field in the UI */
|
|
234
|
+
helpText?: string;
|
|
235
|
+
/** Placeholder text for input fields */
|
|
236
|
+
placeholder?: string;
|
|
237
|
+
/** UI widget type hint (reuses ParamMeta widgets) */
|
|
238
|
+
widget?: ConfigWidget;
|
|
239
|
+
/** Static options for select widgets */
|
|
240
|
+
options?: Array<{
|
|
241
|
+
label: string;
|
|
242
|
+
value: string;
|
|
243
|
+
}>;
|
|
244
|
+
/**
|
|
245
|
+
* Dynamic options handler — fetches options from APIs at runtime.
|
|
246
|
+
* Runs inside the integration container with full auth context.
|
|
247
|
+
* Only valid for `when: ConfigPhase.AfterAuth` (needs credentials).
|
|
248
|
+
*
|
|
249
|
+
* The SDK automatically registers a `POST /config-options/:key` route
|
|
250
|
+
* for each field with an optionsFetcher — transparent to the integration author.
|
|
251
|
+
*/
|
|
252
|
+
optionsFetcher?: OptionsFetcher;
|
|
253
|
+
/** Group label for organizing related fields in the UI */
|
|
254
|
+
group?: string;
|
|
255
|
+
/**
|
|
256
|
+
* Conditional visibility: only show this field when another field
|
|
257
|
+
* matches a specific value. Enables progressive disclosure.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* { visibleWhen: { field: "syncTimeOff", equals: true } }
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
visibleWhen?: {
|
|
265
|
+
field: string;
|
|
266
|
+
equals: unknown;
|
|
267
|
+
};
|
|
268
|
+
}
|
|
57
269
|
export type SchemaType = 'request' | 'response' | 'entity' | 'webhook_payload' | 'configuration';
|
|
58
270
|
export interface IntegrationSchemaDef<T extends ZodTypeAny = ZodTypeAny> {
|
|
59
271
|
/** Human-readable name for this schema */
|
|
@@ -426,6 +638,44 @@ export interface IntegrationDef {
|
|
|
426
638
|
* The integration's handler action does the actual processing.
|
|
427
639
|
*/
|
|
428
640
|
webhooks?: WebhookConfig;
|
|
641
|
+
/**
|
|
642
|
+
* Configuration fields that users fill in during the connect flow.
|
|
643
|
+
* Values are stored per connection and available at runtime via `ctx.config`.
|
|
644
|
+
*
|
|
645
|
+
* Fields are split into two lifecycle phases:
|
|
646
|
+
* - `ConfigPhase.BeforeAuth` — collected before OAuth (e.g., company domain for auth URL)
|
|
647
|
+
* - `ConfigPhase.AfterAuth` — collected after OAuth (e.g., sync preferences, dynamic selects)
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* ```ts
|
|
651
|
+
* import { ConfigPhase } from "@newhomestar/sdk";
|
|
652
|
+
*
|
|
653
|
+
* configFields: [
|
|
654
|
+
* {
|
|
655
|
+
* key: "companyDomain",
|
|
656
|
+
* label: "Company Domain",
|
|
657
|
+
* schema: z.string().min(1),
|
|
658
|
+
* when: ConfigPhase.BeforeAuth,
|
|
659
|
+
* helpText: "Your subdomain (e.g., 'acme' from acme.bamboohr.com)",
|
|
660
|
+
* widget: "text",
|
|
661
|
+
* },
|
|
662
|
+
* {
|
|
663
|
+
* key: "syncInterval",
|
|
664
|
+
* label: "Sync Interval",
|
|
665
|
+
* schema: z.enum(["1h", "6h", "24h"]),
|
|
666
|
+
* when: ConfigPhase.AfterAuth,
|
|
667
|
+
* defaultValue: "6h",
|
|
668
|
+
* widget: "select",
|
|
669
|
+
* options: [
|
|
670
|
+
* { label: "Every hour", value: "1h" },
|
|
671
|
+
* { label: "Every 6 hours", value: "6h" },
|
|
672
|
+
* { label: "Every 24 hours", value: "24h" },
|
|
673
|
+
* ],
|
|
674
|
+
* },
|
|
675
|
+
* ]
|
|
676
|
+
* ```
|
|
677
|
+
*/
|
|
678
|
+
configFields?: ConfigFieldDef[];
|
|
429
679
|
}
|
|
430
680
|
export declare const IntegrationDefSchema: z.ZodObject<{
|
|
431
681
|
slug: z.ZodString;
|
|
@@ -570,6 +820,38 @@ export declare const IntegrationDefSchema: z.ZodObject<{
|
|
|
570
820
|
}, z.core.$strip>>>;
|
|
571
821
|
}, z.core.$strip>>;
|
|
572
822
|
}, z.core.$strip>>;
|
|
823
|
+
configFields: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
824
|
+
key: z.ZodString;
|
|
825
|
+
label: z.ZodString;
|
|
826
|
+
schema: z.ZodAny;
|
|
827
|
+
when: z.ZodEnum<{
|
|
828
|
+
before_auth: "before_auth";
|
|
829
|
+
after_auth: "after_auth";
|
|
830
|
+
}>;
|
|
831
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
832
|
+
defaultValue: z.ZodOptional<z.ZodUnknown>;
|
|
833
|
+
helpText: z.ZodOptional<z.ZodString>;
|
|
834
|
+
placeholder: z.ZodOptional<z.ZodString>;
|
|
835
|
+
widget: z.ZodOptional<z.ZodEnum<{
|
|
836
|
+
number: "number";
|
|
837
|
+
boolean: "boolean";
|
|
838
|
+
date: "date";
|
|
839
|
+
text: "text";
|
|
840
|
+
textarea: "textarea";
|
|
841
|
+
select: "select";
|
|
842
|
+
password: "password";
|
|
843
|
+
}>>;
|
|
844
|
+
options: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
845
|
+
label: z.ZodString;
|
|
846
|
+
value: z.ZodString;
|
|
847
|
+
}, z.core.$strip>>>;
|
|
848
|
+
optionsFetcher: z.ZodOptional<z.ZodAny>;
|
|
849
|
+
group: z.ZodOptional<z.ZodString>;
|
|
850
|
+
visibleWhen: z.ZodOptional<z.ZodObject<{
|
|
851
|
+
field: z.ZodString;
|
|
852
|
+
equals: z.ZodUnknown;
|
|
853
|
+
}, z.core.$strip>>;
|
|
854
|
+
}, z.core.$strip>>>;
|
|
573
855
|
}, z.core.$strip>;
|
|
574
856
|
/**
|
|
575
857
|
* Result of validating an integration definition before build/push.
|