@growthub/cli 0.14.4 → 0.14.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/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/resolvers/[integrationId]/route.js +157 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/resolvers/route.js +86 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +30 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +400 -188
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +3 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +24 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +7 -82
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/resolver-constructor.js +217 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-registry.js +99 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/unified-resolver-registry.js +545 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +30 -2
- package/package.json +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +0 -141
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +0 -64
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +0 -376
|
@@ -16,7 +16,7 @@ export function OrchestrationGraphEmptyCanvas({
|
|
|
16
16
|
<div className="dm-orchestration-canvas dm-orchestration-canvas--empty-state" aria-label="Empty orchestration graph">
|
|
17
17
|
<div className="dm-orchestration-canvas__empty-card">
|
|
18
18
|
<h3>Start orchestration graph</h3>
|
|
19
|
-
<p>Create a governed
|
|
19
|
+
<p>Create a governed workflow plan. Nothing executes until you run the workflow.</p>
|
|
20
20
|
<div className="dm-orchestration-canvas__empty-actions">
|
|
21
21
|
<button type="button" className="dm-btn-primary-sm" disabled={disabled} onClick={onStartFromRegistry}>
|
|
22
22
|
Start from API Registry
|
|
@@ -531,7 +531,7 @@ export function OrchestrationRunTracePanel({
|
|
|
531
531
|
Runs <span aria-hidden="true">/</span> <code>{activeConsoleRecord?.runId || "preview"}</code>
|
|
532
532
|
</span>
|
|
533
533
|
<h2>Run console</h2>
|
|
534
|
-
<p>{summaryText} · {row?.Name || "
|
|
534
|
+
<p>{summaryText} · {row?.Name || "Workflow"}</p>
|
|
535
535
|
</div>
|
|
536
536
|
<div className="dm-run-console__head-actions">
|
|
537
537
|
{canReplay && (
|
|
@@ -131,7 +131,7 @@ export function SandboxOrchestrationEditorPanel({
|
|
|
131
131
|
</button>
|
|
132
132
|
<div className="dm-orchestration-header__titles">
|
|
133
133
|
<h2>Orchestration graph</h2>
|
|
134
|
-
<p>{sandboxRow?.Name || "
|
|
134
|
+
<p>{sandboxRow?.Name || "Workflow"}</p>
|
|
135
135
|
</div>
|
|
136
136
|
</header>
|
|
137
137
|
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css
CHANGED
|
@@ -5046,6 +5046,9 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
5046
5046
|
}
|
|
5047
5047
|
.dm-api-action-card-success { grid-template-columns: 1fr auto; }
|
|
5048
5048
|
.dm-api-action-card-muted { grid-template-columns: 1fr; background: #fafafa; }
|
|
5049
|
+
.dm-api-action-card-workflow { grid-template-columns: 1fr; }
|
|
5050
|
+
.dm-api-action-card-workflow .dm-api-action-card-actions { align-items: flex-start; }
|
|
5051
|
+
.dm-api-action-card-workflow .dm-api-action-card-cta { align-self: flex-start; }
|
|
5049
5052
|
.dm-api-action-card-note { font-size: 11px; color: #6b7280; margin-top: 4px; }
|
|
5050
5053
|
.dm-api-action-checklist { margin: 8px 0 0; padding: 0; list-style: none; display: grid; gap: 6px; }
|
|
5051
5054
|
.dm-api-action-checklist li { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #374151; }
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* row being edited, the source-records sidecar, and the safe runtime signal, it
|
|
7
7
|
* resolves the full operator journey for THIS API as an ordered list of steps:
|
|
8
8
|
*
|
|
9
|
-
* register → configure auth → test → (resolver) →
|
|
10
|
-
*
|
|
9
|
+
* register → configure auth → test → (resolver) → data source → refresh
|
|
10
|
+
* records, then a separate workflow canvas CTA after activation.
|
|
11
11
|
*
|
|
12
12
|
* Each step carries a status (complete | active | pending | blocked | optional),
|
|
13
13
|
* a human description, and — when the operator can act — an `action` descriptor
|
|
14
14
|
* the drawer maps to an existing handler (test / create-data-source /
|
|
15
|
-
*
|
|
15
|
+
* open-data-source / refresh-source / create-workflow-canvas). The cockpit renders
|
|
16
16
|
* this verbatim, so the journey is one derivation, not UI guesswork.
|
|
17
17
|
*
|
|
18
18
|
* Invariants:
|
|
@@ -78,14 +78,18 @@ function sandboxRowsForIntegration(workspaceConfig, integrationId) {
|
|
|
78
78
|
const rows = [];
|
|
79
79
|
for (const object of findObjectsByType(workspaceConfig, "sandbox-environment")) {
|
|
80
80
|
for (const row of Array.isArray(object.rows) ? object.rows : []) {
|
|
81
|
-
const envRefs = clean(row?.envRefs);
|
|
82
81
|
const schedulerId = clean(row?.schedulerRegistryId);
|
|
83
|
-
const cfg = parseMaybeJson(
|
|
82
|
+
const cfg = parseMaybeJson(
|
|
83
|
+
row?.orchestrationConfig
|
|
84
|
+
|| row?.orchestrationGraph
|
|
85
|
+
|| row?.orchestrationDraftConfig
|
|
86
|
+
|| row?.orchestrationDraftGraph
|
|
87
|
+
);
|
|
84
88
|
const callsApi = Array.isArray(cfg?.nodes) && cfg.nodes.some(
|
|
85
89
|
(n) => n?.type === "api-registry-call"
|
|
86
90
|
&& clean(n?.config?.registryId || n?.config?.integrationId) === id,
|
|
87
91
|
);
|
|
88
|
-
if (callsApi || schedulerId === id
|
|
92
|
+
if (callsApi || schedulerId === id) {
|
|
89
93
|
rows.push(row);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
@@ -213,8 +217,11 @@ function deriveApiRegistryCreationState(input = {}) {
|
|
|
213
217
|
status: resolverWired ? "complete" : (tested ? "optional" : "blocked"),
|
|
214
218
|
description: resolverWired
|
|
215
219
|
? `Resolver "${resolverTemplate}" shapes the response into rows.`
|
|
216
|
-
: "Optional:
|
|
217
|
-
|
|
220
|
+
: "Optional: construct a resolver to normalize the response into governed rows. Raw passthrough works without one.",
|
|
221
|
+
// CMS SDK v1.5.1 — the action constructs the governed resolver from the
|
|
222
|
+
// tested response shape (no blank form). The cockpit stages it for one-screen
|
|
223
|
+
// review, applies through the governed lane, and re-tests.
|
|
224
|
+
action: tested && !resolverWired ? { id: "construct-resolver", label: "Construct resolver" } : null,
|
|
218
225
|
});
|
|
219
226
|
|
|
220
227
|
step({
|
|
@@ -245,17 +252,6 @@ function deriveApiRegistryCreationState(input = {}) {
|
|
|
245
252
|
: null,
|
|
246
253
|
});
|
|
247
254
|
|
|
248
|
-
// Optional automation lane — a sandbox/workflow that calls this API.
|
|
249
|
-
step({
|
|
250
|
-
id: "sandbox-tool",
|
|
251
|
-
label: "Automate (sandbox tool)",
|
|
252
|
-
status: sandboxExists ? "complete" : (tested ? "optional" : "blocked"),
|
|
253
|
-
description: sandboxExists
|
|
254
|
-
? "A sandbox tool calls this API."
|
|
255
|
-
: "Optional: wrap this API in a sandbox/workflow you can run or schedule.",
|
|
256
|
-
action: tested && !sandboxExists ? { id: "create-sandbox-tool", label: "Create sandbox tool" } : null,
|
|
257
|
-
});
|
|
258
|
-
|
|
259
255
|
for (const s of steps) { if (!s.hint) delete s.hint; }
|
|
260
256
|
|
|
261
257
|
const required = steps.filter((s) => s.status !== "optional");
|
|
@@ -297,6 +293,15 @@ function deriveApiRegistryCreationState(input = {}) {
|
|
|
297
293
|
score,
|
|
298
294
|
nextStepId: nextStep ? nextStep.id : null,
|
|
299
295
|
nextAction: nextStep && nextStep.action ? { stepId: nextStep.id, ...nextStep.action } : null,
|
|
296
|
+
workflowAction: tested
|
|
297
|
+
? {
|
|
298
|
+
id: sandboxExists ? "open-workflow-canvas" : "create-workflow-canvas",
|
|
299
|
+
label: sandboxExists ? "Open workflow canvas" : "Create workflow",
|
|
300
|
+
description: sandboxExists
|
|
301
|
+
? "Open the workflow canvas that uses this API Registry node."
|
|
302
|
+
: "Open a workflow canvas with this API Registry call already drafted.",
|
|
303
|
+
}
|
|
304
|
+
: null,
|
|
300
305
|
headline: !registered
|
|
301
306
|
? "Register this API to begin."
|
|
302
307
|
: complete
|
|
@@ -28,7 +28,7 @@ const KNOWN_NODE_TYPES = new Set([
|
|
|
28
28
|
|
|
29
29
|
const API_REGISTRY_SETUP_FIELDS = ["integrationId", "baseUrl", "endpoint", "method", "authRef"];
|
|
30
30
|
|
|
31
|
-
const CANONICAL_NODE_ORDER = ["input", "api-request", "transform", "result"];
|
|
31
|
+
const CANONICAL_NODE_ORDER = ["human-input", "input", "api-request", "transform", "result"];
|
|
32
32
|
|
|
33
33
|
function slugifyName(value) {
|
|
34
34
|
return String(value || "")
|
|
@@ -91,34 +91,6 @@ function isApiRegistrySetupComplete(registryRow) {
|
|
|
91
91
|
return getApiRegistrySetupChecklist(registryRow).every((item) => item.ok);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/**
|
|
95
|
-
* Sidecar action state for API Registry → sandbox tool bridge (UI only).
|
|
96
|
-
*/
|
|
97
|
-
function getApiRegistrySandboxToolState(registryRow, workspaceConfig) {
|
|
98
|
-
if (!isApiRegistrySetupComplete(registryRow)) {
|
|
99
|
-
return { kind: "incomplete", checklist: getApiRegistrySetupChecklist(registryRow) };
|
|
100
|
-
}
|
|
101
|
-
if (!isApiRegistryTestSuccessful(registryRow)) {
|
|
102
|
-
const status = String(registryRow?.status || "").trim().toLowerCase();
|
|
103
|
-
if (status === "failed") {
|
|
104
|
-
return {
|
|
105
|
-
kind: "failed",
|
|
106
|
-
message: "Connection test failed. Fix the endpoint or auth reference, then test again."
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
kind: "untested",
|
|
111
|
-
message: "Test connection first. Sandbox tool creation unlocks after a successful test."
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
const integrationId = String(registryRow?.integrationId || "").trim();
|
|
115
|
-
const existing = findSandboxRowsForRegistry(workspaceConfig, integrationId);
|
|
116
|
-
if (existing.length > 0) {
|
|
117
|
-
return { kind: "existing", row: existing[0] };
|
|
118
|
-
}
|
|
119
|
-
return { kind: "create" };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
94
|
function validateOrchestrationGraph(graph) {
|
|
123
95
|
const errors = [];
|
|
124
96
|
if (!graph || typeof graph !== "object") {
|
|
@@ -306,7 +278,12 @@ function findSandboxRowsForRegistry(workspaceConfig, integrationId) {
|
|
|
306
278
|
if (!sandboxObject) return [];
|
|
307
279
|
const rows = Array.isArray(sandboxObject.rows) ? sandboxObject.rows : [];
|
|
308
280
|
return rows.filter((row) => {
|
|
309
|
-
const graph = parseOrchestrationGraph(
|
|
281
|
+
const graph = parseOrchestrationGraph(
|
|
282
|
+
row?.orchestrationConfig
|
|
283
|
+
|| row?.orchestrationGraph
|
|
284
|
+
|| row?.orchestrationDraftConfig
|
|
285
|
+
|| row?.orchestrationDraftGraph
|
|
286
|
+
);
|
|
310
287
|
if (!graph?.nodes) return String(row?.schedulerRegistryId || "").trim() === id;
|
|
311
288
|
return graph.nodes.some(
|
|
312
289
|
(node) => node?.type === "api-registry-call"
|
|
@@ -315,56 +292,6 @@ function findSandboxRowsForRegistry(workspaceConfig, integrationId) {
|
|
|
315
292
|
});
|
|
316
293
|
}
|
|
317
294
|
|
|
318
|
-
function buildSandboxRowFromApiRegistry(workspaceConfig, registryRow, options = {}) {
|
|
319
|
-
const integrationId = String(registryRow?.integrationId || "").trim();
|
|
320
|
-
const baseName = String(options.name || registryRow?.Name || integrationId || "Sandbox Tool").trim();
|
|
321
|
-
const name = baseName.endsWith(" Tool") ? baseName : `${baseName} Tool`;
|
|
322
|
-
const runLocality = String(options.runLocality || "local").trim() === "serverless" ? "serverless" : "local";
|
|
323
|
-
const adapter = String(options.adapter || (runLocality === "serverless" ? "serverless" : "local-process")).trim();
|
|
324
|
-
const graph = options.orchestrationGraph
|
|
325
|
-
? (typeof options.orchestrationGraph === "string"
|
|
326
|
-
? parseOrchestrationGraph(options.orchestrationGraph)
|
|
327
|
-
: options.orchestrationGraph)
|
|
328
|
-
: buildDefaultOrchestrationGraphFromRegistry(registryRow, options);
|
|
329
|
-
|
|
330
|
-
const apiNode = graph?.nodes?.find((n) => n?.type === "api-registry-call");
|
|
331
|
-
const authRef = String(options.authRef || apiNode?.config?.authRef || registryRow?.authRef || integrationId).trim();
|
|
332
|
-
const transformNode = graph?.nodes?.find((n) => n?.type === "transform-filter" || n?.type === "normalize-output");
|
|
333
|
-
const rootPath = transformNode?.config?.rootPath || "data";
|
|
334
|
-
const method = String(registryRow?.method || apiNode?.config?.method || "GET").trim().toUpperCase();
|
|
335
|
-
const endpoint = String(registryRow?.endpoint || apiNode?.config?.endpoint || "").trim();
|
|
336
|
-
const baseUrl = String(registryRow?.baseUrl || apiNode?.config?.baseUrl || "").trim();
|
|
337
|
-
|
|
338
|
-
return {
|
|
339
|
-
Name: name,
|
|
340
|
-
slug: options.slug || slugifyName(name) || slugifyName(integrationId),
|
|
341
|
-
objectType: "sandbox-environment",
|
|
342
|
-
lifecycleStatus: "draft",
|
|
343
|
-
version: "1",
|
|
344
|
-
runLocality,
|
|
345
|
-
schedulerRegistryId: runLocality === "serverless" ? integrationId : "",
|
|
346
|
-
runtime: String(options.runtime || "node").trim(),
|
|
347
|
-
adapter,
|
|
348
|
-
agentHost: String(options.agentHost || "").trim(),
|
|
349
|
-
envRefs: Array.isArray(options.envRefs) ? options.envRefs.join(",") : String(options.envRefs || "").trim(),
|
|
350
|
-
networkAllow: options.networkAllow === true ? "true" : "",
|
|
351
|
-
allowList: String(options.allowList || "").trim(),
|
|
352
|
-
instructions: String(
|
|
353
|
-
options.instructions
|
|
354
|
-
|| `Governed sandbox tool for ${integrationId}. Calls ${method} ${endpoint || baseUrl} and normalizes at "${rootPath}". authRef ${authRef} only — secrets resolve server-side.`
|
|
355
|
-
).trim(),
|
|
356
|
-
command: String(options.command || "").trim(),
|
|
357
|
-
timeoutMs: String(options.timeoutMs || "30000").trim(),
|
|
358
|
-
status: "untested",
|
|
359
|
-
lastTested: "",
|
|
360
|
-
lastRunId: "",
|
|
361
|
-
lastSourceId: "",
|
|
362
|
-
lastResponse: "",
|
|
363
|
-
orchestrationConfig: serializeOrchestrationGraph(graph),
|
|
364
|
-
description: String(options.description || registryRow?.description || "").trim()
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
295
|
/**
|
|
369
296
|
* Find existing data-source rows that already resolve through a given API
|
|
370
297
|
* Registry integration (by `registryId`). Mirrors findSandboxRowsForRegistry so
|
|
@@ -986,7 +913,6 @@ export {
|
|
|
986
913
|
getOrchestrationGraphUiState,
|
|
987
914
|
getNextCanonicalNodeId,
|
|
988
915
|
addCanonicalNodeToGraph,
|
|
989
|
-
buildSandboxRowFromApiRegistry,
|
|
990
916
|
buildDataSourceRowFromApiRegistry,
|
|
991
917
|
findDataSourceRowsForRegistry,
|
|
992
918
|
extractApiRegistryCallNode,
|
|
@@ -996,7 +922,6 @@ export {
|
|
|
996
922
|
findSandboxObject,
|
|
997
923
|
findSandboxRowsForRegistry,
|
|
998
924
|
getApiRegistrySetupChecklist,
|
|
999
|
-
getApiRegistrySandboxToolState,
|
|
1000
925
|
isApiRegistrySetupComplete,
|
|
1001
926
|
isApiRegistryTestSuccessful,
|
|
1002
927
|
normalizeJsonAtPath,
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver Constructor V1 (CMS SDK v1.5.1) — closes the no-code "too many open
|
|
3
|
+
* fields" gap. Instead of asking a non-technical user to hand-author a
|
|
4
|
+
* resolver's rootPath / idField / entityType / auth header, this constructs the
|
|
5
|
+
* governed resolver from facts ALREADY computed: the tested response profile
|
|
6
|
+
* (`profileApiResponse`) and the resolver recommendation (`recommendResolver`),
|
|
7
|
+
* plus the row's own auth config (mirrored from how test-api-record sent it, so
|
|
8
|
+
* the resolver behaves exactly like the test that just passed).
|
|
9
|
+
*
|
|
10
|
+
* Agnostic across the normalized governance taxonomy (http | custom | tool | mcp
|
|
11
|
+
* | chrome | nango), via a single builder dispatch (`getResolverBuilder`), the
|
|
12
|
+
* Nango precedent generalized:
|
|
13
|
+
* - HTTP-shaped kinds (http / custom / and any non-reserved value, including a
|
|
14
|
+
* literal "webhook" an operator may type) materialize a server file built
|
|
15
|
+
* from the response shape;
|
|
16
|
+
* - nango is config-driven (no file) with honest readiness;
|
|
17
|
+
* - reserved kinds (mcp | chrome | tool) cannot be auto-constructed from an
|
|
18
|
+
* HTTP response — advertised truthfully with a concrete next action, never
|
|
19
|
+
* left blank or mislabeled.
|
|
20
|
+
*
|
|
21
|
+
* `connectorKind` is operator-editable text and is honored verbatim — it is
|
|
22
|
+
* never silently normalized.
|
|
23
|
+
*
|
|
24
|
+
* Pure: no fetch, no secrets, never throws. The returned `proposal` (file mode)
|
|
25
|
+
* flows ONLY through the governed apply lane (helper/apply → writeResolverProposalFile)
|
|
26
|
+
* and the no-code cockpit — never a hand edit.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { buildResolverProposal } from "./workspace-resolver-proposal.js";
|
|
30
|
+
import { slugifyIntegrationId, RESOLVER_ENDPOINT_BASE } from "./unified-resolver-registry.js";
|
|
31
|
+
|
|
32
|
+
function clean(value) {
|
|
33
|
+
return String(value == null ? "" : value).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The canonical governed endpoint a row will be exposed at once registered. */
|
|
37
|
+
function endpointFor(integrationId) {
|
|
38
|
+
const slug = slugifyIntegrationId(integrationId, "");
|
|
39
|
+
return slug ? `${RESOLVER_ENDPOINT_BASE}/${slug}` : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A plain-language "what the system detected" summary + a confidence band, so the
|
|
44
|
+
* review panel can SHOW understanding ("I found 42 records under data.items…")
|
|
45
|
+
* instead of a developer form. Pure; derived from the tested response profile.
|
|
46
|
+
* high — top-level/clean records: direct apply is safe.
|
|
47
|
+
* medium — records nested under a container: review the mapping, then apply.
|
|
48
|
+
* low — pagination or no record array: needs human review before trust.
|
|
49
|
+
*/
|
|
50
|
+
function detectShape(profile, recommendation) {
|
|
51
|
+
if (!profile || !profile.parsed) return null;
|
|
52
|
+
const level = clean(recommendation?.level);
|
|
53
|
+
let confidence = "high";
|
|
54
|
+
if (profile.hasPagination || !profile.usable) confidence = "low";
|
|
55
|
+
else if (level === "recommended") confidence = "medium";
|
|
56
|
+
else if (level === "required") confidence = "low";
|
|
57
|
+
const recordPath = clean(profile.arrayPath);
|
|
58
|
+
const entityType = clean(profile.suggestedEntityType) || "records";
|
|
59
|
+
const idField = clean(profile.candidates?.id) || "id";
|
|
60
|
+
return {
|
|
61
|
+
confidence,
|
|
62
|
+
recordCount: Number.isFinite(profile.recordCount) ? profile.recordCount : 0,
|
|
63
|
+
recordPath,
|
|
64
|
+
idField,
|
|
65
|
+
entityType,
|
|
66
|
+
hasPagination: Boolean(profile.hasPagination),
|
|
67
|
+
// One human sentence the panel can show verbatim.
|
|
68
|
+
sentence: profile.usable
|
|
69
|
+
? `Found ${profile.recordCount} ${entityType}${recordPath ? ` under "${recordPath}"` : " (top-level)"}, keyed by "${idField}"${profile.hasPagination ? " — paginated, so a resolver is required to fetch every page" : ""}.`
|
|
70
|
+
: "No record array detected — review the response before activating.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The auth header/prefix the resolver must send — mirrored from how
|
|
76
|
+
* test-api-record built its request (authHeaderName || authHeader || x-api-key;
|
|
77
|
+
* authPrefix), so a constructed resolver matches the test that just succeeded.
|
|
78
|
+
*/
|
|
79
|
+
function deriveAuthHeader(row) {
|
|
80
|
+
const headerName = clean(row?.authHeaderName) || clean(row?.authHeader) || "x-api-key";
|
|
81
|
+
const prefix = clean(row?.authPrefix);
|
|
82
|
+
return { headerName, prefix };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function constructCustomHttpProposal({ row, profile, recommendation, recordRef }) {
|
|
86
|
+
const { headerName, prefix } = deriveAuthHeader(row);
|
|
87
|
+
const rootPath = clean(profile?.arrayPath) || clean(recommendation?.rootPath);
|
|
88
|
+
const idField = clean(profile?.candidates?.id) || "id";
|
|
89
|
+
const entityType = clean(profile?.suggestedEntityType) || clean(row?.entityTypes) || "records";
|
|
90
|
+
|
|
91
|
+
const blanks = [];
|
|
92
|
+
if (!clean(row?.integrationId)) blanks.push("integrationId");
|
|
93
|
+
if (!clean(row?.baseUrl) && !clean(row?.endpoint)) blanks.push("target (baseUrl or endpoint)");
|
|
94
|
+
|
|
95
|
+
const proposal = buildResolverProposal({
|
|
96
|
+
integrationId: row?.integrationId,
|
|
97
|
+
baseUrl: row?.baseUrl,
|
|
98
|
+
endpoint: row?.endpoint,
|
|
99
|
+
method: row?.method,
|
|
100
|
+
authRef: row?.authRef,
|
|
101
|
+
headerName,
|
|
102
|
+
prefix,
|
|
103
|
+
rootPath,
|
|
104
|
+
idField,
|
|
105
|
+
entityType,
|
|
106
|
+
recordRef,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const detected = detectShape(profile, recommendation);
|
|
110
|
+
// Respect the row's declared governance kind (http / custom / webhook / …);
|
|
111
|
+
// default to http when unset. Only the resolver IMPLEMENTATION is HTTP here.
|
|
112
|
+
const declaredKind = clean(row?.connectorKind).toLowerCase() || "http";
|
|
113
|
+
return {
|
|
114
|
+
ok: blanks.length === 0,
|
|
115
|
+
mode: "file",
|
|
116
|
+
connectorKind: declaredKind,
|
|
117
|
+
endpoint: endpointFor(row?.integrationId),
|
|
118
|
+
proposal,
|
|
119
|
+
prefill: { rootPath, idField, entityType, headerName, prefix },
|
|
120
|
+
detected,
|
|
121
|
+
confidence: detected ? detected.confidence : "low",
|
|
122
|
+
authRef: clean(row?.authRef),
|
|
123
|
+
blanks,
|
|
124
|
+
reason: blanks.length
|
|
125
|
+
? `Fill ${blanks.join(", ")} on the row before constructing a resolver.`
|
|
126
|
+
: (recommendation?.reason || "Resolver constructed from the tested response shape."),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Honest readiness for a config-driven (Nango) row. A resolver registers from
|
|
132
|
+
* the row automatically — but only if the row actually carries the minimum
|
|
133
|
+
* binding. We never report "nothing to apply / done" when the endpoint would not
|
|
134
|
+
* be usable; missing config returns an actionable next step instead.
|
|
135
|
+
*/
|
|
136
|
+
function constructNangoReadiness({ row }) {
|
|
137
|
+
const blanks = [];
|
|
138
|
+
const providerKey = clean(row?.providerConfigKey) || clean(row?.integrationId);
|
|
139
|
+
if (!providerKey) blanks.push("providerConfigKey (or integrationId)");
|
|
140
|
+
const hasConnection =
|
|
141
|
+
clean(row?.connectionIds) || clean(row?.connectionId) || clean(row?.nangoConnectionId);
|
|
142
|
+
if (!hasConnection) blanks.push("connectionIds");
|
|
143
|
+
if (!clean(row?.endpoint)) blanks.push("endpoint (Nango proxy path)");
|
|
144
|
+
const ready = blanks.length === 0;
|
|
145
|
+
return {
|
|
146
|
+
ok: ready,
|
|
147
|
+
mode: "config-driven",
|
|
148
|
+
connectorKind: "nango",
|
|
149
|
+
endpoint: endpointFor(row?.integrationId),
|
|
150
|
+
proposal: null,
|
|
151
|
+
prefill: null,
|
|
152
|
+
detected: null,
|
|
153
|
+
blanks,
|
|
154
|
+
state: ready ? "config-driven-ready" : "config-driven-missing-config",
|
|
155
|
+
reason: ready
|
|
156
|
+
? "Config-driven via Nango — the resolver registers from this row automatically once it loads; no file to write. Confirm it appears as registered in the registry."
|
|
157
|
+
: `Config-driven via Nango, but the row is missing ${blanks.join(", ")}. Add these so the resolver can register and the endpoint becomes usable.`,
|
|
158
|
+
nextAction: ready ? null : { id: "edit", label: `Add ${blanks.join(", ")} to the row` },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolve a builder for a connector kind. Each builder emits the same
|
|
164
|
+
* `{ ok, mode, connectorKind, proposal, prefill, blanks, reason }` contract.
|
|
165
|
+
* - custom-http (default) → file mode (materialized resolver)
|
|
166
|
+
* - nango → config-driven (no file; built from the row)
|
|
167
|
+
* - mcp/webhook/chrome → not yet supported (truthful, not blank)
|
|
168
|
+
*/
|
|
169
|
+
function getResolverBuilder(connectorKind) {
|
|
170
|
+
const kind = clean(connectorKind).toLowerCase();
|
|
171
|
+
if (kind === "nango") {
|
|
172
|
+
return (args) => constructNangoReadiness(args);
|
|
173
|
+
}
|
|
174
|
+
// Reserved for auto-construction — these need their own resolver implementation
|
|
175
|
+
// and cannot be derived from an HTTP response shape (taxonomy: mcp|chrome|tool).
|
|
176
|
+
if (["mcp", "chrome", "tool"].includes(kind)) {
|
|
177
|
+
return (args) => ({
|
|
178
|
+
ok: false,
|
|
179
|
+
mode: "unsupported",
|
|
180
|
+
reserved: true,
|
|
181
|
+
connectorKind: kind,
|
|
182
|
+
endpoint: endpointFor(args?.row?.integrationId),
|
|
183
|
+
proposal: null,
|
|
184
|
+
prefill: null,
|
|
185
|
+
detected: null,
|
|
186
|
+
blanks: [],
|
|
187
|
+
// Reserved should build confidence, not feel like a dead end: say what is
|
|
188
|
+
// reserved, what works now, and the concrete next move.
|
|
189
|
+
reason: `Auto-construction for "${kind}" connectors is reserved for a future release. What works today: set this row's connector to custom-http and construct a resolver, or ask the governed helper to propose a plan. Your record stays governed either way.`,
|
|
190
|
+
nextAction: { id: "use-custom-http", label: "Switch to a custom-http resolver" },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return (args) => constructCustomHttpProposal(args);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Construct a governed resolver for one API Registry row from its tested shape.
|
|
198
|
+
*
|
|
199
|
+
* @param {object} input
|
|
200
|
+
* @param {object} input.row the api-registry row (drawer draft)
|
|
201
|
+
* @param {object} [input.profile] profileApiResponse(row.lastResponse)
|
|
202
|
+
* @param {object} [input.recommendation] recommendResolver(profile)
|
|
203
|
+
* @param {object} [input.recordRef] { objectId, rowName } of the governed record
|
|
204
|
+
* @returns {{ ok, mode, connectorKind, proposal, prefill, blanks, reason }}
|
|
205
|
+
*/
|
|
206
|
+
function constructResolverProposal(input = {}) {
|
|
207
|
+
const row = input.row && typeof input.row === "object" ? input.row : {};
|
|
208
|
+
const builder = getResolverBuilder(row.connectorKind);
|
|
209
|
+
return builder({
|
|
210
|
+
row,
|
|
211
|
+
profile: input.profile || null,
|
|
212
|
+
recommendation: input.recommendation || null,
|
|
213
|
+
recordRef: input.recordRef || null,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { constructResolverProposal, getResolverBuilder };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Resolver Registry IO V1 — the confined, server-only bridge between the
|
|
3
|
+
* pure unified-resolver-registry deriver and the filesystem.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities (server-only; the browser never imports this):
|
|
6
|
+
* - read the provenance header off each resolver file (to tag
|
|
7
|
+
* helper-generated vs static-file provenance)
|
|
8
|
+
* - persist the externalized, agent-readable index artifact and the endpoint
|
|
9
|
+
* manifest (gated by persistence mode — read-only runtimes skip silently
|
|
10
|
+
* and the live derivation is still returned over the API)
|
|
11
|
+
*
|
|
12
|
+
* The artifacts are PROJECTIONS of the governed records — do-not-edit, and kept
|
|
13
|
+
* in sync by this write-through. Never logs file contents. Contract:
|
|
14
|
+
* `@growthub/api-contract/resolver-registry`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { promises as fs } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { describePersistenceMode } from "@/lib/workspace-config";
|
|
20
|
+
import {
|
|
21
|
+
RESOLVER_REGISTRY_DIR,
|
|
22
|
+
RESOLVER_REGISTRY_INDEX_FILE,
|
|
23
|
+
RESOLVER_ENDPOINT_MANIFEST_FILE,
|
|
24
|
+
parseResolverFileHeader,
|
|
25
|
+
slugifyIntegrationId,
|
|
26
|
+
buildEndpointManifest,
|
|
27
|
+
} from "@/lib/unified-resolver-registry";
|
|
28
|
+
|
|
29
|
+
const HEADER_BYTES = 600;
|
|
30
|
+
|
|
31
|
+
function resolversDirAbs() {
|
|
32
|
+
return path.resolve(/*turbopackIgnore: true*/ process.cwd(), RESOLVER_REGISTRY_DIR);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read the provenance header of every resolver file.
|
|
37
|
+
* Returns { [slug]: { generated, integrationId, record } }. Never throws.
|
|
38
|
+
*/
|
|
39
|
+
async function readResolverFileMeta() {
|
|
40
|
+
const dir = resolversDirAbs();
|
|
41
|
+
const meta = {};
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = await fs.readdir(dir);
|
|
45
|
+
} catch {
|
|
46
|
+
return meta;
|
|
47
|
+
}
|
|
48
|
+
const jsFiles = entries.filter((f) => f.endsWith(".js") && !f.startsWith("_") && !f.startsWith("."));
|
|
49
|
+
await Promise.all(
|
|
50
|
+
jsFiles.map(async (file) => {
|
|
51
|
+
const slug = slugifyIntegrationId(file, "");
|
|
52
|
+
if (!slug) return;
|
|
53
|
+
try {
|
|
54
|
+
const handle = await fs.open(path.join(dir, file), "r");
|
|
55
|
+
try {
|
|
56
|
+
const buf = Buffer.alloc(HEADER_BYTES);
|
|
57
|
+
const { bytesRead } = await handle.read(buf, 0, HEADER_BYTES, 0);
|
|
58
|
+
meta[slug] = parseResolverFileHeader(buf.toString("utf8", 0, bytesRead));
|
|
59
|
+
} finally {
|
|
60
|
+
await handle.close();
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Unreadable file — leave it out of meta; the deriver still sees the
|
|
64
|
+
// filename via `files` and classifies it as static-file.
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
return meta;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Persist the index + endpoint manifest artifacts (gated). Returns
|
|
73
|
+
* { written: boolean, reason?: string }. Read-only runtimes are not an error —
|
|
74
|
+
* the live API derivation remains the source of truth at request time.
|
|
75
|
+
*/
|
|
76
|
+
async function persistResolverRegistryArtifacts(index) {
|
|
77
|
+
const persistence = describePersistenceMode();
|
|
78
|
+
if (!persistence.canSave) {
|
|
79
|
+
return { written: false, reason: persistence.reason || "read-only runtime" };
|
|
80
|
+
}
|
|
81
|
+
const dir = resolversDirAbs();
|
|
82
|
+
try {
|
|
83
|
+
await fs.mkdir(dir, { recursive: true });
|
|
84
|
+
const indexPath = path.resolve(/*turbopackIgnore: true*/ process.cwd(), RESOLVER_REGISTRY_INDEX_FILE);
|
|
85
|
+
const manifestPath = path.resolve(/*turbopackIgnore: true*/ process.cwd(), RESOLVER_ENDPOINT_MANIFEST_FILE);
|
|
86
|
+
// Confinement — both artifacts live directly in the resolvers dir.
|
|
87
|
+
if (path.dirname(indexPath) !== dir || path.dirname(manifestPath) !== dir) {
|
|
88
|
+
return { written: false, reason: "artifact path escaped the resolvers dir" };
|
|
89
|
+
}
|
|
90
|
+
const manifest = buildEndpointManifest(index, index.generatedAt);
|
|
91
|
+
await fs.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
92
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
93
|
+
return { written: true };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return { written: false, reason: err?.message || "artifact write failed" };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { readResolverFileMeta, persistResolverRegistryArtifacts };
|