@builder.io/ai-utils 0.81.3 → 0.83.0-dev.202607211718.99c5f9cf8
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/package.json +1 -1
- package/src/acl-schema.d.ts +88 -0
- package/src/acl-schema.js +71 -0
- package/src/claw.d.ts +1 -0
- package/src/claw.js +9 -0
- package/src/claw.spec.js +11 -1
- package/src/codegen/investigation-context.d.ts +1 -1
- package/src/codegen.d.ts +10 -0
- package/src/codegen.js +2 -1
- package/src/completion.d.ts +2 -1
- package/src/connectivity/checks/http-check.d.ts +7 -0
- package/src/connectivity/checks/http-check.js +13 -5
- package/src/connectivity/checks/http-check.spec.js +58 -0
- package/src/connectivity/environment.js +6 -0
- package/src/connectivity/node.d.ts +1 -1
- package/src/connectivity/node.js +1 -1
- package/src/connectivity/run-checks.js +13 -6
- package/src/connectivity/targets.d.ts +11 -1
- package/src/connectivity/targets.js +34 -1
- package/src/connectivity/targets.spec.js +51 -1
- package/src/connectivity/types.d.ts +17 -1
- package/src/editor-ai.d.ts +53 -0
- package/src/editor-ai.js +69 -0
- package/src/editor-ai.test.d.ts +1 -0
- package/src/editor-ai.test.js +37 -0
- package/src/embeddings.d.ts +82 -0
- package/src/embeddings.js +106 -0
- package/src/embeddings.spec.d.ts +1 -0
- package/src/embeddings.spec.js +43 -0
- package/src/events.d.ts +27 -1
- package/src/events.js +8 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +3 -0
- package/src/messages.d.ts +15 -0
- package/src/organization.d.ts +9 -0
- package/src/perf-report.d.ts +116 -0
- package/src/perf-report.js +122 -0
- package/src/projects.d.ts +109 -12
- package/src/projects.js +87 -9
- package/src/proxy.d.ts +1 -1
- package/src/realtime.d.ts +232 -0
- package/src/realtime.js +158 -0
- package/src/realtime.spec.d.ts +1 -0
- package/src/realtime.spec.js +122 -0
- package/src/vpc-peering.d.ts +49 -0
- package/src/vpc-peering.js +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const PermissionSchema: z.ZodEnum<{
|
|
3
|
+
list: "list";
|
|
4
|
+
read: "read";
|
|
5
|
+
write: "write";
|
|
6
|
+
}>;
|
|
7
|
+
export type Permission = z.infer<typeof PermissionSchema>;
|
|
8
|
+
export declare const AclEntrySchema: z.ZodObject<{
|
|
9
|
+
action: z.ZodEnum<{
|
|
10
|
+
allow: "allow";
|
|
11
|
+
deny: "deny";
|
|
12
|
+
}>;
|
|
13
|
+
resource: z.ZodString;
|
|
14
|
+
permissions: z.ZodArray<z.ZodEnum<{
|
|
15
|
+
list: "list";
|
|
16
|
+
read: "read";
|
|
17
|
+
write: "write";
|
|
18
|
+
}>>;
|
|
19
|
+
description: z.ZodOptional<z.ZodString>;
|
|
20
|
+
principals: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
export type AclEntry = z.infer<typeof AclEntrySchema>;
|
|
23
|
+
export declare const AclPolicySchema: z.ZodObject<{
|
|
24
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
25
|
+
entries: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
26
|
+
action: z.ZodEnum<{
|
|
27
|
+
allow: "allow";
|
|
28
|
+
deny: "deny";
|
|
29
|
+
}>;
|
|
30
|
+
resource: z.ZodString;
|
|
31
|
+
permissions: z.ZodArray<z.ZodEnum<{
|
|
32
|
+
list: "list";
|
|
33
|
+
read: "read";
|
|
34
|
+
write: "write";
|
|
35
|
+
}>>;
|
|
36
|
+
description: z.ZodOptional<z.ZodString>;
|
|
37
|
+
principals: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
38
|
+
}, z.core.$strip>>>;
|
|
39
|
+
denyDescription: z.ZodOptional<z.ZodString>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
export type AclPolicy = z.infer<typeof AclPolicySchema>;
|
|
42
|
+
export declare const AclDenialSchema: z.ZodObject<{
|
|
43
|
+
kind: z.ZodEnum<{
|
|
44
|
+
"command-allowlist": "command-allowlist";
|
|
45
|
+
"command-security": "command-security";
|
|
46
|
+
"file-access": "file-access";
|
|
47
|
+
}>;
|
|
48
|
+
reason: z.ZodEnum<{
|
|
49
|
+
"deny-pattern-matched": "deny-pattern-matched";
|
|
50
|
+
"no-allow-match": "no-allow-match";
|
|
51
|
+
"security-policy": "security-policy";
|
|
52
|
+
"shell-metacharacter": "shell-metacharacter";
|
|
53
|
+
}>;
|
|
54
|
+
resource: z.ZodString;
|
|
55
|
+
command: z.ZodOptional<z.ZodString>;
|
|
56
|
+
permission: z.ZodOptional<z.ZodEnum<{
|
|
57
|
+
list: "list";
|
|
58
|
+
read: "read";
|
|
59
|
+
write: "write";
|
|
60
|
+
}>>;
|
|
61
|
+
policy: z.ZodOptional<z.ZodString>;
|
|
62
|
+
matchedPattern: z.ZodOptional<z.ZodString>;
|
|
63
|
+
matchedEntry: z.ZodOptional<z.ZodObject<{
|
|
64
|
+
action: z.ZodEnum<{
|
|
65
|
+
allow: "allow";
|
|
66
|
+
deny: "deny";
|
|
67
|
+
}>;
|
|
68
|
+
resource: z.ZodString;
|
|
69
|
+
permissions: z.ZodArray<z.ZodEnum<{
|
|
70
|
+
list: "list";
|
|
71
|
+
read: "read";
|
|
72
|
+
write: "write";
|
|
73
|
+
}>>;
|
|
74
|
+
description: z.ZodOptional<z.ZodString>;
|
|
75
|
+
principals: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
76
|
+
}, z.core.$strip>>;
|
|
77
|
+
message: z.ZodString;
|
|
78
|
+
}, z.core.$strip>;
|
|
79
|
+
export type AclDenial = z.infer<typeof AclDenialSchema>;
|
|
80
|
+
export interface AccessResult {
|
|
81
|
+
allowed: boolean;
|
|
82
|
+
message: string;
|
|
83
|
+
matchedEntry?: AclEntry;
|
|
84
|
+
matchedPattern?: string;
|
|
85
|
+
reason?: "deny-pattern-matched" | "no-allow-match";
|
|
86
|
+
requestedResource?: string;
|
|
87
|
+
requestedPermission?: Permission;
|
|
88
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const PermissionSchema = z
|
|
3
|
+
.enum(["read", "write", "list"])
|
|
4
|
+
.meta({ title: "Permission" });
|
|
5
|
+
// One ACL rule
|
|
6
|
+
export const AclEntrySchema = z
|
|
7
|
+
.object({
|
|
8
|
+
action: z
|
|
9
|
+
.enum(["allow", "deny"])
|
|
10
|
+
.meta({ description: "whether this rule allows or denies access" }),
|
|
11
|
+
resource: z
|
|
12
|
+
.string()
|
|
13
|
+
.meta({ description: "what — supports glob patterns like /files/*.txt" }),
|
|
14
|
+
permissions: z
|
|
15
|
+
.array(PermissionSchema)
|
|
16
|
+
.meta({ description: "actions this rule applies to" }),
|
|
17
|
+
description: z.string().optional().meta({
|
|
18
|
+
description: "custom message, in deny case, this is the error message. This will override denyDescription on AclPolicy if defined.",
|
|
19
|
+
}),
|
|
20
|
+
principals: z.array(z.string()).optional().meta({
|
|
21
|
+
description: "array of teams/roles this rule applies to (e.g., ['developer', 'admin'])",
|
|
22
|
+
}),
|
|
23
|
+
})
|
|
24
|
+
.meta({ title: "AclEntry" });
|
|
25
|
+
// A full ACL policy is just a list of rules
|
|
26
|
+
export const AclPolicySchema = z
|
|
27
|
+
.object({
|
|
28
|
+
secrets: z.array(z.string()).optional(),
|
|
29
|
+
entries: z.array(AclEntrySchema).optional(),
|
|
30
|
+
denyDescription: z.string().optional().meta({
|
|
31
|
+
description: "Default message to use when a resource is denied access",
|
|
32
|
+
}),
|
|
33
|
+
})
|
|
34
|
+
.meta({ title: "AclPolicy" });
|
|
35
|
+
// Structured description of an ACL/policy denial. Travels with the tool result
|
|
36
|
+
// so internal tools can show admins exactly which rule blocked a command/file,
|
|
37
|
+
// and both UIs can render a distinct "blocked, did not run" treatment.
|
|
38
|
+
export const AclDenialSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
kind: z
|
|
41
|
+
.enum(["command-security", "command-allowlist", "file-access"])
|
|
42
|
+
.meta({ description: "which gate produced the denial" }),
|
|
43
|
+
reason: z
|
|
44
|
+
.enum([
|
|
45
|
+
"security-policy",
|
|
46
|
+
"deny-pattern-matched",
|
|
47
|
+
"no-allow-match",
|
|
48
|
+
"shell-metacharacter",
|
|
49
|
+
])
|
|
50
|
+
.meta({ description: "why the denial happened" }),
|
|
51
|
+
resource: z.string().meta({
|
|
52
|
+
description: "the file path or command that was blocked",
|
|
53
|
+
}),
|
|
54
|
+
command: z.string().optional().meta({
|
|
55
|
+
description: "the full command, when the denial is command-related",
|
|
56
|
+
}),
|
|
57
|
+
permission: PermissionSchema.optional().meta({
|
|
58
|
+
description: "the requested permission, for file-access denials",
|
|
59
|
+
}),
|
|
60
|
+
policy: z.string().optional().meta({
|
|
61
|
+
description: "named security policy that matched, when applicable",
|
|
62
|
+
}),
|
|
63
|
+
matchedPattern: z.string().optional().meta({
|
|
64
|
+
description: "the glob/pattern that matched the resource or command",
|
|
65
|
+
}),
|
|
66
|
+
matchedEntry: AclEntrySchema.optional().meta({
|
|
67
|
+
description: "the full ACL entry that matched, for file-access denials",
|
|
68
|
+
}),
|
|
69
|
+
message: z.string().meta({ description: "human-readable explanation" }),
|
|
70
|
+
})
|
|
71
|
+
.meta({ title: "AclDenial" });
|
package/src/claw.d.ts
CHANGED
|
@@ -101,6 +101,7 @@ export type ChannelType<P extends KnownPlatform> = (typeof CHANNEL_TYPES)[P][num
|
|
|
101
101
|
/** Union of every channel sub-type across all platforms. */
|
|
102
102
|
export type AnyChannelType = ChannelType<KnownPlatform>;
|
|
103
103
|
export declare function isChannelType<P extends KnownPlatform>(platform: P, type: string): type is ChannelType<P>;
|
|
104
|
+
export declare function isBuilderBranchChannelId(channelId: string): boolean;
|
|
104
105
|
/** Platform recorded on a logged message; "unknown" when the channelId fails to parse. */
|
|
105
106
|
export type ChannelSource = KnownPlatform | "unknown";
|
|
106
107
|
/** Channel sub-type recorded on a logged message; "unknown" when parsing fails. */
|
package/src/claw.js
CHANGED
|
@@ -81,6 +81,15 @@ export const CHANNEL_TYPES = {
|
|
|
81
81
|
export function isChannelType(platform, type) {
|
|
82
82
|
return CHANNEL_TYPES[platform].includes(type);
|
|
83
83
|
}
|
|
84
|
+
export function isBuilderBranchChannelId(channelId) {
|
|
85
|
+
try {
|
|
86
|
+
const channel = parseChannelId(channelId);
|
|
87
|
+
return channel.platform === "builder" && channel.type === "branch";
|
|
88
|
+
}
|
|
89
|
+
catch (_a) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
84
93
|
/**
|
|
85
94
|
* Converts a Builder channel_id URI to a clickable URL for the
|
|
86
95
|
* corresponding platform (Slack, Jira, etc.).
|
package/src/claw.spec.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { convertChannelIdToUrl, formatIncomingMessage, formatWorkerMessage, formatWorkerReport, } from "./claw";
|
|
2
|
+
import { convertChannelIdToUrl, formatIncomingMessage, formatWorkerMessage, formatWorkerReport, isBuilderBranchChannelId, } from "./claw";
|
|
3
|
+
describe("isBuilderBranchChannelId", () => {
|
|
4
|
+
it("recognizes Builder branch channel IDs", () => {
|
|
5
|
+
expect(isBuilderBranchChannelId("builder/branch/proj-id/my-branch")).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
it("rejects other and invalid channel IDs", () => {
|
|
8
|
+
expect(isBuilderBranchChannelId("slack/channel/team/channel")).toBe(false);
|
|
9
|
+
expect(isBuilderBranchChannelId("builder/project/proj-id")).toBe(false);
|
|
10
|
+
expect(isBuilderBranchChannelId("invalid")).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
3
13
|
describe("convertChannelIdToUrl", () => {
|
|
4
14
|
describe("slack/thread format", () => {
|
|
5
15
|
it("converts a thread channel ID to a Slack app_redirect URL", () => {
|
|
@@ -18,5 +18,5 @@ export interface BuildInvestigationSystemPromptInput {
|
|
|
18
18
|
event: InvestigationEvent;
|
|
19
19
|
completionJson: InvestigationCompletionJSON | undefined;
|
|
20
20
|
}
|
|
21
|
-
export declare function buildInvestigationSystemPrompt({ event, completionJson }: BuildInvestigationSystemPromptInput): string;
|
|
21
|
+
export declare function buildInvestigationSystemPrompt({ event, completionJson, }: BuildInvestigationSystemPromptInput): string;
|
|
22
22
|
export declare const SUGGESTED_QUESTIONS: readonly string[];
|
package/src/codegen.d.ts
CHANGED
|
@@ -2890,6 +2890,8 @@ export declare const CodeGenInputOptionsSchema: z.ZodObject<{
|
|
|
2890
2890
|
systemReminderPrompt: z.ZodOptional<z.ZodString>;
|
|
2891
2891
|
}, z.core.$loose>;
|
|
2892
2892
|
export type CodeGenInputOptions = z.infer<typeof CodeGenInputOptionsSchema> & {
|
|
2893
|
+
/** @internal Derived from BuilderEditorState.components for editor AI. */
|
|
2894
|
+
_allowedComponentNames?: string[];
|
|
2893
2895
|
/**
|
|
2894
2896
|
* Optional factory function to wrap the MCP client with custom behavior
|
|
2895
2897
|
* Used by Editor AI to add permission checks for model operations
|
|
@@ -3295,6 +3297,7 @@ export interface GenerateCompletionStepGit {
|
|
|
3295
3297
|
*/
|
|
3296
3298
|
diagnostics?: GitStatusDiagnostics;
|
|
3297
3299
|
}
|
|
3300
|
+
export declare const FUSION_GENERATION_APP_PLACEHOLDER_COMMENT = "{/* TODO: FUSION_GENERATION_APP_PLACEHOLDER replace everything here with the actual app! */}";
|
|
3298
3301
|
/**
|
|
3299
3302
|
* A typed comment added to the session via the generic `AddComment` tool. The
|
|
3300
3303
|
* `commentType` mirrors the tool's `type` field (e.g. `"code-review"`) and
|
|
@@ -3950,6 +3953,13 @@ export interface MCPAuthRequiredServer {
|
|
|
3950
3953
|
authorizationUrl: string;
|
|
3951
3954
|
scopes: string[];
|
|
3952
3955
|
isReauth: boolean;
|
|
3956
|
+
/**
|
|
3957
|
+
* Server opted in to the LLM-driven `__authenticate` flow via its Firestore
|
|
3958
|
+
* `useAuthenticateTool` field. Servers opted in this way (or, org-wide, via
|
|
3959
|
+
* the `codegen-tool-mcp-authenticate` LD flag) get a synthetic auth tool
|
|
3960
|
+
* instead of a blocking `mcp-auth-required` event.
|
|
3961
|
+
*/
|
|
3962
|
+
useAuthenticateTool?: boolean;
|
|
3953
3963
|
}
|
|
3954
3964
|
export interface GenerateCodeEventMCPAuthRequired {
|
|
3955
3965
|
type: "mcp-auth-required";
|
package/src/codegen.js
CHANGED
|
@@ -1229,7 +1229,7 @@ export const SendMessageToolInputSchema = z
|
|
|
1229
1229
|
description: "When true, send the response as a voice message using text-to-speech. Only supported for Telegram channels. Only set to true when the user's original message was a voice/audio message (look for '[Voice message transcription]' or '[Audio' markers), the channel is Telegram, and the response is short and conversational with no URLs, code, lists, or other content that doesn't translate to audio. Default to false (text) for all text-originated messages.",
|
|
1230
1230
|
}),
|
|
1231
1231
|
from_user_id: z.string().optional().meta({
|
|
1232
|
-
description: "Builder.io user ID this message is from / should be attributed to. Only allowed when channel_id is 'builder/branch/{project_id}/{branch_name}'. When set, the message is delivered to the target branch as coming from this user (role 'user') instead of from the agent.
|
|
1232
|
+
description: "Builder.io user ID this message is from / should be attributed to. Only allowed when channel_id is 'builder/branch/{project_id}/{branch_name}'. When set, the message is delivered to the target branch as coming from this user (role 'user') instead of from the agent. Defaults to the current requester when omitted. Set this explicitly when relaying a message on behalf of someone other than the person who sent the current message — even if it was composed, summarized, or merged from multiple people.",
|
|
1233
1233
|
}),
|
|
1234
1234
|
})
|
|
1235
1235
|
.meta({ title: "SendMessageToolInput" });
|
|
@@ -1918,6 +1918,7 @@ export const CodeGenInputOptionsSchema = z
|
|
|
1918
1918
|
systemReminderPrompt: z.string().optional(),
|
|
1919
1919
|
})
|
|
1920
1920
|
.meta({ title: "CodeGenInputOptions" });
|
|
1921
|
+
export const FUSION_GENERATION_APP_PLACEHOLDER_COMMENT = "{/* TODO: FUSION_GENERATION_APP_PLACEHOLDER replace everything here with the actual app! */}";
|
|
1921
1922
|
export const AutoPushModeSchema = z
|
|
1922
1923
|
.enum(["force-push", "merge-push", "ff-push", "safe-push", "none"])
|
|
1923
1924
|
.meta({ title: "AutoPushMode" });
|
package/src/completion.d.ts
CHANGED
|
@@ -99,7 +99,8 @@ export interface BuilderEditorState {
|
|
|
99
99
|
*/
|
|
100
100
|
content?: BuilderContent;
|
|
101
101
|
/**
|
|
102
|
-
* Builder
|
|
102
|
+
* Components available in the Builder editor, including built-ins.
|
|
103
|
+
* When present, editor AI restricts generated content to this list.
|
|
103
104
|
*/
|
|
104
105
|
components?: Component[];
|
|
105
106
|
/**
|
|
@@ -7,5 +7,12 @@ export interface HttpCheckOptions {
|
|
|
7
7
|
fetchFn?: ConnectivityFetchFn;
|
|
8
8
|
/** Fetch dispatcher for proxy routing (e.g. undici ProxyAgent). */
|
|
9
9
|
dispatcher?: object;
|
|
10
|
+
/**
|
|
11
|
+
* Treat any 4xx as a failure (not just 5xx). Used by `doctor --browser`: a
|
|
12
|
+
* TLS-inspecting proxy (Zscaler) that blocks browser-shaped traffic typically
|
|
13
|
+
* answers with a 403/407/451 block page, which a plain reachability check
|
|
14
|
+
* would otherwise count as a pass.
|
|
15
|
+
*/
|
|
16
|
+
strictHttpStatus?: boolean;
|
|
10
17
|
}
|
|
11
18
|
export declare function httpCheck(options: HttpCheckOptions): Promise<CheckResult>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { mapFetchErrorToConnectivityCode } from "../error-codes.js";
|
|
1
|
+
import { mapFetchErrorToConnectivityCode, mapHttpStatusToErrorCode, } from "../error-codes.js";
|
|
2
2
|
import { isBrowser } from "../environment.js";
|
|
3
3
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
4
4
|
const LATENCY_THRESHOLD_MS = 5000;
|
|
5
5
|
export async function httpCheck(options) {
|
|
6
|
-
|
|
6
|
+
var _a;
|
|
7
|
+
const { target, source, testId, timeout = DEFAULT_TIMEOUT_MS, fetchFn = fetch, dispatcher, strictHttpStatus = false, } = options;
|
|
7
8
|
const startTime = Date.now();
|
|
8
9
|
const controller = new AbortController();
|
|
9
10
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
@@ -46,14 +47,21 @@ export async function httpCheck(options) {
|
|
|
46
47
|
clearTimeout(timeoutId);
|
|
47
48
|
const durationMs = Date.now() - startTime;
|
|
48
49
|
const hasHighLatency = durationMs > LATENCY_THRESHOLD_MS;
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// Standard mode only fails on 5xx (server reachable is what matters).
|
|
51
|
+
// Browser (strict) mode also fails on 4xx to surface proxy block pages.
|
|
52
|
+
const isFailingStatus = strictHttpStatus
|
|
53
|
+
? response.status >= 400
|
|
54
|
+
: response.status >= 500;
|
|
55
|
+
if (isFailingStatus) {
|
|
51
56
|
return {
|
|
52
57
|
source,
|
|
53
58
|
testId,
|
|
54
59
|
target,
|
|
55
60
|
passed: false,
|
|
56
|
-
|
|
61
|
+
// Canonical status->code mapping: 401/403/404/407/503/5xx get specific
|
|
62
|
+
// codes; other 4xx (400/405/429/...) map to "unknown_error" instead of
|
|
63
|
+
// being mislabeled as a specific auth failure.
|
|
64
|
+
errorCode: (_a = mapHttpStatusToErrorCode(response.status)) !== null && _a !== void 0 ? _a : "unknown_error",
|
|
57
65
|
durationMs,
|
|
58
66
|
metadata: {
|
|
59
67
|
statusCode: response.status,
|
|
@@ -24,3 +24,61 @@ describe("httpCheck", () => {
|
|
|
24
24
|
expect(result.metadata).toMatchObject({ reachabilityOnly: true });
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
|
+
describe("httpCheck strictHttpStatus", () => {
|
|
28
|
+
const target = "https://api.builder.io/codegen/health";
|
|
29
|
+
it("treats 403 as a pass by default (server reachable)", async () => {
|
|
30
|
+
const fetchFn = vi
|
|
31
|
+
.fn()
|
|
32
|
+
.mockResolvedValue({ ok: false, status: 403, statusText: "Forbidden" });
|
|
33
|
+
const result = await httpCheck({
|
|
34
|
+
target,
|
|
35
|
+
source: "local",
|
|
36
|
+
testId: "api.builder.io",
|
|
37
|
+
fetchFn,
|
|
38
|
+
});
|
|
39
|
+
expect(result.passed).toBe(true);
|
|
40
|
+
expect(result.metadata).toMatchObject({ statusCode: 403 });
|
|
41
|
+
});
|
|
42
|
+
it("treats 403 as a failure in strict mode (proxy block page)", async () => {
|
|
43
|
+
const fetchFn = vi
|
|
44
|
+
.fn()
|
|
45
|
+
.mockResolvedValue({ ok: false, status: 403, statusText: "Forbidden" });
|
|
46
|
+
const result = await httpCheck({
|
|
47
|
+
target,
|
|
48
|
+
source: "local",
|
|
49
|
+
testId: "api.builder.io",
|
|
50
|
+
fetchFn,
|
|
51
|
+
strictHttpStatus: true,
|
|
52
|
+
});
|
|
53
|
+
expect(result.passed).toBe(false);
|
|
54
|
+
expect(result.errorCode).toBe("http_forbidden");
|
|
55
|
+
expect(result.metadata).toMatchObject({ statusCode: 403 });
|
|
56
|
+
});
|
|
57
|
+
it("maps an unhandled 4xx (429) to unknown_error, not http_forbidden", async () => {
|
|
58
|
+
const fetchFn = vi
|
|
59
|
+
.fn()
|
|
60
|
+
.mockResolvedValue({ ok: false, status: 429, statusText: "Too Many" });
|
|
61
|
+
const result = await httpCheck({
|
|
62
|
+
target,
|
|
63
|
+
source: "local",
|
|
64
|
+
testId: "api.builder.io",
|
|
65
|
+
fetchFn,
|
|
66
|
+
strictHttpStatus: true,
|
|
67
|
+
});
|
|
68
|
+
expect(result.passed).toBe(false);
|
|
69
|
+
expect(result.errorCode).toBe("unknown_error");
|
|
70
|
+
});
|
|
71
|
+
it("still passes on 200 in strict mode", async () => {
|
|
72
|
+
const fetchFn = vi
|
|
73
|
+
.fn()
|
|
74
|
+
.mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
|
|
75
|
+
const result = await httpCheck({
|
|
76
|
+
target,
|
|
77
|
+
source: "local",
|
|
78
|
+
testId: "api.builder.io",
|
|
79
|
+
fetchFn,
|
|
80
|
+
strictHttpStatus: true,
|
|
81
|
+
});
|
|
82
|
+
expect(result.passed).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -13,6 +13,12 @@ export function getCheckTypeForTestId(testId) {
|
|
|
13
13
|
if (testId.startsWith("git-host:")) {
|
|
14
14
|
return testId.replace("git-host:", "");
|
|
15
15
|
}
|
|
16
|
+
if (testId.startsWith("project-health:")) {
|
|
17
|
+
return testId.replace("project-health:", "");
|
|
18
|
+
}
|
|
19
|
+
if (testId.startsWith("project:")) {
|
|
20
|
+
return testId.replace("project:", "");
|
|
21
|
+
}
|
|
16
22
|
return "http";
|
|
17
23
|
}
|
|
18
24
|
export function isCheckAvailable(checkType) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { Source, TestId, Test, RunChecksInput, ProgressEvent, CheckResult, CheckReport, ConnectivityErrorCode, CheckType, Recommendation, LikelyCause, ConnectivityStatus, AnalysisResult, AnalyzeConnectivityInput, ConnectivityFetchFn, } from "./types.js";
|
|
2
2
|
export { runChecks } from "./run-checks.js";
|
|
3
3
|
export { mapNodeErrorToConnectivityCode, mapHttpStatusToErrorCode, mapFetchErrorToConnectivityCode, connectivityErrorCodeToLikelyCause, mapConnectivityErrorMessage, SELF_SIGNED_CERT_ERRORS, CERT_EXPIRED_ERRORS, CERT_NOT_YET_VALID_ERRORS, CERT_INVALID_ERRORS, CERT_HOSTNAME_MISMATCH_ERRORS, SSL_PROTOCOL_ERRORS, SSL_HANDSHAKE_ERRORS, NETWORK_UNREACHABLE_ERRORS, TIMEOUT_ERRORS, PROXY_ERRORS, DNS_ERRORS, } from "./error-codes.js";
|
|
4
|
-
export { BUILDER_TARGETS, DEFAULT_PORTS, DEFAULT_LOCAL_BUILDER_TESTS, BUILDER_TEST_DISPLAY_NAMES, resolveTarget, extractHostname, extractPort, } from "./targets.js";
|
|
4
|
+
export { BUILDER_TARGETS, DEFAULT_PORTS, DEFAULT_LOCAL_BUILDER_TESTS, BUILDER_TEST_DISPLAY_NAMES, resolveTarget, getProjectHealthUrl, extractHostname, extractPort, } from "./targets.js";
|
|
5
5
|
export { isBrowser, isNode, getCheckTypeForTestId, isCheckAvailable, getUnavailabilityReason, } from "./environment.js";
|
|
6
6
|
export { httpCheck } from "./checks/http-check.js";
|
|
7
7
|
export type { HttpCheckOptions } from "./checks/http-check.js";
|
package/src/connectivity/node.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { runChecks } from "./run-checks.js";
|
|
2
2
|
export { mapNodeErrorToConnectivityCode, mapHttpStatusToErrorCode, mapFetchErrorToConnectivityCode, connectivityErrorCodeToLikelyCause, mapConnectivityErrorMessage, SELF_SIGNED_CERT_ERRORS, CERT_EXPIRED_ERRORS, CERT_NOT_YET_VALID_ERRORS, CERT_INVALID_ERRORS, CERT_HOSTNAME_MISMATCH_ERRORS, SSL_PROTOCOL_ERRORS, SSL_HANDSHAKE_ERRORS, NETWORK_UNREACHABLE_ERRORS, TIMEOUT_ERRORS, PROXY_ERRORS, DNS_ERRORS, } from "./error-codes.js";
|
|
3
|
-
export { BUILDER_TARGETS, DEFAULT_PORTS, DEFAULT_LOCAL_BUILDER_TESTS, BUILDER_TEST_DISPLAY_NAMES, resolveTarget, extractHostname, extractPort, } from "./targets.js";
|
|
3
|
+
export { BUILDER_TARGETS, DEFAULT_PORTS, DEFAULT_LOCAL_BUILDER_TESTS, BUILDER_TEST_DISPLAY_NAMES, resolveTarget, getProjectHealthUrl, extractHostname, extractPort, } from "./targets.js";
|
|
4
4
|
export { isBrowser, isNode, getCheckTypeForTestId, isCheckAvailable, getUnavailabilityReason, } from "./environment.js";
|
|
5
5
|
export { httpCheck } from "./checks/http-check.js";
|
|
6
6
|
export { dnsCheck } from "./checks/dns-check.js";
|
|
@@ -7,7 +7,7 @@ import { tcpCheck } from "./checks/tcp-check.js";
|
|
|
7
7
|
import { tlsCheck } from "./checks/tls-check.js";
|
|
8
8
|
import { sshCheck } from "./checks/ssh-check.js";
|
|
9
9
|
export async function runChecks(input) {
|
|
10
|
-
const { tests, gitHost, onProgress, fetchFn, dispatcher, connectFn, sshConnectFn, dnsResolver, } = input;
|
|
10
|
+
const { tests, gitHost, onProgress, fetchFn, dispatcher, connectFn, sshConnectFn, dnsResolver, projectUrl, projectHealthUrl, strictHttpStatus, } = input;
|
|
11
11
|
const results = [];
|
|
12
12
|
const total = tests.length;
|
|
13
13
|
for (let index = 0; index < tests.length; index++) {
|
|
@@ -18,7 +18,7 @@ export async function runChecks(input) {
|
|
|
18
18
|
index,
|
|
19
19
|
total,
|
|
20
20
|
});
|
|
21
|
-
const result = await runSingleCheck(test, gitHost, fetchFn, dispatcher, connectFn, sshConnectFn, dnsResolver);
|
|
21
|
+
const result = await runSingleCheck(test, gitHost, fetchFn, dispatcher, connectFn, sshConnectFn, dnsResolver, { projectUrl, projectHealthUrl, strictHttpStatus });
|
|
22
22
|
results.push(result);
|
|
23
23
|
emitProgress(onProgress, {
|
|
24
24
|
type: "test:complete",
|
|
@@ -37,14 +37,14 @@ export async function runChecks(input) {
|
|
|
37
37
|
results,
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
async function runSingleCheck(test, gitHost, fetchFn, dispatcher, connectFn, sshConnectFn, dnsResolver) {
|
|
40
|
+
async function runSingleCheck(test, gitHost, fetchFn, dispatcher, connectFn, sshConnectFn, dnsResolver, extras) {
|
|
41
41
|
const { source, testId } = test;
|
|
42
42
|
const checkType = getCheckTypeForTestId(testId);
|
|
43
43
|
if (!isCheckAvailable(checkType)) {
|
|
44
44
|
const startTime = Date.now();
|
|
45
45
|
let target;
|
|
46
46
|
try {
|
|
47
|
-
target = resolveTarget(testId, gitHost);
|
|
47
|
+
target = resolveTarget(testId, gitHost, extras);
|
|
48
48
|
}
|
|
49
49
|
catch (_a) {
|
|
50
50
|
target = gitHost || testId;
|
|
@@ -64,7 +64,7 @@ async function runSingleCheck(test, gitHost, fetchFn, dispatcher, connectFn, ssh
|
|
|
64
64
|
}
|
|
65
65
|
let target;
|
|
66
66
|
try {
|
|
67
|
-
target = resolveTarget(testId, gitHost);
|
|
67
|
+
target = resolveTarget(testId, gitHost, extras);
|
|
68
68
|
}
|
|
69
69
|
catch (error) {
|
|
70
70
|
return {
|
|
@@ -81,7 +81,14 @@ async function runSingleCheck(test, gitHost, fetchFn, dispatcher, connectFn, ssh
|
|
|
81
81
|
}
|
|
82
82
|
switch (checkType) {
|
|
83
83
|
case "http":
|
|
84
|
-
return httpCheck({
|
|
84
|
+
return httpCheck({
|
|
85
|
+
target,
|
|
86
|
+
source,
|
|
87
|
+
testId,
|
|
88
|
+
fetchFn,
|
|
89
|
+
dispatcher,
|
|
90
|
+
strictHttpStatus: extras === null || extras === void 0 ? void 0 : extras.strictHttpStatus,
|
|
91
|
+
});
|
|
85
92
|
case "websocket":
|
|
86
93
|
return websocketCheck({ target, source, testId });
|
|
87
94
|
case "dns":
|
|
@@ -12,8 +12,18 @@ export declare const DEFAULT_LOCAL_BUILDER_TESTS: Test[];
|
|
|
12
12
|
* UI surfaces to show friendly labels instead of raw testId strings.
|
|
13
13
|
*/
|
|
14
14
|
export declare const BUILDER_TEST_DISPLAY_NAMES: Partial<Record<TestId, string>>;
|
|
15
|
+
/**
|
|
16
|
+
* Given a full project URL (e.g. https://<id>-<branch>.builderio.dev/... ), return
|
|
17
|
+
* the matching health.builderio.* URL the project is routed through, or null when
|
|
18
|
+
* the host is not a known Builder kube domain.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getProjectHealthUrl(projectUrl: string): string | null;
|
|
15
21
|
export declare const DEFAULT_PORTS: Record<string, number>;
|
|
16
|
-
export
|
|
22
|
+
export interface ResolveTargetExtras {
|
|
23
|
+
projectUrl?: string;
|
|
24
|
+
projectHealthUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function resolveTarget(testId: TestId, gitHost?: string, extras?: ResolveTargetExtras): string;
|
|
17
27
|
export declare function extractHostname(target: string): string;
|
|
18
28
|
/**
|
|
19
29
|
* Extract only an explicitly specified port from a URL, ignoring protocol-specific defaults.
|
|
@@ -37,7 +37,28 @@ export const BUILDER_TEST_DISPLAY_NAMES = {
|
|
|
37
37
|
"builderio.xyz:ws": "*.builderio.xyz (WebSocket)",
|
|
38
38
|
"builderio.dev": "*.builderio.dev",
|
|
39
39
|
"builderio.dev:ws": "*.builderio.dev (WebSocket)",
|
|
40
|
+
"project:dns": "Project URL (DNS)",
|
|
41
|
+
"project:tcp": "Project URL (TCP)",
|
|
42
|
+
"project:tls": "Project URL (TLS)",
|
|
43
|
+
"project:http": "Project URL (HTTP)",
|
|
44
|
+
"project-health:dns": "Health domain (DNS)",
|
|
45
|
+
"project-health:tcp": "Health domain (TCP)",
|
|
46
|
+
"project-health:tls": "Health domain (TLS)",
|
|
47
|
+
"project-health:http": "Health domain (HTTP)",
|
|
40
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* Given a full project URL (e.g. https://<id>-<branch>.builderio.dev/... ), return
|
|
51
|
+
* the matching health.builderio.* URL the project is routed through, or null when
|
|
52
|
+
* the host is not a known Builder kube domain.
|
|
53
|
+
*/
|
|
54
|
+
export function getProjectHealthUrl(projectUrl) {
|
|
55
|
+
const host = extractHostname(projectUrl);
|
|
56
|
+
if (host.endsWith("builderio.xyz"))
|
|
57
|
+
return BUILDER_TARGETS["builderio.xyz"];
|
|
58
|
+
if (host.endsWith("builderio.dev"))
|
|
59
|
+
return BUILDER_TARGETS["builderio.dev"];
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
41
62
|
export const DEFAULT_PORTS = {
|
|
42
63
|
http: 443,
|
|
43
64
|
https: 443,
|
|
@@ -45,13 +66,25 @@ export const DEFAULT_PORTS = {
|
|
|
45
66
|
tcp: 443,
|
|
46
67
|
tls: 443,
|
|
47
68
|
};
|
|
48
|
-
export function resolveTarget(testId, gitHost) {
|
|
69
|
+
export function resolveTarget(testId, gitHost, extras) {
|
|
49
70
|
if (testId.startsWith("git-host:")) {
|
|
50
71
|
if (!gitHost) {
|
|
51
72
|
throw new Error(`gitHost parameter is required for test "${testId}"`);
|
|
52
73
|
}
|
|
53
74
|
return gitHost;
|
|
54
75
|
}
|
|
76
|
+
if (testId.startsWith("project-health:")) {
|
|
77
|
+
if (!(extras === null || extras === void 0 ? void 0 : extras.projectHealthUrl)) {
|
|
78
|
+
throw new Error(`projectHealthUrl is required for test "${testId}"`);
|
|
79
|
+
}
|
|
80
|
+
return extras.projectHealthUrl;
|
|
81
|
+
}
|
|
82
|
+
if (testId.startsWith("project:")) {
|
|
83
|
+
if (!(extras === null || extras === void 0 ? void 0 : extras.projectUrl)) {
|
|
84
|
+
throw new Error(`projectUrl is required for test "${testId}"`);
|
|
85
|
+
}
|
|
86
|
+
return extras.projectUrl;
|
|
87
|
+
}
|
|
55
88
|
const target = BUILDER_TARGETS[testId];
|
|
56
89
|
if (!target) {
|
|
57
90
|
throw new Error(`Unknown testId: ${testId}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { extractPort, extractExplicitPort, BUILDER_TARGETS, DEFAULT_LOCAL_BUILDER_TESTS, BUILDER_TEST_DISPLAY_NAMES, } from "./targets";
|
|
2
|
+
import { extractPort, extractExplicitPort, BUILDER_TARGETS, DEFAULT_LOCAL_BUILDER_TESTS, BUILDER_TEST_DISPLAY_NAMES, resolveTarget, getProjectHealthUrl, } from "./targets";
|
|
3
|
+
import { getCheckTypeForTestId } from "./environment";
|
|
3
4
|
describe("extractPort", () => {
|
|
4
5
|
it("returns explicit port from URL", () => {
|
|
5
6
|
expect(extractPort("https://git.amazon.com:2222", 443)).toBe(2222);
|
|
@@ -76,3 +77,52 @@ describe("BUILDER_TEST_DISPLAY_NAMES", () => {
|
|
|
76
77
|
expect(BUILDER_TEST_DISPLAY_NAMES["builderio.dev:ws"]).toBe("*.builderio.dev (WebSocket)");
|
|
77
78
|
});
|
|
78
79
|
});
|
|
80
|
+
describe("resolveTarget — per-project tests", () => {
|
|
81
|
+
const PROJECT_URL = "https://9739e085f64844e09c9918ce06fa57f5-main.builderio.dev/_builder.io/api/status-v2";
|
|
82
|
+
const HEALTH_URL = "https://health.builderio.dev/health";
|
|
83
|
+
it("resolves project:* testIds to the provided projectUrl", () => {
|
|
84
|
+
for (const id of [
|
|
85
|
+
"project:dns",
|
|
86
|
+
"project:tcp",
|
|
87
|
+
"project:tls",
|
|
88
|
+
"project:http",
|
|
89
|
+
]) {
|
|
90
|
+
expect(resolveTarget(id, undefined, { projectUrl: PROJECT_URL })).toBe(PROJECT_URL);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
it("resolves project-health:* testIds to the provided projectHealthUrl", () => {
|
|
94
|
+
for (const id of [
|
|
95
|
+
"project-health:dns",
|
|
96
|
+
"project-health:tcp",
|
|
97
|
+
"project-health:tls",
|
|
98
|
+
"project-health:http",
|
|
99
|
+
]) {
|
|
100
|
+
expect(resolveTarget(id, undefined, { projectHealthUrl: HEALTH_URL })).toBe(HEALTH_URL);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
it("throws when the required project URL is missing", () => {
|
|
104
|
+
expect(() => resolveTarget("project:dns")).toThrow();
|
|
105
|
+
expect(() => resolveTarget("project-health:dns")).toThrow();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("getProjectHealthUrl", () => {
|
|
109
|
+
it("maps a .dev project URL to the .dev health endpoint", () => {
|
|
110
|
+
expect(getProjectHealthUrl("https://abc-main.builderio.dev/_builder.io/api/status-v2")).toBe("https://health.builderio.dev/health");
|
|
111
|
+
});
|
|
112
|
+
it("maps a .xyz project URL to the .xyz health endpoint", () => {
|
|
113
|
+
expect(getProjectHealthUrl("https://abc-main.builderio.xyz")).toBe("https://health.builderio.xyz/health");
|
|
114
|
+
});
|
|
115
|
+
it("returns null for a non-kube domain", () => {
|
|
116
|
+
expect(getProjectHealthUrl("https://example.com/foo")).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("getCheckTypeForTestId — per-project tests", () => {
|
|
120
|
+
it("derives the check type from the project testId suffix", () => {
|
|
121
|
+
expect(getCheckTypeForTestId("project:dns")).toBe("dns");
|
|
122
|
+
expect(getCheckTypeForTestId("project:tcp")).toBe("tcp");
|
|
123
|
+
expect(getCheckTypeForTestId("project:tls")).toBe("tls");
|
|
124
|
+
expect(getCheckTypeForTestId("project:http")).toBe("http");
|
|
125
|
+
expect(getCheckTypeForTestId("project-health:dns")).toBe("dns");
|
|
126
|
+
expect(getCheckTypeForTestId("project-health:tls")).toBe("tls");
|
|
127
|
+
});
|
|
128
|
+
});
|