1import * as we from "@distilled.cloud/gcp/workspaceevents_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 configIdOf,
12 getConfig,
13 jsonEqual,
14 MAX_CONFIG_ID_LENGTH,
15 parentOfConfig,
16 toConfigName,
17 toPhysicalId,
18 toTaskName,
19} from "./internal.ts";
20
21export type AuthenticationInfo = {
22 /** Supported authentication schemes (`Basic`, `Bearer`, …). */
23 schemes?: string[];
24 /** Optional credentials sent with the notification. */
25 credentials?: string;
26};
27
28export type PushNotificationConfig = {
29 /** HTTPS URL that receives task updates. */
30 url?: string;
31 /** Authentication attached to each notification. */
32 authentication?: AuthenticationInfo;
33 /** Client token unique for this task or session. */
34 token?: string;
35 /** Client-assigned identifier for this push configuration. */
36 id?: string;
37};
38
39export type TasksPushNotificationConfigProps = {
40 /**
41 * Parent task (`tasks/{task}` or `{task}`). Immutable — changing it
42 * replaces the config.
43 */
44 task: string;
45 /**
46 * Config id (the `{config}` segment of
47 * `tasks/{task}/pushNotificationConfigs/{config}`). If omitted, a
48 * unique RFC1035 name is generated. Immutable — changing it replaces
49 * the config.
50 */
51 configId?: string;
52 /**
53 * HTTPS URL that receives task updates.
54 */
55 url: string;
56 /**
57 * Authentication attached to each notification.
58 */
59 authentication?: AuthenticationInfo;
60 /**
61 * Client token unique for this task or session, sent with every
62 * notification as given.
63 */
64 token?: string;
65 /**
66 * Optional tenant path parameter (experimental).
67 */
68 tenant?: string;
69};
70
71export type TasksPushNotificationConfig = Resource<
72 "GCP.WorkspaceEvents.TasksPushNotificationConfig",
73 TasksPushNotificationConfigProps,
74 {
75 /** Full resource name `tasks/{task}/pushNotificationConfigs/{config}`. */
76 name: string;
77 /** Config id (last path segment). */
78 configId: string;
79 /** Parent task resource name. */
80 task: string;
81 /** Project id used when the config was reconciled. */
82 project: string;
83 /** HTTPS URL that receives task updates. */
84 url: string | undefined;
85 /** Authentication attached to each notification. */
86 authentication: AuthenticationInfo | undefined;
87 /** Client token. */
88 token: string | undefined;
89 /** Client-assigned identifier. */
90 notificationId: string | undefined;
91 /** Optional tenant. */
92 tenant: string | undefined;
93 },
94 never,
95 Providers
96>;
97
98/**
99 * A push-notification config for a Workspace Events / A2A task.
100 *
101 * The API has no labels field and the token is sent to the receiver, so
102 * Alchemy writes no ownership marker: it tracks the config by name (a
103 * generated config id embeds the stack, stage, and logical id). Task and config id
104 * are identity — changing either replaces the config. There is no patch
105 * method; changing the URL, token, or authentication replaces the
106 * config. Creating a config requires an existing task.
107 *
108 * ### Creating a Config
109 * **Example:** HTTPS webhook for a task
110 * ```typescript
111 * const config = yield* GCP.WorkspaceEvents.TasksPushNotificationConfig(
112 * "Updates",
113 * {
114 * task: "tasks/TASK_ID",
115 * url: "https://example.com/workspace-events",
116 * },
117 * );
118 * ```
119 *
120 * **Example:** Bearer authentication
121 * ```typescript
122 * const config = yield* GCP.WorkspaceEvents.TasksPushNotificationConfig(
123 * "Updates",
124 * {
125 * task: "tasks/TASK_ID",
126 * url: "https://example.com/workspace-events",
127 * authentication: { schemes: ["Bearer"], credentials: "token" },
128 * },
129 * );
130 * ```
131 *
132 * @resource
133 * @category WorkspaceEvents
134 */
135export const TasksPushNotificationConfig =
136 Resource<TasksPushNotificationConfig>(
137 "GCP.WorkspaceEvents.TasksPushNotificationConfig",
138 );
139
140export class TasksPushNotificationConfigNotResolved extends Data.TaggedError(
141 "GCP.WorkspaceEvents.TasksPushNotificationConfigNotResolved",
142)<{
143 name: string;
144}> {}
145
146const authOf = (
147 auth: we.AuthenticationInfo | undefined,
148): AuthenticationInfo | undefined => {
149 if (auth === undefined) return undefined;
150 return {
151 schemes: auth.schemes,
152 credentials: auth.credentials,
153 };
154};
155
156const toAttrs = (
157 config: we.TaskPushNotificationConfig,
158 project: string,
159 tenant: string | undefined,
160) => {
161 const name = config.name ?? "";
162 const push = config.pushNotificationConfig;
163 return {
164 name,
165 configId: configIdOf(name),
166 task: parentOfConfig(name),
167 project,
168 url: push?.url,
169 authentication: authOf(push?.authentication),
170 token: push?.token,
171 notificationId: push?.id,
172 tenant,
173 };
174};
175
176export const TasksPushNotificationConfigProvider = () =>
177 Provider.succeed(TasksPushNotificationConfig, {
178 stables: ["name", "configId", "task", "project"],
179
180 diff: Effect.fn(function* ({ news, olds, output }) {
181 if (!isResolved(news)) return undefined;
182 const previousTask = olds?.task ?? output?.task;
183 const nextTask = toTaskName(news.task);
184 const previousId = olds?.configId ?? output?.configId;
185 const identityChanged =
186 (previousTask !== undefined && toTaskName(previousTask) !== nextTask) ||
187 (previousId !== undefined &&
188 news.configId !== undefined &&
189 news.configId !== previousId);
190 const previousUrl = olds?.url ?? output?.url;
191 const previousToken = olds?.token ?? output?.token;
192 const previousAuth = olds?.authentication ?? output?.authentication;
193 const payloadChanged =
194 (previousUrl !== undefined && news.url !== previousUrl) ||
195 (news.token !== undefined &&
196 previousToken !== undefined &&
197 news.token !== previousToken) ||
198 (news.authentication !== undefined &&
199 previousAuth !== undefined &&
200 !jsonEqual(news.authentication, previousAuth));
201 if (!identityChanged && !payloadChanged) return undefined;
202 const samePhysical =
203 previousTask !== undefined &&
204 toTaskName(previousTask) === nextTask &&
205 previousId !== undefined &&
206 (news.configId === undefined || news.configId === previousId);
207 return {
208 action: "replace" as const,
209 deleteFirst: samePhysical,
210 };
211 }),
212
213 read: Effect.fn(function* ({ id, olds, output }) {
214 const env = yield* GcpEnvironment.current;
215 const task = toTaskName(olds?.task ?? output?.task ?? "");
216 const configId = yield* toPhysicalId(
217 id,
218 olds?.configId,
219 output?.configId,
220 "config",
221 MAX_CONFIG_ID_LENGTH,
222 );
223 const name = output?.name ?? toConfigName(task, configId);
224 const existing = yield* getConfig(name, olds?.tenant ?? output?.tenant);
225 if (existing === undefined) return undefined;
226 const attrs = toAttrs(
227 existing,
228 env.project,
229 olds?.tenant ?? output?.tenant,
230 );
231 // Only an explicit config id found without state is ambiguous.
232 return output === undefined && olds?.configId !== undefined
233 ? Unowned(attrs)
234 : attrs;
235 }),
236
237 reconcile: Effect.fn(function* ({ id, news, output }) {
238 const env = yield* GcpEnvironment.current;
239 const task = toTaskName(news.task);
240 const configId = yield* toPhysicalId(
241 id,
242 news.configId,
243 output?.configId,
244 "config",
245 MAX_CONFIG_ID_LENGTH,
246 );
247 const name = output?.name ?? toConfigName(task, configId);
248 const token = news.token;
249
250 let current = yield* getConfig(name, news.tenant);
251
252 if (current === undefined) {
253 const created = yield* we
254 .createTasksPushNotificationConfigs({
255 parent: task,
256 configId,
257 tenant: news.tenant,
258 body: {
259 name,
260 pushNotificationConfig: {
261 url: news.url,
262 authentication: news.authentication,
263 token,
264 id: configId,
265 },
266 },
267 })
268 .pipe(
269 Effect.catchTag("Conflict", () => getConfig(name, news.tenant)),
270 );
271 current = created ?? (yield* getConfig(name, news.tenant));
272 }
273
274 if (current === undefined) {
275 return yield* new TasksPushNotificationConfigNotResolved({ name });
276 }
277
278 return toAttrs(current, env.project, news.tenant);
279 }),
280
281 delete: Effect.fn(function* ({ output }) {
282 const name = output.name || toConfigName(output.task, output.configId);
283 if (name.length === 0) return;
284 yield* we
285 .deleteTasksPushNotificationConfigs({
286 name,
287 tenant: output.tenant,
288 })
289 .pipe(Effect.catchTag("NotFound", () => Effect.void));
290 }),
291 });