1import { EnvironmentId } from "@t3tools/contracts";
2import { assert, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as FetchHttpClient from "effect/http/FetchHttpClient";
8import { HttpClient, HttpServer } from "effect/http";
9import * as NetAddress from "effect/net/NetAddress";
10import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
11import * as NodeServices from "@effect/platform-node/NodeServices";
12
13import * as EnvironmentKeys from "./environmentKeys.ts";
14import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
15import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
16import * as ServerConfig from "../config.ts";
17import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
18import * as AgentAwarenessRelay from "../relay/AgentAwarenessRelay.ts";
19import * as CliTokenManager from "./CliTokenManager.ts";
20import * as CloudLink from "./CloudLink.ts";
21import * as ConnectReachability from "./ConnectReachability.ts";
22import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";
23import {
24 CLOUD_LINKED_USER_ID,
25 HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET,
26 PUBLISH_AGENT_ACTIVITY_SECRET,
27 RELAY_ENVIRONMENT_CREDENTIAL_SECRET,
28 RELAY_URL_SECRET,
29} from "./config.ts";
30import * as ManagedPolicy from "../policy/ManagedPolicy.ts";
31
32const encode = (value: string) => new TextEncoder().encode(value);
33
34const withService = <A, E>(
35 options: {
36 readonly failHoldWrite?: boolean;
37 readonly failActivityWrite?: boolean;
38 readonly relayFails?: boolean;
39 readonly failActivityRead?: boolean;
40 readonly failHoldRead?: boolean;
41 readonly holdFirstRelayCall?: {
42 readonly started: () => void;
43 readonly released: Promise<void>;
44 };
45 },
46 body: (input: {
47 readonly preferences: { readonly update: CloudLink.CloudLink["Service"]["updatePreferences"] };
48 readonly stored: Map<string, Uint8Array>;
49 readonly relayCalls: Array<boolean>;
50 }) => Effect.Effect<A, E, never>,
51) =>
52 Effect.gen(function* () {
53 const stored = new Map<string, Uint8Array>([
54 [RELAY_URL_SECRET, encode("https://relay.test")],
55 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, encode("credential")],
56 [HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET, encode("false")],
57 [PUBLISH_AGENT_ACTIVITY_SECRET, encode("false")],
58 ]);
59 const relayCalls: Array<boolean> = [];
60 const fetch: typeof globalThis.fetch = Object.assign(
61 (_input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
62 const payload = JSON.parse(String(init?.body)) as { holdWebhooksWhileOffline: boolean };
63 relayCalls.push(payload.holdWebhooksWhileOffline);
64 if (relayCalls.length === 1 && options.holdFirstRelayCall) {
65 options.holdFirstRelayCall.started();
66 return options.holdFirstRelayCall.released.then(() => Response.json(payload));
67 }
68 return options.relayFails
69 ? Promise.resolve(new Response("unavailable", { status: 503 }))
70 : Promise.resolve(Response.json(payload));
71 },
72 { preconnect: () => {} },
73 );
74 const layerDependencies = Layer.mergeAll(
75 Layer.mock(ServerSecretStore.ServerSecretStore)({
76 get: (name) =>
77 (options.failActivityRead && name === PUBLISH_AGENT_ACTIVITY_SECRET) ||
78 (options.failHoldRead && name === HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET)
79 ? Effect.fail(
80 new ServerSecretStore.SecretStoreReadError({
81 resource: name,
82 cause: new Error("busy"),
83 }),
84 )
85 : Effect.succeed(Option.fromNullishOr(stored.get(name))),
86 set: (name, value) =>
87 (options.failHoldWrite && name === HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET) ||
88 (options.failActivityWrite && name === PUBLISH_AGENT_ACTIVITY_SECRET)
89 ? Effect.fail(
90 new ServerSecretStore.SecretStorePersistError({
91 resource: name,
92 cause: new Error("read-only"),
93 }),
94 )
95 : Effect.sync(() => void stored.set(name, value)),
96 }),
97 Layer.mock(ServerEnvironment.ServerEnvironment)({
98 getEnvironmentId: Effect.succeed(EnvironmentId.make("environment-1")),
99 }),
100 Layer.mock(AgentAwarenessRelay.AgentAwarenessRelay)({ requestCatchUp: () => Effect.void }),
101 Layer.mock(ManagedEndpointRuntime.CloudManagedEndpointRuntime)({}),
102 ManagedPolicy.layerTest(),
103 Layer.mock(ConnectReachability.ConnectReachability)({
104 latest: Effect.succeedNone,
105 }),
106 Layer.mock(EnvironmentAuth.EnvironmentAuth)({}),
107 Layer.mock(CliTokenManager.CloudCliTokenManager)({}),
108 Layer.mock(HttpServer.HttpServer)({
109 address: NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 3773),
110 }),
111 Layer.mock(ServerConfig.ServerConfig)({} as ServerConfig.ServerConfig["Service"]),
112 Layer.succeed(
113 HttpClient.HttpClient,
114 HttpClient.make(() => Effect.die("unused")),
115 ),
116 NodeServices.layer,
117 NodeCrypto.layer,
118 );
119 return yield* Effect.gen(function* () {
120 const link = yield* CloudLink.CloudLink;
121 return yield* body({ preferences: { update: link.updatePreferences }, stored, relayCalls });
122 }).pipe(
123 Effect.provide(
124 CloudLink.layer.pipe(
125 // These tests never sign proofs; the mocked secret store cannot create a key pair.
126 Layer.provide(Layer.mock(EnvironmentKeys.EnvironmentSigningKey)({ publicKey: "" })),
127 Layer.provide(layerDependencies),
128 ),
129 ),
130 Effect.provideService(FetchHttpClient.Fetch, fetch),
131 );
132 });
133
134it.effect("tells the relay before saving the hold setting locally", () =>
135 withService({}, ({ preferences, stored, relayCalls }) =>
136 Effect.gen(function* () {
137 yield* preferences.update({ publishAgentActivity: true, holdWebhooksWhileOffline: true });
138 assert.deepEqual(relayCalls, [true]);
139 assert.equal(
140 new TextDecoder().decode(stored.get(HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET)),
141 "true",
142 );
143 }),
144 ),
145);
146
147it.effect("puts the relay back when the local save fails", () =>
148 withService({ failHoldWrite: true }, ({ preferences, stored, relayCalls }) =>
149 Effect.gen(function* () {
150 const error = yield* preferences
151 .update({ publishAgentActivity: true, holdWebhooksWhileOffline: true })
152 .pipe(Effect.flip);
153 assert.deepInclude(error, {
154 _tag: "CloudLinkInternalError",
155 operation: "persist-preferences",
156 });
157 assert.deepEqual(relayCalls, [true, false]);
158 assert.equal(
159 new TextDecoder().decode(stored.get(HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET)),
160 "false",
161 );
162 }),
163 ),
164);
165
166it.effect("leaves the relay untouched when the activity setting can't be saved", () =>
167 withService({ failActivityWrite: true }, ({ preferences, stored, relayCalls }) =>
168 Effect.gen(function* () {
169 const error = yield* preferences
170 .update({ publishAgentActivity: true, holdWebhooksWhileOffline: true })
171 .pipe(Effect.flip);
172 assert.deepInclude(error, {
173 _tag: "CloudLinkInternalError",
174 operation: "persist-preferences",
175 });
176 assert.deepEqual(relayCalls, []);
177 assert.equal(
178 new TextDecoder().decode(stored.get(HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET)),
179 "false",
180 );
181 }),
182 ),
183);
184
185it.effect("keeps both settings unchanged when the relay refuses the hold change", () =>
186 withService({ relayFails: true }, ({ preferences, stored }) =>
187 Effect.gen(function* () {
188 yield* preferences
189 .update({ publishAgentActivity: true, holdWebhooksWhileOffline: true })
190 .pipe(Effect.flip);
191 assert.equal(new TextDecoder().decode(stored.get(PUBLISH_AGENT_ACTIVITY_SECRET)), "false");
192 assert.equal(
193 new TextDecoder().decode(stored.get(HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET)),
194 "false",
195 );
196 }),
197 ),
198);
199
200it.effect("two overlapping updates are applied one after the other", () => {
201 let release!: () => void;
202 let started!: () => void;
203 const released = new Promise<void>((resolve) => {
204 release = resolve;
205 });
206 const firstAtRelay = new Promise<void>((resolve) => {
207 started = resolve;
208 });
209 return withService({ holdFirstRelayCall: { started, released } }, ({ preferences, stored }) =>
210 Effect.gen(function* () {
211 const first = yield* preferences
212 .update({ publishAgentActivity: true, holdWebhooksWhileOffline: true })
213 .pipe(Effect.forkChild);
214 yield* Effect.promise(() => firstAtRelay);
215 const second = yield* preferences
216 .update({ publishAgentActivity: false, holdWebhooksWhileOffline: false })
217 .pipe(Effect.forkChild);
218 yield* Effect.yieldNow;
219 assert.equal(new TextDecoder().decode(stored.get(PUBLISH_AGENT_ACTIVITY_SECRET)), "true");
220 release();
221 yield* Fiber.join(first);
222 yield* Fiber.join(second);
223 assert.equal(new TextDecoder().decode(stored.get(PUBLISH_AGENT_ACTIVITY_SECRET)), "false");
224 assert.equal(
225 new TextDecoder().decode(stored.get(HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET)),
226 "false",
227 );
228 }),
229 );
230});
231
232it.effect("an update the client walks away from still finishes", () => {
233 let release!: () => void;
234 let started!: () => void;
235 const released = new Promise<void>((resolve) => {
236 release = resolve;
237 });
238 const atRelay = new Promise<void>((resolve) => {
239 started = resolve;
240 });
241 return withService({ holdFirstRelayCall: { started, released } }, ({ preferences, stored }) =>
242 Effect.gen(function* () {
243 const update = yield* preferences
244 .update({ publishAgentActivity: true, holdWebhooksWhileOffline: true })
245 .pipe(Effect.forkChild);
246 yield* Effect.promise(() => atRelay);
247 const interrupted = yield* Fiber.interrupt(update).pipe(Effect.forkChild);
248 yield* Effect.yieldNow;
249 release();
250 yield* Fiber.join(interrupted);
251 assert.equal(new TextDecoder().decode(stored.get(PUBLISH_AGENT_ACTIVITY_SECRET)), "true");
252 assert.equal(
253 new TextDecoder().decode(stored.get(HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET)),
254 "true",
255 );
256 }),
257 );
258});
259
260it.effect("unlinks on startup and refuses to link when the policy turns Connect off", () =>
261 Effect.gen(function* () {
262 const stored = new Map<string, Uint8Array>([
263 [CLOUD_LINKED_USER_ID, encode("user-1")],
264 [RELAY_URL_SECRET, encode("https://relay.test")],
265 ]);
266 const appliedConfigs: Array<unknown> = [];
267 const layerDependencies = Layer.mergeAll(
268 Layer.mock(ServerSecretStore.ServerSecretStore)({
269 get: (name) => Effect.succeed(Option.fromNullishOr(stored.get(name))),
270 set: (name, value) => Effect.sync(() => void stored.set(name, value)),
271 remove: (name) => Effect.sync(() => void stored.delete(name)),
272 }),
273 Layer.mock(ServerEnvironment.ServerEnvironment)({}),
274 Layer.mock(AgentAwarenessRelay.AgentAwarenessRelay)({}),
275 Layer.mock(ManagedEndpointRuntime.CloudManagedEndpointRuntime)({
276 withLinkStateLock: (effect) => effect,
277 applyConfig: (config) =>
278 Effect.sync(() => {
279 appliedConfigs.push(config);
280 return { status: "disabled" as const };
281 }),
282 }),
283 ManagedPolicy.layerTest({
284 schemaVersion: 1,
285 organization: "Example Corp",
286 limits: { connect: false },
287 }),
288 Layer.mock(ConnectReachability.ConnectReachability)({}),
289 Layer.mock(EnvironmentAuth.EnvironmentAuth)({}),
290 Layer.mock(CliTokenManager.CloudCliTokenManager)({}),
291 Layer.mock(HttpServer.HttpServer)({
292 address: NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 3773),
293 }),
294 Layer.mock(ServerConfig.ServerConfig)({} as ServerConfig.ServerConfig["Service"]),
295 Layer.succeed(
296 HttpClient.HttpClient,
297 HttpClient.make(() => Effect.die("unused")),
298 ),
299 NodeServices.layer,
300 NodeCrypto.layer,
301 );
302 yield* Effect.gen(function* () {
303 const link = yield* CloudLink.CloudLink;
304 assert.isFalse(stored.has(CLOUD_LINKED_USER_ID));
305 assert.deepEqual(appliedConfigs, [null]);
306
307 const applyError = yield* link.applyRelayConfig({} as never).pipe(Effect.flip);
308 const mintError = yield* link.mintCredential({} as never).pipe(Effect.flip);
309 assert.equal(applyError.message, "Example Corp's policy does not allow T3 Connect.");
310 assert.equal(mintError._tag, "ManagedPolicyRefusedError");
311 }).pipe(
312 Effect.provide(
313 CloudLink.layer.pipe(
314 // These tests never sign proofs; the mocked secret store cannot create a key pair.
315 Layer.provide(Layer.mock(EnvironmentKeys.EnvironmentSigningKey)({ publicKey: "" })),
316 Layer.provide(layerDependencies),
317 ),
318 ),
319 );
320 }),
321);