1import * as Neon from "@distilled.cloud/neon";
2import * as Effect from "effect/Effect";
3import { OwnedBySomeoneElse, Unowned } from "../AdoptPolicy.ts";
4import { isResolved } from "../Diff.ts";
5import * as Provider from "../Provider.ts";
6import { Resource } from "../Resource.ts";
7import { authPlanScope, authRequest, InvalidManagedAuth } from "./Auth.ts";
8import type { Providers } from "./Providers.ts";
9
10export interface AuthTrustedDomainProps {
11 /** Managed Auth integration to trust the origin on. */
12 auth: { projectId: string; branchId: string };
13 /** Exact trusted origin, including scheme and optional port. */
14 domain: string;
15}
16
17export interface AuthTrustedDomainAttributes {
18 /** Neon project identity. */
19 projectId: string;
20 /** Branch identity. */
21 branchId: string;
22 /** Trusted origin owned by this child. */
23 domain: string;
24}
25
26export interface AuthTrustedDomain extends Resource<
27 "Neon.AuthTrustedDomain",
28 AuthTrustedDomainProps,
29 AuthTrustedDomainAttributes,
30 never,
31 Providers
32> {}
33
34/**
35 * Own one trusted origin without replacing the Auth integration or overwriting
36 * other origins. Existing entries require explicit adoption.
37 *
38 * ### Trust a deployed frontend
39 * **Example:** Resolve the site URL after creating Auth
40 * ```typescript
41 * yield* Neon.AuthTrustedDomain("SiteOrigin", { auth, domain: site.url });
42 * ```
43 *
44 * @resource
45 * @product Auth
46 */
47export const AuthTrustedDomain = Resource<AuthTrustedDomain>(
48 "Neon.AuthTrustedDomain",
49);
50
51const observe = (
52 scope: { projectId: string; branchId: string },
53 domain: string,
54) =>
55 Neon.listBranchNeonAuthTrustedDomains(authRequest(scope)).pipe(
56 Effect.map((response) =>
57 response.domains.some(
58 (item) =>
59 item.domain === domain && item.auth_provider === "better_auth",
60 ),
61 ),
62 Effect.catchTag("NotFound", () => Effect.succeed(false)),
63 );
64
65export const AuthTrustedDomainProvider = () =>
66 Provider.succeed(AuthTrustedDomain, {
67 stables: ["projectId", "branchId", "domain"],
68 diff: Effect.fn(function* ({ olds, news, output }) {
69 const scope =
70 "auth" in news
71 ? yield* authPlanScope({ branch: news.auth })
72 : undefined;
73 if (
74 output &&
75 (!scope ||
76 scope.projectId !== output.projectId ||
77 scope.branchId !== output.branchId ||
78 !("domain" in news) ||
79 news.domain !== output.domain)
80 )
81 return { action: "replace" } as const;
82 if (!isResolved(news)) return;
83 if (
84 news.auth.projectId !== olds.auth.projectId ||
85 news.auth.branchId !== olds.auth.branchId ||
86 news.domain !== olds.domain
87 ) {
88 return { action: "replace" } as const;
89 }
90 if (!(yield* observe(news.auth, news.domain)))
91 return { action: "update" } as const;
92 }),
93 read: Effect.fn(function* ({ olds, output }) {
94 if (
95 !output &&
96 (!olds?.auth?.projectId || !olds.auth.branchId || !olds.domain)
97 )
98 return undefined;
99 const scope = output ?? olds.auth;
100 const domain = output?.domain ?? olds.domain;
101 if (!(yield* observe(scope, domain))) return undefined;
102 const attrs = {
103 projectId: scope.projectId,
104 branchId: scope.branchId,
105 domain,
106 };
107 return output ? attrs : Unowned(attrs);
108 }),
109 reconcile: Effect.fn(function* ({ news, output }) {
110 if (
111 output &&
112 (news.auth.projectId !== output.projectId ||
113 news.auth.branchId !== output.branchId ||
114 news.domain !== output.domain)
115 )
116 return yield* new InvalidManagedAuth({
117 message:
118 "Trusted origin identity changed without replacement; refusing to mutate either origin",
119 });
120 const request = authRequest(news.auth);
121 const current = yield* observe(news.auth, news.domain);
122 if (current && !output)
123 return yield* new OwnedBySomeoneElse({
124 message: "Existing trusted origin requires explicit adoption",
125 resourceType: "Neon.AuthTrustedDomain",
126 });
127 if (!current) {
128 yield* Neon.addBranchNeonAuthTrustedDomain({
129 ...request,
130 auth_provider: "better_auth",
131 domain: news.domain,
132 }).pipe(
133 Effect.catchTag("Conflict", () =>
134 Effect.fail(
135 new OwnedBySomeoneElse({
136 message:
137 "Trusted origin appeared during creation; explicit adoption is required",
138 resourceType: "Neon.AuthTrustedDomain",
139 }),
140 ),
141 ),
142 );
143 }
144 if (!(yield* observe(news.auth, news.domain))) {
145 return yield* Effect.fail(
146 new Error("Trusted origin was not visible after reconciliation"),
147 );
148 }
149 return {
150 projectId: news.auth.projectId,
151 branchId: news.auth.branchId,
152 domain: news.domain,
153 };
154 }),
155 delete: Effect.fn(function* ({ output }) {
156 if (yield* observe(output, output.domain)) {
157 yield* Neon.deleteBranchNeonAuthTrustedDomain({
158 ...authRequest(output),
159 auth_provider: "better_auth",
160 domains: [{ domain: output.domain }],
161 }).pipe(Effect.catchTag("NotFound", () => Effect.void));
162 }
163 }),
164 });