@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@builder.io/ai-utils",
3
- "version": "0.89.0",
3
+ "version": "0.90.0",
4
4
  "description": "Builder.io AI utils",
5
5
  "files": [
6
6
  "src"
@@ -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
@@ -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.",
@@ -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
- export type CreateSingleTenancyVpcParams = CreateVpcGcpPeeringParams | CreateVpcGcpNetworkAttachmentParams;
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`, and
83
- * `vpc.bridgeCidr` are immutable because changing them would diverge from
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
- * Aggregated live status of the resources backing a VPC connection. Which
102
- * checks are present depends on the connection type: peering configs report
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 {};
@@ -1 +1,59 @@
1
- export {};
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
+ });