1import * as Neon from "@distilled.cloud/neon";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { OwnedBySomeoneElse, Unowned } from "../AdoptPolicy.ts";
5import { isResolved } from "../Diff.ts";
6import * as Provider from "../Provider.ts";
7import { Resource } from "../Resource.ts";
8import type { Providers } from "./Providers.ts";
9
10export interface OrganizationVPCEndpointProps {
11 /** Existing Neon organization ID. Changing it replaces the registration, cleaning up the old scope first. */
12 orgId: string;
13 /** Neon AWS region ID, such as aws-us-east-2. Azure is not supported. Changes replace the registration. */
14 regionId: string;
15 /** Existing AWS VPC endpoint ID, not a VPC ID. Changes replace the registration. */
16 vpcEndpointId: string;
17 /** Descriptive label on the Neon registration. */
18 label: string;
19}
20
21export interface OrganizationVPCEndpointAttributes {
22 /** Organization containing the registration. */
23 orgId: string;
24 /** Neon AWS region containing the registration. */
25 regionId: string;
26 /** Referenced AWS VPC endpoint; never created, modified, or deleted by this resource. */
27 vpcEndpointId: string;
28 /** Observed label, or null when the registration is absent. */
29 label: string | null;
30 /** Original adopted label; null denotes a registration created by Alchemy. */
31 initialLabel: string | null;
32 /** Last managed label, retained across refreshes for drift-safe cleanup. */
33 managedLabel: string;
34 /** Observed Neon registration state (for example new or accepted); null when absent. */
35 state: string | null;
36}
37
38export interface OrganizationVPCEndpoint extends Resource<
39 "Neon.OrganizationVPCEndpoint",
40 OrganizationVPCEndpointProps,
41 OrganizationVPCEndpointAttributes,
42 never,
43 Providers
44> {}
45
46/**
47 * Register an EXISTING AWS PrivateLink endpoint with a Neon organization.
48 * Requires an entitled organization admin and a supported AWS region matching
49 * the endpoint and client. Azure is unsupported. Neon allows at most ten
50 * private networking configurations per AWS region. This neither provisions
51 * the AWS endpoint nor configures its DNS, and does not block public connections.
52 *
53 * Existing registrations require explicit resource-scoped adoption. Their label
54 * is durably captured before mutation and restored on destroy, without revoking
55 * the registration. New registrations are unregistered on destroy only if their
56 * label is still managed and no projects restrict access through them.
57 * IMPORTANT: Neon permanently prevents re-registering a removed endpoint in the
58 * same organization. Replacement cleans up the old scope first; use a fresh AWS
59 * endpoint when necessary. External drift blocks cleanup. Preserve the state
60 * store and serialize writers: Neon exposes no conditional association writes.
61 *
62 * ### Register private networking
63 * **Example:** Bind an existing AWS endpoint
64 * ```typescript
65 * const network = yield* Neon.OrganizationVPCEndpoint("PrivateNetwork", {
66 * orgId: "org-example-12345678",
67 * regionId: "aws-us-east-2",
68 * vpcEndpointId: "vpce-0123456789abcdef0",
69 * label: "Application network",
70 * });
71 * ```
72 *
73 * @resource
74 * @product Organization
75 */
76export const OrganizationVPCEndpoint = Resource<OrganizationVPCEndpoint>(
77 "Neon.OrganizationVPCEndpoint",
78);
79
80export class InvalidOrganizationVPCEndpoint extends Data.TaggedError(
81 "InvalidOrganizationVPCEndpoint",
82)<{
83 /** Invalid scope or unsafe mutation condition. */
84 message: string;
85}> {}
86
87/** @internal */
88export const validateOrganizationVPCEndpoint = (
89 props: OrganizationVPCEndpointProps,
90) =>
91 /^[a-z0-9-]{1,60}$/.test(props.orgId) &&
92 /^aws-[a-z0-9-]+$/.test(props.regionId) &&
93 /^vpce-[a-f0-9]+$/.test(props.vpcEndpointId) &&
94 props.label.trim().length > 0
95 ? Effect.void
96 : Effect.fail(
97 new InvalidOrganizationVPCEndpoint({
98 message:
99 "An organization ID, AWS Neon region, existing vpce- endpoint ID, and nonempty label are required",
100 }),
101 );
102
103const request = (scope: {
104 orgId: string;
105 regionId: string;
106 vpcEndpointId: string;
107}) => ({
108 org_id: scope.orgId,
109 region_id: scope.regionId,
110 vpc_endpoint_id: scope.vpcEndpointId,
111});
112
113const observe = (scope: {
114 orgId: string;
115 regionId: string;
116 vpcEndpointId: string;
117}) =>
118 Neon.getOrganizationVPCEndpointDetails(request(scope)).pipe(
119 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
120 );
121
122export const OrganizationVPCEndpointProvider = () =>
123 Provider.succeed(OrganizationVPCEndpoint, {
124 stables: ["orgId", "regionId", "vpcEndpointId", "initialLabel"],
125 diff: Effect.fn(function* ({ olds, news, output }) {
126 const previous = output ?? olds;
127 if (
128 !("orgId" in news) ||
129 !("regionId" in news) ||
130 !("vpcEndpointId" in news)
131 ) {
132 return { action: "replace", deleteFirst: true } as const;
133 }
134 for (const key of ["orgId", "regionId", "vpcEndpointId"] as const) {
135 if (!isResolved(news[key]) || news[key] !== previous[key]) {
136 return { action: "replace", deleteFirst: true } as const;
137 }
138 }
139 if (!isResolved<OrganizationVPCEndpointProps>(news)) return;
140 yield* validateOrganizationVPCEndpoint(news);
141 if ((yield* observe(news))?.label !== news.label) {
142 return { action: "update" } as const;
143 }
144 }),
145 read: Effect.fn(function* ({ olds, output }) {
146 const scope = output ?? olds;
147 if (!scope?.orgId || !scope.regionId || !scope.vpcEndpointId) return;
148 const observed = yield* observe(scope);
149 if (output)
150 return {
151 ...output,
152 label: observed?.label ?? null,
153 state: observed?.state ?? null,
154 };
155 if (!observed) return;
156 return Unowned({
157 orgId: scope.orgId,
158 regionId: scope.regionId,
159 vpcEndpointId: scope.vpcEndpointId,
160 label: observed.label,
161 initialLabel: observed.label,
162 managedLabel: observed.label,
163 state: observed.state,
164 });
165 }),
166 reconcile: Effect.fn(function* ({ news, output, olds }) {
167 yield* validateOrganizationVPCEndpoint(news);
168 if (
169 output &&
170 (output.orgId !== news.orgId ||
171 output.regionId !== news.regionId ||
172 output.vpcEndpointId !== news.vpcEndpointId)
173 ) {
174 return yield* new InvalidOrganizationVPCEndpoint({
175 message: "Registration identity changed without replacement",
176 });
177 }
178 const observed = yield* observe(news);
179 if (observed && !output) {
180 return yield* new OwnedBySomeoneElse({
181 message:
182 "Existing VPC registration requires scoped adoption; an equal endpoint ID is not ownership",
183 resourceType: "Neon.OrganizationVPCEndpoint",
184 });
185 }
186 if (
187 output &&
188 !olds &&
189 observed?.label !== output.managedLabel &&
190 observed?.label !== news.label
191 ) {
192 return yield* new InvalidOrganizationVPCEndpoint({
193 message:
194 "VPC registration changed since its adoption baseline was captured",
195 });
196 }
197 if (!observed && output) {
198 return yield* new InvalidOrganizationVPCEndpoint({
199 message:
200 "The registration was removed externally; Neon forbids re-registering it in this organization",
201 });
202 }
203 if (observed?.label !== news.label) {
204 yield* Neon.assignOrganizationVPCEndpoint({
205 ...request(news),
206 label: news.label,
207 }).pipe(
208 Effect.catchTag(
209 "Conflict",
210 Effect.fn(function* (error) {
211 const raced = yield* observe(news);
212 if (!raced || raced.label !== news.label) return yield* error;
213 if (!output)
214 return yield* new OwnedBySomeoneElse({
215 message:
216 "A registration appeared during creation; explicit adoption is required",
217 resourceType: "Neon.OrganizationVPCEndpoint",
218 });
219 }),
220 ),
221 );
222 }
223 const current = yield* observe(news);
224 if (!current || current.label !== news.label) {
225 return yield* new InvalidOrganizationVPCEndpoint({
226 message: "VPC registration changed during reconciliation",
227 });
228 }
229 return {
230 orgId: news.orgId,
231 regionId: news.regionId,
232 vpcEndpointId: news.vpcEndpointId,
233 label: current.label,
234 initialLabel: output?.initialLabel ?? null,
235 managedLabel: news.label,
236 state: current.state,
237 };
238 }),
239 delete: Effect.fn(function* ({ output, olds }) {
240 const observed = yield* observe(output);
241 if (!observed) return;
242 if (
243 output.initialLabel !== null &&
244 observed.label === output.initialLabel
245 )
246 return;
247 if (
248 observed.label !== output.managedLabel &&
249 observed.label !== olds.label
250 ) {
251 return yield* new InvalidOrganizationVPCEndpoint({
252 message:
253 "Refusing cleanup of an externally relabeled VPC registration",
254 });
255 }
256 if (output.initialLabel !== null) {
257 yield* Neon.assignOrganizationVPCEndpoint({
258 ...request(output),
259 label: output.initialLabel,
260 }).pipe(
261 Effect.catchTag(
262 "NotFound",
263 Effect.fn(function* (error) {
264 if (yield* observe(output)) return yield* error;
265 }),
266 ),
267 );
268 } else {
269 if (observed.num_restricted_projects !== 0) {
270 return yield* new InvalidOrganizationVPCEndpoint({
271 message:
272 "Remove project VPC restrictions before unregistering this endpoint",
273 });
274 }
275 yield* Neon.deleteOrganizationVPCEndpoint(request(output)).pipe(
276 Effect.catchTag("NotFound", () => Effect.void),
277 );
278 }
279 }),
280 });