@clue-ai/cli 0.0.15 → 0.0.17

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.
@@ -0,0 +1,91 @@
1
+ export const AI_SETUP_CONTRACT_VERSION =
2
+ "2026-05-10.api-connectivity-contract.v6";
3
+
4
+ export const SETUP_DOCTRINE = {
5
+ purpose:
6
+ "Clue setup installs an external SDK integration so observed product facts can reach Clue's Customer Value Understanding Engine. It is not an opportunity to improve, refactor, redesign, or reinterpret the host application.",
7
+ minimal_diff_reason:
8
+ "The customer must be able to review and merge the setup diff with confidence. Extra formatting, refactors, auth rewrites, UI changes, or unrelated cleanup make the integration harder to trust.",
9
+ ai_decision_boundary:
10
+ "The AI should use repository understanding only to choose existing lifecycle boundaries for ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout.",
11
+ deterministic_control_boundary:
12
+ "Everything that can be controlled mechanically should be controlled by the CLI, generated skills, documentation contract, lifecycle plan schema, and setup-check static guards.",
13
+ documentation_reason:
14
+ "SDK signatures, environment variable names, browser token behavior, and verification ownership are contracts. The AI must read the setup documents instead of relying on memory.",
15
+ failure_posture:
16
+ "When a lifecycle point, SDK package, signature, env name, or verification step is unclear, the correct output is a blocker or user_verification_pending, not a guessed implementation or a completion claim.",
17
+ };
18
+
19
+ export const DETERMINISTIC_CONTROL_MODEL = {
20
+ ai_should_decide: [
21
+ "which existing bootstrap point owns ClueInit",
22
+ "which existing login/session success point owns ClueIdentify",
23
+ "which existing account/workspace/organization resolution point owns ClueSetAccount",
24
+ "which existing logout/session reset point owns ClueLogout",
25
+ "whether a lifecycle point is unclear and should be reported as blocked",
26
+ ],
27
+ cli_should_control: [
28
+ "official SDK package names",
29
+ "official public SDK function names and supported lifecycle API set",
30
+ "environment variable names produced by setup and consumed by setup code",
31
+ "machine-owned semantic workflow generation and verification",
32
+ "setup-watch ownership as user-operated verification",
33
+ "static rejection of known unsafe wiring such as leaked secrets, wrong SDKs, blocking lifecycle calls, broad ClueTrack setup, and unsafe browser token proxy patterns",
34
+ "local API connectivity preflight for the four required setup hops before user-operated setup-watch",
35
+ ],
36
+ };
37
+
38
+ export const API_CONNECTIVITY_CONTRACT = {
39
+ purpose:
40
+ "Clue setup has four distinct HTTP hops. The AI must not collapse customer-owned proxy routes and Clue-owned ingest routes into one vague browser-token endpoint.",
41
+ hops: {
42
+ client_backend_browser_token_proxy: {
43
+ owner: "customer_backend",
44
+ method: "POST",
45
+ path: "/api/v1/clue/browser-tokens",
46
+ caller: "customer_frontend_browserTokenProvider",
47
+ purpose:
48
+ "Issue a short-lived browser token without exposing CLUE_API_KEY to browser code.",
49
+ },
50
+ clue_backend_browser_token_issue: {
51
+ owner: "clue_backend",
52
+ method: "POST",
53
+ path: "/api/v1/ingest/browser-tokens",
54
+ caller: "customer_backend_browser_token_proxy",
55
+ purpose:
56
+ "Clue backend validates CLUE_API_KEY, project, environment, service key, and origin, then returns a browser token.",
57
+ },
58
+ browser_ingest: {
59
+ owner: "clue_backend",
60
+ method: "POST",
61
+ path: "/api/v1/ingest/browser",
62
+ caller: "customer_frontend_browser_sdk",
63
+ env_name: "NEXT_PUBLIC_CLUE_INGEST_ENDPOINT for Next.js browser code",
64
+ purpose:
65
+ "Browser SDK sends observation source events with x-clue-browser-token.",
66
+ },
67
+ backend_ingest: {
68
+ owner: "clue_backend",
69
+ method: "POST",
70
+ path: "/api/v1/ingest/backend",
71
+ caller: "customer_backend_sdk",
72
+ env_name: "CLUE_INGEST_ENDPOINT",
73
+ purpose:
74
+ "Backend SDK sends observation source events with server-side CLUE_API_KEY.",
75
+ },
76
+ },
77
+ preflight_command: "setup-doctor --local",
78
+ setup_watch_boundary:
79
+ "setup-doctor checks local API connectivity before user flows. setup-watch remains user-operated lifecycle and event-delivery verification.",
80
+ };
81
+
82
+ export const setupDoctrineSkillLines = () => [
83
+ `- Purpose: ${SETUP_DOCTRINE.purpose}`,
84
+ `- Minimal diff reason: ${SETUP_DOCTRINE.minimal_diff_reason}`,
85
+ `- AI decision boundary: ${SETUP_DOCTRINE.ai_decision_boundary}`,
86
+ `- Static control boundary: ${SETUP_DOCTRINE.deterministic_control_boundary}`,
87
+ `- Documentation reason: ${SETUP_DOCTRINE.documentation_reason}`,
88
+ `- Failure posture: ${SETUP_DOCTRINE.failure_posture}`,
89
+ `- API connectivity: ${API_CONNECTIVITY_CONTRACT.purpose}`,
90
+ `- API preflight: run ${API_CONNECTIVITY_CONTRACT.preflight_command} when local services and required env are available; do not substitute it for user-operated setup-watch.`,
91
+ ];
@@ -12,6 +12,7 @@ import {
12
12
  stripSourceNoise,
13
13
  } from "./lifecycle-guard.mjs";
