@builder.io/ai-utils 0.89.0 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/editor-ai.d.ts +10 -0
- package/src/editor-ai.js +12 -0
- package/src/organization.d.ts +33 -0
- package/src/projects.d.ts +147 -1
- package/src/projects.js +75 -0
- package/src/single-tenancy.d.ts +164 -13
- package/src/single-tenancy.js +59 -1
- package/src/single-tenancy.test.d.ts +1 -0
- package/src/single-tenancy.test.js +97 -0
package/package.json
CHANGED
package/src/editor-ai.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const EDITOR_AI_READ_DEFAULT_LIMIT = 250;
|
|
3
3
|
export declare const EDITOR_AI_READ_MAX_LIMIT = 1500;
|
|
4
|
+
export declare const EditorAiComponentSchema: z.ZodObject<{
|
|
5
|
+
name: z.ZodString;
|
|
6
|
+
}, z.core.$loose>;
|
|
7
|
+
export type EditorAiComponent = z.infer<typeof EditorAiComponentSchema>;
|
|
4
8
|
export declare const EditorAiReadRequestSchema: z.ZodObject<{
|
|
5
9
|
contentId: z.ZodString;
|
|
6
10
|
offset: z.ZodOptional<z.ZodNumber>;
|
|
@@ -22,6 +26,9 @@ export declare const EditorAiEditRequestSchema: z.ZodObject<{
|
|
|
22
26
|
old_str: z.ZodString;
|
|
23
27
|
new_str: z.ZodString;
|
|
24
28
|
activeLocale: z.ZodOptional<z.ZodString>;
|
|
29
|
+
components: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
30
|
+
name: z.ZodString;
|
|
31
|
+
}, z.core.$loose>>>;
|
|
25
32
|
}, z.core.$strip>;
|
|
26
33
|
export type EditorAiEditRequest = z.infer<typeof EditorAiEditRequestSchema>;
|
|
27
34
|
export declare const EditorAiEditResponseSchema: z.ZodObject<{
|
|
@@ -34,6 +41,9 @@ export declare const EditorAiWriteRequestSchema: z.ZodObject<{
|
|
|
34
41
|
contentId: z.ZodString;
|
|
35
42
|
content: z.ZodString;
|
|
36
43
|
activeLocale: z.ZodOptional<z.ZodString>;
|
|
44
|
+
components: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
45
|
+
name: z.ZodString;
|
|
46
|
+
}, z.core.$loose>>>;
|
|
37
47
|
}, z.core.$strip>;
|
|
38
48
|
export type EditorAiWriteRequest = z.infer<typeof EditorAiWriteRequestSchema>;
|
|
39
49
|
export declare const EditorAiWriteResponseSchema: z.ZodObject<{
|
package/src/editor-ai.js
CHANGED
|
@@ -3,6 +3,16 @@ import { z } from "zod";
|
|
|
3
3
|
// endpoints. Shared so the Builder CMS MCP server reuses the same shapes.
|
|
4
4
|
export const EDITOR_AI_READ_DEFAULT_LIMIT = 250;
|
|
5
5
|
export const EDITOR_AI_READ_MAX_LIMIT = 1500;
|
|
6
|
+
// Registered component names are sanitized into valid JSX identifiers when
|
|
7
|
+
// content is serialized ("Hero Banner" -> "Hero_Banner"). Blocks already in the
|
|
8
|
+
// content restore from their own stored name, but a block the model introduces
|
|
9
|
+
// for the first time can only be mapped back via this catalog.
|
|
10
|
+
export const EditorAiComponentSchema = z
|
|
11
|
+
.looseObject({
|
|
12
|
+
name: z.string().min(1),
|
|
13
|
+
})
|
|
14
|
+
.meta({ title: "EditorAiComponent" });
|
|
15
|
+
const componentsField = z.array(EditorAiComponentSchema).optional();
|
|
6
16
|
export const EditorAiReadRequestSchema = z
|
|
7
17
|
.object({
|
|
8
18
|
contentId: z.string().min(1),
|
|
@@ -34,6 +44,7 @@ export const EditorAiEditRequestSchema = z
|
|
|
34
44
|
new_str: z.string(),
|
|
35
45
|
// Locale to operate on; defaults to "Default" when omitted.
|
|
36
46
|
activeLocale: z.string().min(1).optional(),
|
|
47
|
+
components: componentsField,
|
|
37
48
|
})
|
|
38
49
|
.meta({ title: "EditorAiEditRequest" });
|
|
39
50
|
export const EditorAiEditResponseSchema = z
|
|
@@ -49,6 +60,7 @@ export const EditorAiWriteRequestSchema = z
|
|
|
49
60
|
content: z.string().min(1),
|
|
50
61
|
// Locale to operate on; defaults to "Default" when omitted.
|
|
51
62
|
activeLocale: z.string().min(1).optional(),
|
|
63
|
+
components: componentsField,
|
|
52
64
|
})
|
|
53
65
|
.meta({ title: "EditorAiWriteRequest" });
|
|
54
66
|
export const EditorAiWriteResponseSchema = z
|
package/src/organization.d.ts
CHANGED
|
@@ -243,6 +243,38 @@ export interface Coupon extends SubscriptionInfo {
|
|
|
243
243
|
id: string;
|
|
244
244
|
plan: keyof SubscriptionInfoMap;
|
|
245
245
|
}
|
|
246
|
+
export type BuilderAuthRule = {
|
|
247
|
+
type: "builderAuth";
|
|
248
|
+
id: string;
|
|
249
|
+
} & ({
|
|
250
|
+
scope: "orgMember";
|
|
251
|
+
} | {
|
|
252
|
+
scope: "namedUsers";
|
|
253
|
+
userIds: string[];
|
|
254
|
+
});
|
|
255
|
+
export type SiteAccessRule = {
|
|
256
|
+
type: "password";
|
|
257
|
+
id: string;
|
|
258
|
+
passwordHash: string;
|
|
259
|
+
} | BuilderAuthRule | {
|
|
260
|
+
type: "ip";
|
|
261
|
+
id: string;
|
|
262
|
+
cidrs: string[];
|
|
263
|
+
};
|
|
264
|
+
export interface SiteAccessControl {
|
|
265
|
+
enabled: boolean;
|
|
266
|
+
combinator: "any" | "all";
|
|
267
|
+
rules: SiteAccessRule[];
|
|
268
|
+
tokenVersion?: number;
|
|
269
|
+
}
|
|
270
|
+
export interface SiteSessionClaims {
|
|
271
|
+
userId: string;
|
|
272
|
+
slug: string;
|
|
273
|
+
/** Project-level policy version at mint time. */
|
|
274
|
+
tokenVersion: number;
|
|
275
|
+
/** Org-level policy version at mint time; an org rotation invalidates the session too. */
|
|
276
|
+
orgTokenVersion: number;
|
|
277
|
+
}
|
|
246
278
|
export interface FeatureMap {
|
|
247
279
|
abTesting?: boolean;
|
|
248
280
|
metrics?: boolean;
|
|
@@ -321,6 +353,7 @@ export interface FeatureMap {
|
|
|
321
353
|
customDockerImages?: boolean;
|
|
322
354
|
performanceInfrastructure?: boolean;
|
|
323
355
|
previewPasswordProtection?: boolean;
|
|
356
|
+
siteAccessControl?: boolean;
|
|
324
357
|
publicPreviews?: boolean;
|
|
325
358
|
privacyMode?: boolean;
|
|
326
359
|
usageInsights?: boolean;
|
package/src/projects.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import type { BuilderConfigHosting } from "./builder-config.js";
|
|
3
3
|
import type { ConnectivityErrorCode, CheckType, LikelyCause } from "./connectivity/types.js";
|
|
4
4
|
import type { LaunchServerState, LaunchServerStatus, BranchBackup, CommitMode, GitSnapshot, GenerateUserMessage, CodeGenToolMap, GenerateCompletionStep, ExitState } from "./codegen";
|
|
5
|
-
import type { FallbackTokensPrivate } from "./organization";
|
|
5
|
+
import type { FallbackTokensPrivate, SiteAccessControl } from "./organization";
|
|
6
6
|
/**
|
|
7
7
|
* Temporary type for date fields during migration.
|
|
8
8
|
* Handles both old (string) and new (number) formats.
|
|
@@ -1720,6 +1720,24 @@ export interface OrganizationPrivate {
|
|
|
1720
1720
|
updatedAt: number;
|
|
1721
1721
|
fallbackTokens?: FallbackTokensPrivate;
|
|
1722
1722
|
fusionWebhookSecrets?: Record<string, string>;
|
|
1723
|
+
/**
|
|
1724
|
+
* Full org-default hosted-site access policy (rules + scrypt password hashes).
|
|
1725
|
+
* Server-only: never exposed to clients. UI reads via
|
|
1726
|
+
* GET /projects/hosting/site-access.
|
|
1727
|
+
*/
|
|
1728
|
+
siteAccessControl?: SiteAccessControl;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Server-only per-project document (`projects_private/{projectId}`), mirroring
|
|
1732
|
+
* `organizations_private`. Holds secrets that must never reach clients.
|
|
1733
|
+
*/
|
|
1734
|
+
export interface ProjectPrivate {
|
|
1735
|
+
projectId: string;
|
|
1736
|
+
/**
|
|
1737
|
+
* Full project-level hosted-site access policy (rules + scrypt password
|
|
1738
|
+
* hashes). Server-only; UI reads via GET /projects/hosting/site-access.
|
|
1739
|
+
*/
|
|
1740
|
+
siteAccessControl?: SiteAccessControl;
|
|
1723
1741
|
}
|
|
1724
1742
|
export interface CreateProjectOptions {
|
|
1725
1743
|
name?: string;
|
|
@@ -3037,6 +3055,134 @@ export declare const CloneProjectOptionsSchema: z.ZodObject<{
|
|
|
3037
3055
|
useKube: z.ZodDefault<z.ZodBoolean>;
|
|
3038
3056
|
}, z.core.$strip>;
|
|
3039
3057
|
export type CloneProjectOptions = z.infer<typeof CloneProjectOptionsSchema>;
|
|
3058
|
+
/**
|
|
3059
|
+
* Write-time contract for a single hosted-site access rule. Mirrors the stored
|
|
3060
|
+
* {@link SiteAccessRule} in `organization.ts`, except a password rule carries a
|
|
3061
|
+
* **plaintext** `password` (write-only — scrypt hashing happens server-side)
|
|
3062
|
+
* instead of `passwordHash`, and `id` is optional (the server assigns stable
|
|
3063
|
+
* ids to new rules). Omitting `password` on an existing rule (matched by `id`)
|
|
3064
|
+
* preserves the stored hash. `organization.ts` remains the source of truth for
|
|
3065
|
+
* the persisted shape.
|
|
3066
|
+
*/
|
|
3067
|
+
export declare const SiteAccessRuleInputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3068
|
+
type: z.ZodLiteral<"password">;
|
|
3069
|
+
id: z.ZodOptional<z.ZodString>;
|
|
3070
|
+
password: z.ZodOptional<z.ZodString>;
|
|
3071
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
3072
|
+
type: z.ZodLiteral<"builderAuth">;
|
|
3073
|
+
id: z.ZodOptional<z.ZodString>;
|
|
3074
|
+
scope: z.ZodEnum<{
|
|
3075
|
+
namedUsers: "namedUsers";
|
|
3076
|
+
orgMember: "orgMember";
|
|
3077
|
+
}>;
|
|
3078
|
+
userIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
3079
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
3080
|
+
type: z.ZodLiteral<"ip">;
|
|
3081
|
+
id: z.ZodOptional<z.ZodString>;
|
|
3082
|
+
cidrs: z.ZodArray<z.ZodString>;
|
|
3083
|
+
}, z.core.$strip>], "type">;
|
|
3084
|
+
export type SiteAccessRuleInput = z.infer<typeof SiteAccessRuleInputSchema>;
|
|
3085
|
+
/** Write-time contract for one level of a site's access boundary. */
|
|
3086
|
+
export declare const SiteAccessControlInputSchema: z.ZodObject<{
|
|
3087
|
+
enabled: z.ZodBoolean;
|
|
3088
|
+
combinator: z.ZodDefault<z.ZodEnum<{
|
|
3089
|
+
all: "all";
|
|
3090
|
+
any: "any";
|
|
3091
|
+
}>>;
|
|
3092
|
+
rules: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3093
|
+
type: z.ZodLiteral<"password">;
|
|
3094
|
+
id: z.ZodOptional<z.ZodString>;
|
|
3095
|
+
password: z.ZodOptional<z.ZodString>;
|
|
3096
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
3097
|
+
type: z.ZodLiteral<"builderAuth">;
|
|
3098
|
+
id: z.ZodOptional<z.ZodString>;
|
|
3099
|
+
scope: z.ZodEnum<{
|
|
3100
|
+
namedUsers: "namedUsers";
|
|
3101
|
+
orgMember: "orgMember";
|
|
3102
|
+
}>;
|
|
3103
|
+
userIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
3104
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
3105
|
+
type: z.ZodLiteral<"ip">;
|
|
3106
|
+
id: z.ZodOptional<z.ZodString>;
|
|
3107
|
+
cidrs: z.ZodArray<z.ZodString>;
|
|
3108
|
+
}, z.core.$strip>], "type">>;
|
|
3109
|
+
}, z.core.$strip>;
|
|
3110
|
+
export type SiteAccessControlInput = z.infer<typeof SiteAccessControlInputSchema>;
|
|
3111
|
+
/**
|
|
3112
|
+
* Request to save a site's access boundary at the project or org-default level.
|
|
3113
|
+
* Handled by `POST /projects/hosting/site-access` — a dedicated write path
|
|
3114
|
+
* because passwords must be hashed server-side, the paid `siteAccessControl`
|
|
3115
|
+
* feature (and enterprise, for `ip` rules) is enforced at save time,
|
|
3116
|
+
* `tokenVersion` is bumped on change, and the Envoy `SecurityPolicy` is
|
|
3117
|
+
* reconciled. The generic project settings update does none of this.
|
|
3118
|
+
*/
|
|
3119
|
+
export declare const SaveSiteAccessControlRequestSchema: z.ZodObject<{
|
|
3120
|
+
level: z.ZodEnum<{
|
|
3121
|
+
org: "org";
|
|
3122
|
+
project: "project";
|
|
3123
|
+
}>;
|
|
3124
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
3125
|
+
organizationId: z.ZodOptional<z.ZodString>;
|
|
3126
|
+
access: z.ZodObject<{
|
|
3127
|
+
enabled: z.ZodBoolean;
|
|
3128
|
+
combinator: z.ZodDefault<z.ZodEnum<{
|
|
3129
|
+
all: "all";
|
|
3130
|
+
any: "any";
|
|
3131
|
+
}>>;
|
|
3132
|
+
rules: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3133
|
+
type: z.ZodLiteral<"password">;
|
|
3134
|
+
id: z.ZodOptional<z.ZodString>;
|
|
3135
|
+
password: z.ZodOptional<z.ZodString>;
|
|
3136
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
3137
|
+
type: z.ZodLiteral<"builderAuth">;
|
|
3138
|
+
id: z.ZodOptional<z.ZodString>;
|
|
3139
|
+
scope: z.ZodEnum<{
|
|
3140
|
+
namedUsers: "namedUsers";
|
|
3141
|
+
orgMember: "orgMember";
|
|
3142
|
+
}>;
|
|
3143
|
+
userIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
3144
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
3145
|
+
type: z.ZodLiteral<"ip">;
|
|
3146
|
+
id: z.ZodOptional<z.ZodString>;
|
|
3147
|
+
cidrs: z.ZodArray<z.ZodString>;
|
|
3148
|
+
}, z.core.$strip>], "type">>;
|
|
3149
|
+
}, z.core.$strip>;
|
|
3150
|
+
}, z.core.$strip>;
|
|
3151
|
+
export type SaveSiteAccessControlRequest = z.infer<typeof SaveSiteAccessControlRequestSchema>;
|
|
3152
|
+
/**
|
|
3153
|
+
* Request to read a site's (sanitized) access boundary for one level. Handled
|
|
3154
|
+
* by `GET /projects/hosting/site-access` so the settings editor can prefill the
|
|
3155
|
+
* configured rules. Full policy lives in server-only storage
|
|
3156
|
+
* (`organizations_private` / `projects_private`).
|
|
3157
|
+
*/
|
|
3158
|
+
export declare const GetSiteAccessControlRequestSchema: z.ZodObject<{
|
|
3159
|
+
level: z.ZodEnum<{
|
|
3160
|
+
org: "org";
|
|
3161
|
+
project: "project";
|
|
3162
|
+
}>;
|
|
3163
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
3164
|
+
organizationId: z.ZodOptional<z.ZodString>;
|
|
3165
|
+
}, z.core.$strip>;
|
|
3166
|
+
export type GetSiteAccessControlRequest = z.infer<typeof GetSiteAccessControlRequestSchema>;
|
|
3167
|
+
/**
|
|
3168
|
+
* Body for `POST /projects/hosting/site-access/exchange`. Trades a Firebase ID
|
|
3169
|
+
* token for a short-lived, site-scoped exchange code (so the ID token never
|
|
3170
|
+
* reaches the site host). Sent as `application/x-www-form-urlencoded`.
|
|
3171
|
+
*/
|
|
3172
|
+
export declare const SiteAccessExchangeRequestSchema: z.ZodObject<{
|
|
3173
|
+
idToken: z.ZodString;
|
|
3174
|
+
host: z.ZodString;
|
|
3175
|
+
}, z.core.$strip>;
|
|
3176
|
+
export type SiteAccessExchangeRequest = z.infer<typeof SiteAccessExchangeRequestSchema>;
|
|
3177
|
+
/**
|
|
3178
|
+
* Body/query for `POST /__builder-site-access/callback`. The exchange `code` is
|
|
3179
|
+
* preferred from the body; query is accepted for defensive compatibility.
|
|
3180
|
+
*/
|
|
3181
|
+
export declare const SiteAccessLoginReturnParamsSchema: z.ZodObject<{
|
|
3182
|
+
code: z.ZodOptional<z.ZodString>;
|
|
3183
|
+
returnTo: z.ZodOptional<z.ZodString>;
|
|
3184
|
+
}, z.core.$strip>;
|
|
3185
|
+
export type SiteAccessLoginReturnParams = z.infer<typeof SiteAccessLoginReturnParamsSchema>;
|
|
3040
3186
|
export declare const DuplicateBranchOptionsSchema: z.ZodObject<{
|
|
3041
3187
|
projectId: z.ZodString;
|
|
3042
3188
|
sourceBranchName: z.ZodString;
|
package/src/projects.js
CHANGED
|
@@ -1609,6 +1609,81 @@ export const CloneProjectOptionsSchema = z.object({
|
|
|
1609
1609
|
description: "Whether to provision on cloud-v2 (Kubernetes). Defaults to false.",
|
|
1610
1610
|
}),
|
|
1611
1611
|
});
|
|
1612
|
+
/**
|
|
1613
|
+
* Write-time contract for a single hosted-site access rule. Mirrors the stored
|
|
1614
|
+
* {@link SiteAccessRule} in `organization.ts`, except a password rule carries a
|
|
1615
|
+
* **plaintext** `password` (write-only — scrypt hashing happens server-side)
|
|
1616
|
+
* instead of `passwordHash`, and `id` is optional (the server assigns stable
|
|
1617
|
+
* ids to new rules). Omitting `password` on an existing rule (matched by `id`)
|
|
1618
|
+
* preserves the stored hash. `organization.ts` remains the source of truth for
|
|
1619
|
+
* the persisted shape.
|
|
1620
|
+
*/
|
|
1621
|
+
export const SiteAccessRuleInputSchema = z.discriminatedUnion("type", [
|
|
1622
|
+
z.object({
|
|
1623
|
+
type: z.literal("password"),
|
|
1624
|
+
id: z.string().optional(),
|
|
1625
|
+
password: z.string().min(1).optional(),
|
|
1626
|
+
}),
|
|
1627
|
+
z.object({
|
|
1628
|
+
type: z.literal("builderAuth"),
|
|
1629
|
+
id: z.string().optional(),
|
|
1630
|
+
scope: z.enum(["orgMember", "namedUsers"]),
|
|
1631
|
+
userIds: z.array(z.string()).optional(),
|
|
1632
|
+
}),
|
|
1633
|
+
z.object({
|
|
1634
|
+
type: z.literal("ip"),
|
|
1635
|
+
id: z.string().optional(),
|
|
1636
|
+
cidrs: z.array(z.string().min(1)),
|
|
1637
|
+
}),
|
|
1638
|
+
]);
|
|
1639
|
+
/** Write-time contract for one level of a site's access boundary. */
|
|
1640
|
+
export const SiteAccessControlInputSchema = z.object({
|
|
1641
|
+
enabled: z.boolean(),
|
|
1642
|
+
combinator: z.enum(["any", "all"]).default("all"),
|
|
1643
|
+
rules: z.array(SiteAccessRuleInputSchema),
|
|
1644
|
+
});
|
|
1645
|
+
/**
|
|
1646
|
+
* Request to save a site's access boundary at the project or org-default level.
|
|
1647
|
+
* Handled by `POST /projects/hosting/site-access` — a dedicated write path
|
|
1648
|
+
* because passwords must be hashed server-side, the paid `siteAccessControl`
|
|
1649
|
+
* feature (and enterprise, for `ip` rules) is enforced at save time,
|
|
1650
|
+
* `tokenVersion` is bumped on change, and the Envoy `SecurityPolicy` is
|
|
1651
|
+
* reconciled. The generic project settings update does none of this.
|
|
1652
|
+
*/
|
|
1653
|
+
export const SaveSiteAccessControlRequestSchema = z.object({
|
|
1654
|
+
level: z.enum(["project", "org"]),
|
|
1655
|
+
projectId: z.string().optional(),
|
|
1656
|
+
organizationId: z.string().optional(),
|
|
1657
|
+
access: SiteAccessControlInputSchema,
|
|
1658
|
+
});
|
|
1659
|
+
/**
|
|
1660
|
+
* Request to read a site's (sanitized) access boundary for one level. Handled
|
|
1661
|
+
* by `GET /projects/hosting/site-access` so the settings editor can prefill the
|
|
1662
|
+
* configured rules. Full policy lives in server-only storage
|
|
1663
|
+
* (`organizations_private` / `projects_private`).
|
|
1664
|
+
*/
|
|
1665
|
+
export const GetSiteAccessControlRequestSchema = z.object({
|
|
1666
|
+
level: z.enum(["project", "org"]),
|
|
1667
|
+
projectId: z.string().optional(),
|
|
1668
|
+
organizationId: z.string().optional(),
|
|
1669
|
+
});
|
|
1670
|
+
/**
|
|
1671
|
+
* Body for `POST /projects/hosting/site-access/exchange`. Trades a Firebase ID
|
|
1672
|
+
* token for a short-lived, site-scoped exchange code (so the ID token never
|
|
1673
|
+
* reaches the site host). Sent as `application/x-www-form-urlencoded`.
|
|
1674
|
+
*/
|
|
1675
|
+
export const SiteAccessExchangeRequestSchema = z.object({
|
|
1676
|
+
idToken: z.string().min(1),
|
|
1677
|
+
host: z.string().min(1),
|
|
1678
|
+
});
|
|
1679
|
+
/**
|
|
1680
|
+
* Body/query for `POST /__builder-site-access/callback`. The exchange `code` is
|
|
1681
|
+
* preferred from the body; query is accepted for defensive compatibility.
|
|
1682
|
+
*/
|
|
1683
|
+
export const SiteAccessLoginReturnParamsSchema = z.object({
|
|
1684
|
+
code: z.string().min(1).optional(),
|
|
1685
|
+
returnTo: z.string().optional(),
|
|
1686
|
+
});
|
|
1612
1687
|
export const DuplicateBranchOptionsSchema = z.object({
|
|
1613
1688
|
projectId: z.string().min(1).meta({
|
|
1614
1689
|
description: "Project that owns the branch being duplicated.",
|
package/src/single-tenancy.d.ts
CHANGED
|
@@ -22,8 +22,53 @@ export interface VpcGcpNetworkAttachment extends VpcConnectionBase {
|
|
|
22
22
|
gcpProjectId: string;
|
|
23
23
|
networkAttachmentName: string;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* One customer-owned VPC Endpoint Service that we create a matching interface
|
|
27
|
+
* VPC Endpoint for, in the shared Builder AWS VPC.
|
|
28
|
+
*/
|
|
29
|
+
export interface PrivateLinkEndpoint {
|
|
30
|
+
/** `com.amazonaws.vpce.<region>.vpce-svc-xxx` — the customer's Endpoint Service. */
|
|
31
|
+
serviceName: string;
|
|
32
|
+
/**
|
|
33
|
+
* Customer-private hostnames that resolve to this endpoint, rendered as dnsmasq
|
|
34
|
+
* `address=/<hostname>/<ip>` overrides on the customer's proxy VM. Needed
|
|
35
|
+
* because a customer Endpoint Service cannot enable private DNS without domain
|
|
36
|
+
* verification.
|
|
37
|
+
*/
|
|
38
|
+
hostnames: string[];
|
|
39
|
+
/** Ports the customer exposes. Scopes the endpoint's AWS security group. */
|
|
40
|
+
ports: number[];
|
|
41
|
+
/**
|
|
42
|
+
* Server-allocated host number for this endpoint's pinned ENI IPs, unique
|
|
43
|
+
* across the shared Builder VPC and never renumbered: reusing an index gives a
|
|
44
|
+
* new endpoint an old one's IP, so a pod holding a stale DNS answer reaches the
|
|
45
|
+
* wrong service. Guarded by the `ip_index` preconditions in
|
|
46
|
+
* packages/terraform/aws-vpc-module/privatelink.tf.
|
|
47
|
+
*/
|
|
48
|
+
ipIndex: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* AWS PrivateLink — the customer provisions an NLB plus a VPC Endpoint Service
|
|
52
|
+
* allowlisting Builder's AWS account; we create the matching interface endpoint
|
|
53
|
+
* in a shared Builder VPC, reached from the customer's GCP proxy VM over the
|
|
54
|
+
* shared GCP↔AWS bridge. Per-service rather than per-VPC, so a config holds N
|
|
55
|
+
* endpoints.
|
|
56
|
+
*/
|
|
57
|
+
export interface VpcAwsPrivateLink extends VpcConnectionBase {
|
|
58
|
+
type: "aws-privatelink";
|
|
59
|
+
/** Must match the region aws-vpc-module runs in ({@link BUILDER_AWS_REGION}). */
|
|
60
|
+
awsRegion: string;
|
|
61
|
+
endpoints: PrivateLinkEndpoint[];
|
|
62
|
+
/**
|
|
63
|
+
* Highest `ipIndex` this config has ever been allocated, retained after the
|
|
64
|
+
* endpoint holding it is removed. Without it, deleting the highest endpoint
|
|
65
|
+
* would lower the next allocation and hand a new endpoint the removed one's
|
|
66
|
+
* pinned IPs. Optional for configs written before it existed.
|
|
67
|
+
*/
|
|
68
|
+
ipIndexWatermark?: number;
|
|
69
|
+
}
|
|
25
70
|
/** Discriminated union of all VPC connectivity types. */
|
|
26
|
-
export type VpcConnection = VpcGcpPeering | VpcGcpNetworkAttachment;
|
|
71
|
+
export type VpcConnection = VpcGcpPeering | VpcGcpNetworkAttachment | VpcAwsPrivateLink;
|
|
27
72
|
/**
|
|
28
73
|
* Dedicated node pool preferences for a single tenancy customer.
|
|
29
74
|
* Independent of VPC connectivity.
|
|
@@ -67,7 +112,23 @@ export interface CreateVpcGcpNetworkAttachmentParams {
|
|
|
67
112
|
networkAttachmentName: string;
|
|
68
113
|
customerDnsServers: string[];
|
|
69
114
|
}
|
|
70
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Endpoint fields a caller supplies. Omits the server-allocated `ipIndex`, which
|
|
117
|
+
* makes the never-renumber invariant structural rather than a validation rule.
|
|
118
|
+
*/
|
|
119
|
+
export type CreatePrivateLinkEndpointParams = Omit<PrivateLinkEndpoint, "ipIndex">;
|
|
120
|
+
export interface CreateVpcAwsPrivateLinkParams {
|
|
121
|
+
type: "aws-privatelink";
|
|
122
|
+
awsRegion: string;
|
|
123
|
+
endpoints: CreatePrivateLinkEndpointParams[];
|
|
124
|
+
/**
|
|
125
|
+
* Unused for PrivateLink, which resolves via static dnsmasq overrides derived
|
|
126
|
+
* from `endpoints[].hostnames` rather than forwarding to customer resolvers.
|
|
127
|
+
* Accepted for parity with the other modes; defaults to `[]`.
|
|
128
|
+
*/
|
|
129
|
+
customerDnsServers?: string[];
|
|
130
|
+
}
|
|
131
|
+
export type CreateSingleTenancyVpcParams = CreateVpcGcpPeeringParams | CreateVpcGcpNetworkAttachmentParams | CreateVpcAwsPrivateLinkParams;
|
|
71
132
|
/**
|
|
72
133
|
* Full create payload — VPC connectivity plus optional dedicated node pool.
|
|
73
134
|
* The {@link CreateSingleTenancyVpcParams} discriminated union still drives
|
|
@@ -79,15 +140,26 @@ export type CreateSingleTenancyConfigOpts = CreateSingleTenancyVpcParams & {
|
|
|
79
140
|
/**
|
|
80
141
|
* Patch payload for an existing single tenancy config.
|
|
81
142
|
* Only the safe-to-edit fields are exposed; `vpc.type`, `vpc.proxyIp`,
|
|
82
|
-
* `vpc.gcpProjectId`, `vpc.gcpNetworkName`, `vpc.networkAttachmentName`,
|
|
83
|
-
* `vpc.bridgeCidr` are immutable because changing them
|
|
84
|
-
* already-provisioned infrastructure.
|
|
143
|
+
* `vpc.gcpProjectId`, `vpc.gcpNetworkName`, `vpc.networkAttachmentName`,
|
|
144
|
+
* `vpc.bridgeCidr`, and `vpc.awsRegion` are immutable because changing them
|
|
145
|
+
* would diverge from already-provisioned infrastructure.
|
|
85
146
|
*/
|
|
86
147
|
export interface UpdateSingleTenancyConfigOpts {
|
|
87
148
|
enabled?: boolean;
|
|
88
149
|
customerDnsServers?: string[];
|
|
89
150
|
importCustomRoutes?: boolean;
|
|
90
151
|
dedicatedNodePool?: DedicatedNodePoolConfig | null;
|
|
152
|
+
/**
|
|
153
|
+
* `aws-privatelink` only. Full declarative replacement of the endpoint list,
|
|
154
|
+
* reconciled server-side by `serviceName`: an existing name keeps its allocated
|
|
155
|
+
* `ipIndex` with hostnames and ports updated in place, a new name gets a fresh
|
|
156
|
+
* index, and an omitted name is removed. An empty array is valid and means the
|
|
157
|
+
* customer removed their last service.
|
|
158
|
+
*
|
|
159
|
+
* Declarative rather than add/remove verbs so it mirrors the diff-driven,
|
|
160
|
+
* `serviceName`-keyed terraform model and stays idempotent under retry.
|
|
161
|
+
*/
|
|
162
|
+
endpoints?: CreatePrivateLinkEndpointParams[];
|
|
91
163
|
}
|
|
92
164
|
/** Status of a single provisioned VPC resource (bridge VPC, proxy VM, etc.). */
|
|
93
165
|
export interface SingleTenancyLiveCheck {
|
|
@@ -97,23 +169,102 @@ export interface SingleTenancyLiveCheck {
|
|
|
97
169
|
resourceName?: string;
|
|
98
170
|
resourceType?: string;
|
|
99
171
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* `bridgeVpc`/`proxyVm`/`peering`, network-attachment configs report
|
|
104
|
-
* `proxyVm`/`networkAttachment`.
|
|
105
|
-
*/
|
|
106
|
-
export interface SingleTenancyLiveStatus {
|
|
107
|
-
bridgeVpc?: SingleTenancyLiveCheck;
|
|
172
|
+
/** The one check every connection type reports: each gets a proxy VM. */
|
|
173
|
+
interface LiveStatusBase {
|
|
174
|
+
type: string;
|
|
108
175
|
proxyVm?: SingleTenancyLiveCheck;
|
|
176
|
+
}
|
|
177
|
+
/** Bridge VPC on our side, plus the peering between it and the customer's. */
|
|
178
|
+
export interface LiveStatusGcpPeering extends LiveStatusBase {
|
|
179
|
+
type: "gcp-peering";
|
|
180
|
+
bridgeVpc?: SingleTenancyLiveCheck;
|
|
109
181
|
peering?: SingleTenancyLiveCheck;
|
|
182
|
+
}
|
|
183
|
+
/** The customer-owned network attachment our proxy VM's nic1 connects to. */
|
|
184
|
+
export interface LiveStatusGcpNetworkAttachment extends LiveStatusBase {
|
|
185
|
+
type: "gcp-network-attachment";
|
|
110
186
|
networkAttachment?: SingleTenancyLiveCheck;
|
|
111
187
|
}
|
|
188
|
+
export interface LiveStatusAwsPrivateLink extends LiveStatusBase {
|
|
189
|
+
type: "aws-privatelink";
|
|
190
|
+
/**
|
|
191
|
+
* One entry per configured endpoint, because PrivateLink is per-service: each
|
|
192
|
+
* endpoint is its own interface VPC Endpoint with its own ENIs, security group
|
|
193
|
+
* and AWS state. `resourceName` carries the endpoint's `serviceName`, except on
|
|
194
|
+
* the single aggregate row {@link buildErrorLiveStatus} emits when the live
|
|
195
|
+
* lookup itself failed.
|
|
196
|
+
*/
|
|
197
|
+
endpoints?: SingleTenancyLiveCheck[];
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Aggregated live status of the resources backing a VPC connection, discriminated
|
|
201
|
+
* on the same `type` as {@link VpcConnection} so which checks exist is known at
|
|
202
|
+
* compile time rather than by convention. A flat superset would let a consumer
|
|
203
|
+
* read `bridgeVpc` off an AWS status, or let a producer emit endpoint rows for a
|
|
204
|
+
* peering config.
|
|
205
|
+
*
|
|
206
|
+
* A new connection mode adds a variant. Plurality is a property of the resource,
|
|
207
|
+
* not of AWS: an HA VPN mode would report an array of tunnels and BGP peers.
|
|
208
|
+
*/
|
|
209
|
+
export type SingleTenancyLiveStatus = LiveStatusGcpPeering | LiveStatusGcpNetworkAttachment | LiveStatusAwsPrivateLink;
|
|
112
210
|
/** Computed infrastructure resource names derived from a config id/type. */
|
|
113
211
|
export interface SingleTenancyDerivedNames {
|
|
114
212
|
bridgeVpcName?: string;
|
|
115
213
|
proxyVmName?: string;
|
|
116
214
|
bridgePeeringName?: string;
|
|
117
215
|
networkAttachmentName?: string;
|
|
216
|
+
/** `aws-privatelink`: serviceName → the endpoint's terraform `Name` tag. */
|
|
217
|
+
privateLinkEndpointNames?: Record<string, string>;
|
|
218
|
+
/** `aws-privatelink`: serviceName → the endpoint's pinned ENI IPs, one per AZ. */
|
|
219
|
+
privateLinkEndpointIps?: Record<string, string[]>;
|
|
118
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* The single AWS region Builder's PrivateLink infrastructure runs in. Keep in
|
|
223
|
+
* sync with the `aws` provider region in packages/terraform/main.tf — an Endpoint
|
|
224
|
+
* Service in another region is unreachable by our interface endpoints.
|
|
225
|
+
*/
|
|
226
|
+
export declare const BUILDER_AWS_REGION = "us-east-1";
|
|
227
|
+
/**
|
|
228
|
+
* AWS-generated VPC Endpoint Service name: `com.amazonaws.vpce.<region>.vpce-svc-<id>`.
|
|
229
|
+
* The id is 8 or 17 hex chars — AWS resource ids are never a length in between.
|
|
230
|
+
*/
|
|
231
|
+
export declare const PRIVATELINK_SERVICE_NAME_RE: RegExp;
|
|
232
|
+
/**
|
|
233
|
+
* Bounds for a `PrivateLinkEndpoint.ipIndex`. AWS reserves the first four and the
|
|
234
|
+
* last address of every subnet, so an out-of-range index passes terraform's
|
|
235
|
+
* `cidrhost()` but fails at apply.
|
|
236
|
+
*/
|
|
237
|
+
export declare const PRIVATELINK_MIN_IP_INDEX = 4;
|
|
238
|
+
export declare const PRIVATELINK_MAX_IP_INDEX = 4094;
|
|
239
|
+
/**
|
|
240
|
+
* Parses a pasted VPC Endpoint Service name, deriving the region embedded in it.
|
|
241
|
+
* Returns `null` for a malformed name. Callers reject a region that isn't
|
|
242
|
+
* {@link BUILDER_AWS_REGION} themselves, so the two failures get distinct messages.
|
|
243
|
+
*/
|
|
244
|
+
export declare function parsePrivateLinkServiceName(raw: string): {
|
|
245
|
+
serviceName: string;
|
|
246
|
+
awsRegion: string;
|
|
247
|
+
} | null;
|
|
248
|
+
/**
|
|
249
|
+
* Whether a VPC connection can carry pod traffic anywhere at all — the bar for
|
|
250
|
+
* pointing an organization's workloads at it.
|
|
251
|
+
*
|
|
252
|
+
* A running proxy VM is not that bar for `aws-privatelink`. Its egress policy
|
|
253
|
+
* table is built entirely from the configured endpoints, with `blackhole default`
|
|
254
|
+
* underneath, so a connection with an empty `endpoints` array yields a VM that
|
|
255
|
+
* drops *every* packet a pod sends — not just traffic to whichever service was
|
|
256
|
+
* removed. The two GCP flavors have no such state: egress leaves through nic1 into
|
|
257
|
+
* the customer's network, which exists as soon as the connection does.
|
|
258
|
+
*
|
|
259
|
+
* An empty endpoint list is a legitimate thing to store (a customer removing their
|
|
260
|
+
* last service), so this gates routing rather than rejecting the write.
|
|
261
|
+
*
|
|
262
|
+
* Takes the two fields it reads rather than a {@link VpcConnection}, so the staff
|
|
263
|
+
* app can pass its flattened list item — where `type` and `endpoints` are hoisted
|
|
264
|
+
* to the top level — without a cast.
|
|
265
|
+
*/
|
|
266
|
+
export declare function canCarryPodTraffic(vpc: {
|
|
267
|
+
type?: VpcConnection["type"];
|
|
268
|
+
endpoints?: readonly unknown[];
|
|
269
|
+
} | undefined): boolean;
|
|
119
270
|
export {};
|
package/src/single-tenancy.js
CHANGED
|
@@ -1 +1,59 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The single AWS region Builder's PrivateLink infrastructure runs in. Keep in
|
|
3
|
+
* sync with the `aws` provider region in packages/terraform/main.tf — an Endpoint
|
|
4
|
+
* Service in another region is unreachable by our interface endpoints.
|
|
5
|
+
*/
|
|
6
|
+
export const BUILDER_AWS_REGION = "us-east-1";
|
|
7
|
+
/**
|
|
8
|
+
* AWS-generated VPC Endpoint Service name: `com.amazonaws.vpce.<region>.vpce-svc-<id>`.
|
|
9
|
+
* The id is 8 or 17 hex chars — AWS resource ids are never a length in between.
|
|
10
|
+
*/
|
|
11
|
+
export const PRIVATELINK_SERVICE_NAME_RE = /^com\.amazonaws\.vpce\.([a-z0-9-]+)\.vpce-svc-(?:[0-9a-f]{8}|[0-9a-f]{17})$/;
|
|
12
|
+
/**
|
|
13
|
+
* Bounds for a `PrivateLinkEndpoint.ipIndex`. AWS reserves the first four and the
|
|
14
|
+
* last address of every subnet, so an out-of-range index passes terraform's
|
|
15
|
+
* `cidrhost()` but fails at apply.
|
|
16
|
+
*/
|
|
17
|
+
export const PRIVATELINK_MIN_IP_INDEX = 4;
|
|
18
|
+
export const PRIVATELINK_MAX_IP_INDEX = 4094;
|
|
19
|
+
/**
|
|
20
|
+
* Parses a pasted VPC Endpoint Service name, deriving the region embedded in it.
|
|
21
|
+
* Returns `null` for a malformed name. Callers reject a region that isn't
|
|
22
|
+
* {@link BUILDER_AWS_REGION} themselves, so the two failures get distinct messages.
|
|
23
|
+
*/
|
|
24
|
+
export function parsePrivateLinkServiceName(raw) {
|
|
25
|
+
const serviceName = raw.trim();
|
|
26
|
+
const match = serviceName.match(PRIVATELINK_SERVICE_NAME_RE);
|
|
27
|
+
if (!(match === null || match === void 0 ? void 0 : match[1])) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return { serviceName, awsRegion: match[1] };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether a VPC connection can carry pod traffic anywhere at all — the bar for
|
|
34
|
+
* pointing an organization's workloads at it.
|
|
35
|
+
*
|
|
36
|
+
* A running proxy VM is not that bar for `aws-privatelink`. Its egress policy
|
|
37
|
+
* table is built entirely from the configured endpoints, with `blackhole default`
|
|
38
|
+
* underneath, so a connection with an empty `endpoints` array yields a VM that
|
|
39
|
+
* drops *every* packet a pod sends — not just traffic to whichever service was
|
|
40
|
+
* removed. The two GCP flavors have no such state: egress leaves through nic1 into
|
|
41
|
+
* the customer's network, which exists as soon as the connection does.
|
|
42
|
+
*
|
|
43
|
+
* An empty endpoint list is a legitimate thing to store (a customer removing their
|
|
44
|
+
* last service), so this gates routing rather than rejecting the write.
|
|
45
|
+
*
|
|
46
|
+
* Takes the two fields it reads rather than a {@link VpcConnection}, so the staff
|
|
47
|
+
* app can pass its flattened list item — where `type` and `endpoints` are hoisted
|
|
48
|
+
* to the top level — without a cast.
|
|
49
|
+
*/
|
|
50
|
+
export function canCarryPodTraffic(vpc) {
|
|
51
|
+
var _a;
|
|
52
|
+
if (!(vpc === null || vpc === void 0 ? void 0 : vpc.type)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (vpc.type === "aws-privatelink") {
|
|
56
|
+
return ((_a = vpc.endpoints) !== null && _a !== void 0 ? _a : []).length > 0;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { BUILDER_AWS_REGION, canCarryPodTraffic, parsePrivateLinkServiceName, } from "./single-tenancy";
|
|
3
|
+
describe("parsePrivateLinkServiceName", () => {
|
|
4
|
+
it("parses a current-format (17 hex char) service name", () => {
|
|
5
|
+
expect(parsePrivateLinkServiceName("com.amazonaws.vpce.us-east-1.vpce-svc-0123456789abcdef0")).toEqual({
|
|
6
|
+
serviceName: "com.amazonaws.vpce.us-east-1.vpce-svc-0123456789abcdef0",
|
|
7
|
+
awsRegion: "us-east-1",
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
it("parses a legacy-format (8 hex char) service name", () => {
|
|
11
|
+
var _a;
|
|
12
|
+
expect((_a = parsePrivateLinkServiceName("com.amazonaws.vpce.us-east-1.vpce-svc-0123abcd")) === null || _a === void 0 ? void 0 : _a.awsRegion).toBe("us-east-1");
|
|
13
|
+
});
|
|
14
|
+
it("trims surrounding whitespace from a pasted value", () => {
|
|
15
|
+
expect(parsePrivateLinkServiceName(" com.amazonaws.vpce.us-east-1.vpce-svc-0123abcd\n")).toEqual({
|
|
16
|
+
serviceName: "com.amazonaws.vpce.us-east-1.vpce-svc-0123abcd",
|
|
17
|
+
awsRegion: "us-east-1",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it("derives a region other than ours rather than rejecting it, so callers can report the two failures differently", () => {
|
|
21
|
+
const parsed = parsePrivateLinkServiceName("com.amazonaws.vpce.us-west-2.vpce-svc-0123456789abcdef0");
|
|
22
|
+
expect(parsed === null || parsed === void 0 ? void 0 : parsed.awsRegion).toBe("us-west-2");
|
|
23
|
+
expect(parsed === null || parsed === void 0 ? void 0 : parsed.awsRegion).not.toBe(BUILDER_AWS_REGION);
|
|
24
|
+
});
|
|
25
|
+
it("rejects an id length between the two valid formats", () => {
|
|
26
|
+
expect(parsePrivateLinkServiceName("com.amazonaws.vpce.us-east-1.vpce-svc-0123456789")).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
it("rejects a missing vpce-svc- prefix", () => {
|
|
29
|
+
expect(parsePrivateLinkServiceName("com.amazonaws.vpce.us-east-1.0123abcd")).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
it("rejects an interface endpoint id in place of a service id", () => {
|
|
32
|
+
expect(parsePrivateLinkServiceName("com.amazonaws.vpce.us-east-1.vpce-0123456789abcdef0")).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
it("rejects uppercase, since AWS service names are always lowercase", () => {
|
|
35
|
+
expect(parsePrivateLinkServiceName("com.amazonaws.vpce.us-east-1.vpce-svc-0123ABCD")).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it("rejects a GCP network attachment URI", () => {
|
|
38
|
+
expect(parsePrivateLinkServiceName("projects/acme/regions/us-central1/networkAttachments/builder-connection")).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
it("rejects an empty string", () => {
|
|
41
|
+
expect(parsePrivateLinkServiceName("")).toBeNull();
|
|
42
|
+
expect(parsePrivateLinkServiceName(" ")).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("canCarryPodTraffic", () => {
|
|
46
|
+
const awsVpc = (endpoints) => ({
|
|
47
|
+
type: "aws-privatelink",
|
|
48
|
+
awsRegion: "us-east-1",
|
|
49
|
+
proxyIp: "10.64.0.10",
|
|
50
|
+
customerDnsServers: [],
|
|
51
|
+
endpoints,
|
|
52
|
+
});
|
|
53
|
+
it("rejects an aws-privatelink connection with no endpoints", () => {
|
|
54
|
+
// Its proxy VM runs, but the policy table is `blackhole default` and nothing
|
|
55
|
+
// else, so pointing an org at it drops every packet its pods send.
|
|
56
|
+
expect(canCarryPodTraffic(awsVpc([]))).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it("accepts an aws-privatelink connection with an endpoint", () => {
|
|
59
|
+
expect(canCarryPodTraffic(awsVpc([
|
|
60
|
+
{
|
|
61
|
+
serviceName: "com.amazonaws.vpce.us-east-1.vpce-svc-000000000000000a0",
|
|
62
|
+
hostnames: ["db.internal"],
|
|
63
|
+
ports: [5432],
|
|
64
|
+
ipIndex: 7,
|
|
65
|
+
},
|
|
66
|
+
]))).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it("treats a document with no endpoints key like an empty list", () => {
|
|
69
|
+
const { endpoints: _omitted, ...withoutEndpoints } = awsVpc([]);
|
|
70
|
+
expect(canCarryPodTraffic(withoutEndpoints)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it("accepts both GCP flavors, whose egress exists as soon as the config does", () => {
|
|
73
|
+
// Typed as VpcConnection rather than passed inline, so this also pins that a
|
|
74
|
+
// full connection object satisfies the narrowed parameter.
|
|
75
|
+
const peering = {
|
|
76
|
+
type: "gcp-peering",
|
|
77
|
+
gcpProjectId: "p",
|
|
78
|
+
gcpNetworkName: "n",
|
|
79
|
+
bridgeCidr: "10.12.0.0/29",
|
|
80
|
+
importCustomRoutes: false,
|
|
81
|
+
proxyIp: "10.64.0.11",
|
|
82
|
+
customerDnsServers: [],
|
|
83
|
+
};
|
|
84
|
+
const attachment = {
|
|
85
|
+
type: "gcp-network-attachment",
|
|
86
|
+
gcpProjectId: "p",
|
|
87
|
+
networkAttachmentName: "builder-connection",
|
|
88
|
+
proxyIp: "10.64.0.12",
|
|
89
|
+
customerDnsServers: [],
|
|
90
|
+
};
|
|
91
|
+
expect(canCarryPodTraffic(peering)).toBe(true);
|
|
92
|
+
expect(canCarryPodTraffic(attachment)).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it("rejects a config with no VPC connectivity at all", () => {
|
|
95
|
+
expect(canCarryPodTraffic(undefined)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|