14
14
  import { listAllowedSourceFiles } from "./path-policy.mjs";
15
+ import { AI_SETUP_CONTRACT_VERSION } from "./setup-ai-contract.mjs";
15
16
  import { runSemanticInventory } from "./semantic-ci.mjs";
16
17
 
17
18
  const DEFAULT_WORKFLOW_PATH = ".github/workflows/clue-semantic-snapshot.yml";
@@ -27,7 +28,7 @@ const SETUP_SKILLS = [
27
28
  "clue-local-verification",
28
29
  "clue-setup-report",
29
30
  ];
30
- const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.v4";
31
+ const SETUP_SKILL_CONTENT_VERSION = AI_SETUP_CONTRACT_VERSION;
31
32
  const REQUIRED_SETUP_SKILL_PHRASES = {
32
33
  "clue-sdk-instrumentation": [
33
34
  "Do not create no-op wrappers",
@@ -42,6 +43,7 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
42
43
  "Whitespace-only changes are allowed only on lines directly changed for Clue SDK wiring",
43
44
  "The local backend token endpoint is part of the customer app, not the Clue API",
44
45
  "The frontend `browserTokenProvider` must send the same service key used by `ClueInit`",
46
+ "Do not forward `origin`, `projectKey`, or `environment` from JSON/body payload fields under server `CLUE_API_KEY`",
45
47
  "For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable",
46
48
  ],
47
49
  "clue-setup-audit": [
@@ -49,12 +51,14 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
49
51
  "Reject Django SDK setup when `clue-django-sdk` installability has not been verified",
50
52
  "Reject ClueTrack instrumentation unless the user explicitly requested product event tracking",
51
53
  "Reject Next.js browser/client code that reads non-public `process.env.CLUE_*` variables",
54
+ "Reject browser token proxy code that forwards origin, projectKey, or environment from request JSON/body under server `CLUE_API_KEY`",
52
55
  "Reject whitespace-only edits, import sorting, formatter churn",
53
56
  "Reject unrelated refactors, renames, file moves",
54
57
  "Execution agents must not approve, certify, or mark their own work complete",
55
58
  ],
56
59
  "clue-local-verification": [
57
60
  "`setup-check --require-sdk-lifecycle` is a static source check only",
61
+ "`setup-doctor --local` API connectivity preflight",
58
62
  "Verify frontend SDK installability/import",
59
63
  "Verify backend SDK installability/import",
60
64
  "Do not run `npx -y @clue-ai/cli setup-watch --local` automatically",
@@ -806,6 +810,17 @@ const findNextBrowserTokenProviderMissingPublicServiceKeyFiles = ({
806
810
  .map((source) => source.file_path);
807
811
  };
808
812
 
813
+ const findBrowserTokenProviderWrongProxyPathFiles = (frontendSources) =>
814
+ frontendSources
815
+ .filter(sourceLooksLikeBrowserTokenProvider)
816
+ .filter((source) => {
817
+ const text = stripSourceNoise(source.text);
818
+ if (!/\bfetch\s*\(/.test(text)) return false;
819
+ if (/\/api\/v1\/clue\/browser-tokens\b/.test(text)) return false;
820
+ return /browser[-_]?tokens?|browser[-_]?token/i.test(text);
821
+ })
822
+ .map((source) => source.file_path);
823
+
809
824
  const sourceLooksLikeBrowserTokenProxy = (source) => {
810
825
  const text = stripSourceNoise(source.text);
811
826
  return (
@@ -827,6 +842,43 @@ const findBrowserTokenProxyUsingBackendServiceKeyFiles = (backendSources) =>
827
842
  })
828
843
  .map((source) => source.file_path);
829
844
 
845
+ const bodyOriginPatterns = [
846
+ /\b(?:payload|body|requestBody|request_body|data|input)\.origin\b/i,
847
+ /\b(?:payload|body|requestBody|request_body|data|input)\s*\[\s*["']origin["']\s*\]/i,
848
+ /\b(?:payload|body|requestBody|request_body|data|input)\.get\s*\(\s*["']origin["']/i,
849
+ /["']origin["']\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
850
+ /\borigin\s*=\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
851
+ ];
852
+
853
+ const bodyProjectEnvironmentPatterns = [
854
+ /\b(?:projectKey|project_key)\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
855
+ /["'](?:projectKey|project_key)["']\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
856
+ /\benvironment\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
857
+ /["']environment["']\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
858
+ ];
859
+
860
+ const findBrowserTokenProxyTrustingBodyOriginFiles = (backendSources) =>
861
+ backendSources
862
+ .filter(sourceLooksLikeBrowserTokenProxy)
863
+ .filter((source) => {
864
+ const text = stripSourceNoise(source.text);
865
+ return bodyOriginPatterns.some((pattern) => pattern.test(text));
866
+ })
867
+ .map((source) => source.file_path);
868
+
869
+ const findBrowserTokenProxyTrustingBodyProjectEnvironmentFiles = (
870
+ backendSources,
871
+ ) =>
872
+ backendSources
873
+ .filter(sourceLooksLikeBrowserTokenProxy)
874
+ .filter((source) => {
875
+ const text = stripSourceNoise(source.text);
876
+ return bodyProjectEnvironmentPatterns.some((pattern) =>
877
+ pattern.test(text),
878
+ );
879
+ })
880
+ .map((source) => source.file_path);
881
+
830
882
  const backendSdkSpec = (framework) =>
831
883
  BACKEND_SDK_BY_FRAMEWORK[String(framework ?? "").toLowerCase()] ?? {
832
884
  packages: Object.values(BACKEND_SDK_BY_FRAMEWORK).flatMap(
@@ -947,6 +999,8 @@ const checkSdkLifecycle = ({
947
999
  findWildcardFrontendSdkDependencyFiles(dependencySources);
948
1000
  const browserTokenProviderMissingServiceKeyFiles =
949
1001
  findBrowserTokenProviderMissingServiceKeyFiles(frontendSources);
1002
+ const browserTokenProviderWrongProxyPathFiles =
1003
+ findBrowserTokenProviderWrongProxyPathFiles(frontendSources);
950
1004
  const nextBrowserTokenProviderMissingPublicServiceKeyFiles =
951
1005
  findNextBrowserTokenProviderMissingPublicServiceKeyFiles({
952
1006
  dependencySources,
@@ -954,6 +1008,10 @@ const checkSdkLifecycle = ({
954
1008
  });
955
1009
  const browserTokenProxyUsingBackendServiceKeyFiles =
956
1010
  findBrowserTokenProxyUsingBackendServiceKeyFiles(backendSources);
1011
+ const browserTokenProxyTrustingBodyOriginFiles =
1012
+ findBrowserTokenProxyTrustingBodyOriginFiles(backendSources);
1013
+ const browserTokenProxyTrustingBodyProjectEnvironmentFiles =
1014
+ findBrowserTokenProxyTrustingBodyProjectEnvironmentFiles(backendSources);
957
1015
  const backendIdentityRequired =
958
1016
  backendPresent &&
959
1017
  /\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
@@ -1014,10 +1072,16 @@ const checkSdkLifecycle = ({
1014
1072
  wildcard_sdk_dependency_files: wildcardFrontendSdkDependencyFiles,
1015
1073
  browser_token_provider_missing_service_key_files:
1016
1074
  browserTokenProviderMissingServiceKeyFiles,
1075
+ browser_token_provider_wrong_proxy_path_files:
1076
+ browserTokenProviderWrongProxyPathFiles,
1017
1077
  next_browser_token_provider_missing_public_service_key_files:
1018
1078
  nextBrowserTokenProviderMissingPublicServiceKeyFiles,
1019
1079
  browser_token_proxy_uses_backend_service_key_files:
1020
1080
  browserTokenProxyUsingBackendServiceKeyFiles,
1081
+ browser_token_proxy_trusts_body_origin_files:
1082
+ browserTokenProxyTrustingBodyOriginFiles,
1083
+ browser_token_proxy_trusts_body_project_environment_files:
1084
+ browserTokenProxyTrustingBodyProjectEnvironmentFiles,
1021
1085
  },
1022
1086
  has_noop_wrapper: noOpPattern.test(combined),
1023
1087
  component_lifecycle_init_files: componentLifecycleInitFiles,
@@ -1037,8 +1101,11 @@ const checkSdkLifecycle = ({
1037
1101
  nextLifecycleNonPublicEnvFiles.length === 0 &&
1038
1102
  wildcardFrontendSdkDependencyFiles.length === 0 &&
1039
1103
  browserTokenProviderMissingServiceKeyFiles.length === 0 &&
1104
+ browserTokenProviderWrongProxyPathFiles.length === 0 &&
1040
1105
  nextBrowserTokenProviderMissingPublicServiceKeyFiles.length === 0 &&
1041
1106
  browserTokenProxyUsingBackendServiceKeyFiles.length === 0 &&
1107
+ browserTokenProxyTrustingBodyOriginFiles.length === 0 &&
1108
+ browserTokenProxyTrustingBodyProjectEnvironmentFiles.length === 0 &&
1042
1109
  !noOpPattern.test(combined) &&
1043
1110
  componentLifecycleInitFiles.length === 0 &&
1044
1111
  blockingLifecycleFiles.length === 0,
@@ -0,0 +1,435 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { API_CONNECTIVITY_CONTRACT } from "./setup-ai-contract.mjs";
4
+
5
+ const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
6
+ const BROWSER_TOKEN_PROXY_PATH =
7
+ API_CONNECTIVITY_CONTRACT.hops.client_backend_browser_token_proxy.path;
8
+ const CLUE_BROWSER_TOKEN_PATH =
9
+ API_CONNECTIVITY_CONTRACT.hops.clue_backend_browser_token_issue.path;
10
+ const BROWSER_INGEST_PATH = API_CONNECTIVITY_CONTRACT.hops.browser_ingest.path;
11
+ const BACKEND_INGEST_PATH = API_CONNECTIVITY_CONTRACT.hops.backend_ingest.path;
12
+
13
+ const optionalString = (value) =>
14
+ typeof value === "string" && value.trim() ? value.trim() : null;
15
+
16
+ const trimTrailingSlash = (value) => String(value).replace(/\/+$/, "");
17
+
18
+ const joinUrl = (baseUrl, path) => `${trimTrailingSlash(baseUrl)}${path}`;
19
+
20
+ const readJsonIfPresent = async (path) => {
21
+ try {
22
+ return JSON.parse(await readFile(path, "utf8"));
23
+ } catch (error) {
24
+ if (error?.code === "ENOENT") return null;
25
+ throw error;
26
+ }
27
+ };
28
+
29
+ const manifestWatchTargets = (manifest, kind) => {
30
+ const targets = manifest?.lifecycle_verification?.watch_targets;
31
+ if (!Array.isArray(targets)) return [];
32
+ return targets.filter((target) => target?.kind === kind);
33
+ };
34
+
35
+ const firstTargetUrl = ({ env, manifest, kind }) => {
36
+ for (const target of manifestWatchTargets(manifest, kind)) {
37
+ const explicitUrl = optionalString(target.url);
38
+ if (explicitUrl) return explicitUrl;
39
+ const envName = optionalString(target.url_env_name);
40
+ if (envName && optionalString(env[envName])) return optionalString(env[envName]);
41
+ const candidates = Array.isArray(target.local_url_candidates)
42
+ ? target.local_url_candidates
43
+ : [];
44
+ for (const candidate of candidates) {
45
+ const value = optionalString(candidate);
46
+ if (value) return value;
47
+ }
48
+ }
49
+ return null;
50
+ };
51
+
52
+ const firstTargetServiceKey = (manifest, kind) => {
53
+ for (const target of manifestWatchTargets(manifest, kind)) {
54
+ const serviceKey = optionalString(target.service_key ?? target.serviceKey);
55
+ if (serviceKey) return serviceKey;
56
+ }
57
+ return null;
58
+ };
59
+
60
+ const readTextResponse = async (response) => {
61
+ try {
62
+ return await response.text();
63
+ } catch {
64
+ return "";
65
+ }
66
+ };
67
+
68
+ const parseJsonText = (text) => {
69
+ try {
70
+ return text.trim() ? JSON.parse(text) : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ };
75
+
76
+ const postJson = async ({ body, fetchImpl, headers = {}, url }) => {
77
+ try {
78
+ const response = await fetchImpl(url, {
79
+ method: "POST",
80
+ headers: {
81
+ "content-type": "application/json",
82
+ ...headers,
83
+ },
84
+ body: JSON.stringify(body),
85
+ });
86
+ const text = await readTextResponse(response);
87
+ return {
88
+ transportOk: true,
89
+ response,
90
+ text,
91
+ json: parseJsonText(text),
92
+ };
93
+ } catch (error) {
94
+ return {
95
+ transportOk: false,
96
+ error: error instanceof Error ? error.message : String(error),
97
+ };
98
+ }
99
+ };
100
+
101
+ const compactFailure = (result) => {
102
+ if (!result.transportOk) return result.error ?? "request failed";
103
+ const jsonMessage =
104
+ typeof result.json?.message === "string"
105
+ ? result.json.message
106
+ : typeof result.json?.error === "string"
107
+ ? result.json.error
108
+ : null;
109
+ return jsonMessage ?? result.text?.slice(0, 240) ?? "request failed";
110
+ };
111
+
112
+ const tokenFromResult = (result) =>
113
+ result.transportOk &&
114
+ result.response.ok &&
115
+ typeof result.json?.token === "string" &&
116
+ result.json.token.trim()
117
+ ? result.json.token.trim()
118
+ : null;
119
+
120
+ const buildCheck = ({
121
+ error = null,
122
+ id,
123
+ method = "POST",
124
+ passed,
125
+ result = null,
126
+ url,
127
+ }) => ({
128
+ id,
129
+ method,
130
+ url,
131
+ passed: Boolean(passed),
132
+ status: result?.transportOk ? result.response.status : null,
133
+ error: passed ? null : (error ?? compactFailure(result)),
134
+ });
135
+
136
+ const buildBrowserEventPayload = ({ environment, projectKey, serviceKey }) => {
137
+ const timestamp = new Date().toISOString();
138
+ const id = `setup_doctor_${Date.now()}`;
139
+ return {
140
+ batch_id: `batch_${id}`,
141
+ idempotency_key: `idem_${id}`,
142
+ sent_at: timestamp,
143
+ source_type: "browser_sdk",
144
+ source_schema_version: "1",
145
+ producer_metadata: {
146
+ producer_id: serviceKey,
147
+ sdk_type: "browser",
148
+ sdk_version: "setup-doctor",
149
+ },
150
+ events: [
151
+ {
152
+ event_id: `event_${id}`,
153
+ event_category: "custom",
154
+ event_name: "setup_doctor_browser_connectivity",
155
+ occurred_at: timestamp,
156
+ source_event_id: `event_${id}`,
157
+ source_schema_version: "1",
158
+ source_event_type: "setup_doctor_browser_connectivity",
159
+ source_event_kind: "custom",
160
+ producer_id: serviceKey,
161
+ project_key: projectKey,
162
+ environment,
163
+ properties: {},
164
+ metrics: { count: 1 },
165
+ },
166
+ ],
167
+ };
168
+ };
169
+
170
+ const buildBackendEventPayload = ({
171
+ apiKey,
172
+ backendServiceKey,
173
+ environment,
174
+ projectKey,
175
+ }) => {
176
+ const timestamp = new Date().toISOString();
177
+ return {
178
+ projectKey,
179
+ apiKey,
180
+ environment,
181
+ sdkType: "backend",
182
+ sdkVersion: "setup-doctor",
183
+ schemaVersion: 1,
184
+ events: [
185
+ {
186
+ event_name: "setup_doctor_backend_connectivity",
187
+ occurred_at: timestamp,
188
+ service_key: backendServiceKey,
189
+ properties: {},
190
+ metrics: { count: 1 },
191
+ },
192
+ ],
193
+ };
194
+ };
195
+
196
+ const requiredInputCheck = ({ id, missing, url = null }) => ({
197
+ id,
198
+ method: "POST",
199
+ url,
200
+ passed: false,
201
+ status: null,
202
+ error: `missing required input: ${missing.join(", ")}`,
203
+ });
204
+
205
+ export const runSetupDoctor = async ({
206
+ env = process.env,
207
+ fetchImpl = fetch,
208
+ flags,
209
+ repoRoot = ".",
210
+ }) => {
211
+ const manifestPath = String(
212
+ flags.get("manifest") || DEFAULT_SETUP_MANIFEST_PATH,
213
+ );
214
+ const manifest = await readJsonIfPresent(resolve(repoRoot, manifestPath));
215
+ const clueApiBaseUrl =
216
+ optionalString(flags.get("clue-api-base-url")) ??
217
+ optionalString(env.CLUE_API_BASE_URL) ??
218
+ optionalString(manifest?.clue_context?.clue_api_base_url);
219
+ const projectKey =
220
+ optionalString(flags.get("project-key")) ??
221
+ optionalString(env.CLUE_PROJECT_KEY) ??
222
+ optionalString(manifest?.clue_context?.project_key);
223
+ const environment =
224
+ optionalString(flags.get("environment")) ??
225
+ optionalString(env.CLUE_ENVIRONMENT) ??
226
+ optionalString(manifest?.clue_context?.environment) ??
227
+ "dev";
228
+ const apiKey =
229
+ optionalString(flags.get("clue-api-key")) ??
230
+ optionalString(env.CLUE_API_KEY);
231
+ const serviceKey =
232
+ optionalString(flags.get("service-key")) ??
233
+ optionalString(env.NEXT_PUBLIC_CLUE_SERVICE_KEY) ??
234
+ firstTargetServiceKey(manifest, "frontend");
235
+ const backendServiceKey =
236
+ optionalString(flags.get("backend-service-key")) ??
237
+ optionalString(env.CLUE_SERVICE_KEY) ??
238
+ firstTargetServiceKey(manifest, "backend") ??
239
+ serviceKey;
240
+ const clientBackendUrl =
241
+ optionalString(flags.get("client-backend-url")) ??
242
+ firstTargetUrl({ env, manifest, kind: "backend" });
243
+ const clientFrontendUrl =
244
+ optionalString(flags.get("client-frontend-url")) ??
245
+ firstTargetUrl({ env, manifest, kind: "frontend" });
246
+ const origin =
247
+ optionalString(flags.get("origin")) ??
248
+ clientFrontendUrl ??
249
+ "http://localhost";
250
+ const browserTokenProxyUrl = clientBackendUrl
251
+ ? joinUrl(clientBackendUrl, BROWSER_TOKEN_PROXY_PATH)
252
+ : null;
253
+ const clueBrowserTokenUrl = clueApiBaseUrl
254
+ ? joinUrl(clueApiBaseUrl, CLUE_BROWSER_TOKEN_PATH)
255
+ : null;
256
+ const browserIngestUrl =
257
+ optionalString(flags.get("browser-ingest-url")) ??
258
+ optionalString(env.NEXT_PUBLIC_CLUE_INGEST_ENDPOINT) ??
259
+ optionalString(manifest?.clue_context?.ingest_endpoints?.browser) ??
260
+ (clueApiBaseUrl ? joinUrl(clueApiBaseUrl, BROWSER_INGEST_PATH) : null);
261
+ const backendIngestUrl =
262
+ optionalString(flags.get("backend-ingest-url")) ??
263
+ optionalString(env.CLUE_INGEST_ENDPOINT) ??
264
+ optionalString(manifest?.clue_context?.ingest_endpoints?.backend) ??
265
+ (clueApiBaseUrl ? joinUrl(clueApiBaseUrl, BACKEND_INGEST_PATH) : null);
266
+
267
+ const checks = [];
268
+
269
+ let proxyToken = null;
270
+ if (!browserTokenProxyUrl || !serviceKey) {
271
+ checks.push(
272
+ requiredInputCheck({
273
+ id: "client_backend_browser_token_proxy",
274
+ missing: [
275
+ ...(!browserTokenProxyUrl ? ["client-backend-url"] : []),
276
+ ...(!serviceKey ? ["service-key"] : []),
277
+ ],
278
+ url: browserTokenProxyUrl,
279
+ }),
280
+ );
281
+ } else {
282
+ const result = await postJson({
283
+ fetchImpl,
284
+ url: browserTokenProxyUrl,
285
+ headers: { origin },
286
+ body: { serviceKey },
287
+ });
288
+ proxyToken = tokenFromResult(result);
289
+ checks.push(
290
+ buildCheck({
291
+ id: "client_backend_browser_token_proxy",
292
+ passed: Boolean(proxyToken),
293
+ result,
294
+ url: browserTokenProxyUrl,
295
+ }),
296
+ );
297
+ }
298
+
299
+ let directToken = null;
300
+ if (!clueBrowserTokenUrl || !apiKey || !projectKey || !environment || !serviceKey) {
301
+ checks.push(
302
+ requiredInputCheck({
303
+ id: "clue_backend_browser_token_issue",
304
+ missing: [
305
+ ...(!clueBrowserTokenUrl ? ["clue-api-base-url"] : []),
306
+ ...(!apiKey ? ["CLUE_API_KEY"] : []),
307
+ ...(!projectKey ? ["CLUE_PROJECT_KEY"] : []),
308
+ ...(!environment ? ["CLUE_ENVIRONMENT"] : []),
309
+ ...(!serviceKey ? ["service-key"] : []),
310
+ ],
311
+ url: clueBrowserTokenUrl,
312
+ }),
313
+ );
314
+ } else {
315
+ const result = await postJson({
316
+ fetchImpl,
317
+ url: clueBrowserTokenUrl,
318
+ headers: { "x-clue-api-key": apiKey },
319
+ body: {
320
+ projectKey,
321
+ environment,
322
+ serviceKey,
323
+ origin,
324
+ },
325
+ });
326
+ directToken = tokenFromResult(result);
327
+ checks.push(
328
+ buildCheck({
329
+ id: "clue_backend_browser_token_issue",
330
+ passed: Boolean(directToken),
331
+ result,
332
+ url: clueBrowserTokenUrl,
333
+ }),
334
+ );
335
+ }
336
+
337
+ const browserToken = proxyToken ?? directToken;
338
+ if (!browserIngestUrl || !projectKey || !environment || !serviceKey || !browserToken) {
339
+ checks.push(
340
+ requiredInputCheck({
341
+ id: "browser_ingest",
342
+ missing: [
343
+ ...(!browserIngestUrl ? ["browser-ingest-url"] : []),
344
+ ...(!projectKey ? ["CLUE_PROJECT_KEY"] : []),
345
+ ...(!environment ? ["CLUE_ENVIRONMENT"] : []),
346
+ ...(!serviceKey ? ["service-key"] : []),
347
+ ...(!browserToken ? ["browser token"] : []),
348
+ ],
349
+ url: browserIngestUrl,
350
+ }),
351
+ );
352
+ } else {
353
+ const result = await postJson({
354
+ fetchImpl,
355
+ url: browserIngestUrl,
356
+ headers: {
357
+ origin,
358
+ "x-clue-project-key": projectKey,
359
+ "x-clue-service-key": serviceKey,
360
+ "x-clue-browser-token": browserToken,
361
+ "x-clue-sdk-request": "browser",
362
+ "x-clue-environment": environment,
363
+ "x-clue-sdk-version": "setup-doctor",
364
+ "x-clue-source-schema-version": "1",
365
+ },
366
+ body: buildBrowserEventPayload({ environment, projectKey, serviceKey }),
367
+ });
368
+ checks.push(
369
+ buildCheck({
370
+ id: "browser_ingest",
371
+ passed: Boolean(result.transportOk && result.response.ok),
372
+ result,
373
+ url: browserIngestUrl,
374
+ }),
375
+ );
376
+ }
377
+
378
+ if (!backendIngestUrl || !apiKey || !projectKey || !environment || !backendServiceKey) {
379
+ checks.push(
380
+ requiredInputCheck({
381
+ id: "backend_ingest",
382
+ missing: [
383
+ ...(!backendIngestUrl ? ["backend-ingest-url"] : []),
384
+ ...(!apiKey ? ["CLUE_API_KEY"] : []),
385
+ ...(!projectKey ? ["CLUE_PROJECT_KEY"] : []),
386
+ ...(!environment ? ["CLUE_ENVIRONMENT"] : []),
387
+ ...(!backendServiceKey ? ["backend-service-key"] : []),
388
+ ],
389
+ url: backendIngestUrl,
390
+ }),
391
+ );
392
+ } else {
393
+ const result = await postJson({
394
+ fetchImpl,
395
+ url: backendIngestUrl,
396
+ headers: {
397
+ "x-clue-project-key": projectKey,
398
+ "x-clue-api-key": apiKey,
399
+ },
400
+ body: buildBackendEventPayload({
401
+ apiKey,
402
+ backendServiceKey,
403
+ environment,
404
+ projectKey,
405
+ }),
406
+ });
407
+ checks.push(
408
+ buildCheck({
409
+ id: "backend_ingest",
410
+ passed: Boolean(result.transportOk && result.response.ok),
411
+ result,
412
+ url: backendIngestUrl,
413
+ }),
414
+ );
415
+ }
416
+
417
+ const passed = checks.every((check) => check.passed);
418
+ return {
419
+ status: passed ? "passed" : "failed",
420
+ passed,
421
+ contract: API_CONNECTIVITY_CONTRACT,
422
+ checks,
423
+ inputs: {
424
+ manifest_loaded: Boolean(manifest),
425
+ client_backend_url_configured: Boolean(clientBackendUrl),
426
+ client_frontend_url_configured: Boolean(clientFrontendUrl),
427
+ clue_api_base_url_configured: Boolean(clueApiBaseUrl),
428
+ project_key_configured: Boolean(projectKey),
429
+ environment_configured: Boolean(environment),
430
+ service_key_configured: Boolean(serviceKey),
431
+ backend_service_key_configured: Boolean(backendServiceKey),
432
+ clue_api_key_configured: Boolean(apiKey),
433
+ },
434
+ };
435
+ };