1import { assert, it } from "@effect/vitest";
2import {
3 AuthAccessReadScope,
4 AuthAccessWriteScope,
5 AuthAdministrativeScopes,
6 AuthFilesystemReadScope,
7 AuthPreviewOperateScope,
8 authScopeResponse,
9 AuthOrchestrationReadScope,
10 AuthOrchestrationOperateScope,
11 AuthSessionId,
12 EnvironmentId,
13 ORCHESTRATION_PROTOCOL_VERSION,
14 type AuthEnvironmentScope,
15 type ExecutionEnvironmentDescriptor,
16} from "@t3tools/contracts";
17import * as Effect from "effect/Effect";
18import * as FileSystem from "effect/FileSystem";
19import * as Layer from "effect/Layer";
20import * as Option from "effect/Option";
21import { HttpClient, HttpClientResponse } from "effect/http";
22import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
23import * as ServerEnvironment from "./ServerEnvironment.ts";
24import * as Hub from "./Hub.ts";
25import * as OutboundHub from "./OutboundHub.ts";
26import * as HostedAuthConfig from "../auth/HostedAuthConfig.ts";
27
28const leafId = EnvironmentId.make("leaf-a");
29const descriptor: ExecutionEnvironmentDescriptor = {
30 environmentId: leafId,
31 label: "Laptop",
32 platform: { os: "linux", arch: "x64" },
33 serverVersion: "test",
34 orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION,
35 capabilities: { repositoryIdentity: true },
36};
37const session = (scopes: ReadonlyArray<AuthEnvironmentScope> = AuthAdministrativeScopes) => ({
38 sessionId: AuthSessionId.make("phone"),
39 subject: "phone",
40 method: "bearer-access-token" as const,
41 scopes,
42});
43
44const fixture = (
45 grant: ReadonlyArray<AuthEnvironmentScope> = AuthAdministrativeScopes,
46 permissions: "modern" | "legacy" | "empty" = "modern",
47) =>
48 Effect.gen(function* () {
49 const data = new Map<string, Uint8Array>();
50 const requests: Array<{ url: string; authorization?: string; body: string }> = [];
51 let identity = leafId;
52 let serverVersion = "test";
53 let unavailable = false;
54 let malformed = false;
55 const client = HttpClient.make((request) =>
56 Effect.sync(() => {
57 requests.push({
58 url: request.url,
59 ...(request.headers.authorization
60 ? { authorization: request.headers.authorization }
61 : {}),
62 body:
63 request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : "",
64 });
65 if (unavailable)
66 return HttpClientResponse.fromWeb(request, new Response(null, { status: 503 }));
67 let result: unknown;
68 if (request.url.endsWith("/.well-known/t3/environment"))
69 result = malformed ? {} : { ...descriptor, environmentId: identity, serverVersion };
70 else if (request.url.endsWith("/api/auth/session"))
71 result = {
72 authenticated: true,
73 auth: {
74 policy: "remote-reachable",
75 bootstrapMethods: ["one-time-token"],
76 sessionMethods: ["bearer-access-token"],
77 sessionCookieName: "session",
78 },
79 scopes: authScopeResponse(grant).scopes,
80 ...(permissions === "legacy"
81 ? {}
82 : { permissions: permissions === "empty" ? [] : grant }),
83 };
84 else if (request.url.endsWith("/api/auth/delegated-session"))
85 result = {
86 access_token: "private-leaf-session",
87 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
88 token_type: "Bearer",
89 expires_in: 3600,
90 scope: JSON.parse(requests.at(-1)?.body ?? "{}").scopes?.join(" "),
91 };
92 else throw new Error(`Unexpected request ${request.url}`);
93 return HttpClientResponse.fromWeb(request, Response.json(result));
94 }),
95 );
96 const store = ServerSecretStore.ServerSecretStore.of({
97 get: (key) => Effect.sync(() => Option.fromUndefinedOr(data.get(key))),
98 set: (key, value) =>
99 Effect.sync(() => {
100 data.set(key, value);
101 }),
102 remove: (key) =>
103 Effect.sync(() => {
104 data.delete(key);
105 }),
106 create: () => Effect.die("unused"),
107 getOrCreateRandom: () => Effect.die("unused"),
108 });
109 const layer = Hub.layer.pipe(
110 Layer.provide(Layer.succeed(HostedAuthConfig.HostedAuthConfig, Option.none())),
111 Layer.provide(
112 Layer.succeed(OutboundHub.OutboundHub, {
113 enrollments: Effect.succeed([]),
114 connect: () => Effect.die("unused"),
115 remove: () => Effect.die("unused"),
116 accept: () => Effect.die("unused"),
117 channel: () => Effect.die("unused"),
118 }),
119 ),
120 Layer.provide(Layer.succeed(ServerSecretStore.ServerSecretStore, store)),
121 Layer.provide(Layer.succeed(HttpClient.HttpClient, client)),
122 Layer.provide(
123 Layer.succeed(ServerEnvironment.ServerEnvironment, {
124 getEnvironmentId: Effect.succeed(EnvironmentId.make("hub")),
125 getDescriptor: Effect.succeed({
126 ...descriptor,
127 environmentId: EnvironmentId.make("hub"),
128 }),
129 }),
130 ),
131 Layer.provide(
132 Layer.succeed(
133 FileSystem.FileSystem,
134 FileSystem.makeNoop({ readFileString: () => Effect.succeed("leaf-admin-token\n") }),
135 ),
136 ),
137 );
138 const service = yield* Hub.Hub.pipe(Effect.provide(layer));
139 const enroll = service.manage({
140 type: "hub.enroll",
141 url: "http://leaf.private/base/",
142 environmentId: leafId,
143 tokenFile: "/private/grant",
144 });
145 return {
146 service,
147 requests,
148 data,
149 enroll,
150 restart: Hub.Hub.pipe(Effect.provide(layer)),
151 changeVersion: (value: string) => {
152 serverVersion = value;
153 },
154 setUnavailable: () => {
155 unavailable = true;
156 },
157 setMalformed: () => {
158 malformed = true;
159 },
160 changeIdentity: (id: EnvironmentId) => {
161 identity = id;
162 },
163 };
164 });
165
166it.effect(
167 "requires an explicit canonical public URL and persists an identity-checked private enrollment",
168 () =>
169 Effect.gen(function* () {
170 const f = yield* fixture();
171 assert.strictEqual((yield* f.enroll.pipe(Effect.flip)).reason, "configuration");
172 yield* f.service.manage({ type: "hub.configure", publicUrl: "https://hub.example/prefix" });
173 yield* f.enroll;
174 const directory = yield* (yield* f.restart).directory(session());
175 assert.strictEqual(
176 directory.environments[0]?.httpBaseUrl,
177 "https://hub.example/prefix/environments/leaf-a/",
178 );
179 assert.strictEqual(directory.hubEnvironmentId, "hub");
180 assert.strictEqual(JSON.stringify(directory).includes("leaf-admin-token"), false);
181 assert.strictEqual(JSON.stringify(directory).includes("leaf.private"), false);
182 assert.strictEqual(f.requests[0]?.authorization, undefined);
183 }),
184);
185
186it.effect("does not send a private grant to a different machine identity", () =>
187 Effect.gen(function* () {
188 const f = yield* fixture();
189 yield* f.service.manage({ type: "hub.configure", publicUrl: "https://hub.example/" });
190 f.changeIdentity(EnvironmentId.make("impostor"));
191 assert.strictEqual((yield* f.enroll.pipe(Effect.flip)).reason, "identity");
192 assert.strictEqual(f.requests.length, 1);
193 assert.strictEqual(f.requests[0]?.authorization, undefined);
194 }),
195);
196
197it.effect(
198 "bounds delegated sessions by both client and enrolled machine grants and keeps leaf credentials private",
199 () =>
200 Effect.gen(function* () {
201 const f = yield* fixture([
202 AuthAccessReadScope,
203 AuthAccessWriteScope,
204 AuthOrchestrationReadScope,
205 ]);
206 yield* f.service.manage({ type: "hub.configure", publicUrl: "https://hub.example/" });
207 yield* f.enroll;
208 const connected = yield* f.service.connect(
209 leafId,
210 session([AuthOrchestrationReadScope, AuthOrchestrationOperateScope, AuthAccessWriteScope]),
211 );
212 assert.strictEqual(connected.token, "private-leaf-session");
213 const pairing = f.requests.find((r) => r.url.endsWith("delegated-session"));
214 assert.deepStrictEqual(JSON.parse(pairing?.body ?? "{}").scopes, [
215 AuthOrchestrationReadScope,
216 ]);
217 assert.deepStrictEqual((yield* f.service.directory(session())).environments[0]?.scopes, [
218 AuthOrchestrationReadScope,
219 ]);
220 const count = f.requests.length;
221 yield* f.service.connect(
222 leafId,
223 session([AuthOrchestrationReadScope, AuthOrchestrationOperateScope, AuthAccessWriteScope]),
224 );
225 assert.strictEqual(f.requests.length, count);
226 }),
227);
228
229it.effect(
230 "removing or replacing a machine invalidates its active proxy and rejects subsequent connections",
231 () =>
232 Effect.gen(function* () {
233 const f = yield* fixture();
234 yield* f.service.manage({ type: "hub.configure", publicUrl: "https://hub.example/" });
235 yield* f.enroll;
236 const connected = yield* f.service.connect(leafId, session());
237 yield* f.service.manage({ type: "hub.remove", environmentId: leafId });
238 yield* connected.invalidated;
239 assert.strictEqual(
240 (yield* f.service.connect(leafId, session()).pipe(Effect.flip)).reason,
241 "not-found",
242 );
243 assert.deepStrictEqual((yield* f.service.directory(session())).environments, []);
244 }),
245);
246
247it.effect("checks machine identity again before releasing its saved delegation grant", () =>
248 Effect.gen(function* () {
249 const f = yield* fixture();
250 yield* f.service.manage({ type: "hub.configure", publicUrl: "https://hub.example/" });
251 yield* f.enroll;
252 f.changeIdentity(EnvironmentId.make("replacement"));
253 const count = f.requests.length;
254 assert.strictEqual(
255 (yield* f.service.connect(leafId, session()).pipe(Effect.flip)).reason,
256 "identity",
257 );
258 assert.strictEqual(f.requests.length, count + 1);
259 assert.strictEqual(f.requests.at(-1)?.authorization, undefined);
260 }),
261);
262
263it.effect(
264 "refreshes changed machine descriptors, persists them for offline discovery, and never hides an identity mismatch",
265 () =>
266 Effect.gen(function* () {
267 const f = yield* fixture();
268 yield* f.service.manage({ type: "hub.configure", publicUrl: "https://hub.example/" });
269 yield* f.enroll;
270 f.changeVersion("updated");
271 assert.strictEqual((yield* f.service.descriptor(leafId)).serverVersion, "updated");
272 assert.strictEqual(
273 (yield* f.service.directory(session())).environments[0]?.descriptor.serverVersion,
274 "updated",
275 );
276 f.setUnavailable();
277 const restarted = yield* f.restart;
278 assert.strictEqual((yield* restarted.descriptor(leafId)).serverVersion, "updated");
279 }),
280);
281
282it.effect(
283 "rejects malformed or changed discovery identities instead of treating them as offline",
284 () =>
285 Effect.gen(function* () {
286 const f = yield* fixture();
287 yield* f.service.manage({ type: "hub.configure", publicUrl: "https://hub.example/" });
288 yield* f.enroll;
289 f.changeIdentity(EnvironmentId.make("replacement"));
290 assert.strictEqual(
291 (yield* f.service.directory(session()).pipe(Effect.flip)).reason,
292 "identity",
293 );
294 f.setMalformed();
295 assert.strictEqual(
296 (yield* f.service.descriptor(leafId).pipe(Effect.flip)).reason,
297 "identity",
298 );
299 }),
300);
301
302it.effect.each(["modern", "legacy"] as const)(
303 "delegates granular permissions from a %s machine's auth response",
304 (permissions) =>
305 Effect.gen(function* () {
306 const f = yield* fixture(AuthAdministrativeScopes, permissions);
307 yield* f.service.manage({ type: "hub.configure", publicUrl: "https://hub.example/" });
308 yield* f.enroll;
309 yield* f.service.connect(leafId, session());
310 const pairing = f.requests.find((request) => request.url.endsWith("delegated-session"));
311 const scopes = JSON.parse(pairing?.body ?? "{}").scopes;
312 assert.include(scopes, AuthFilesystemReadScope);
313 assert.include(scopes, AuthPreviewOperateScope);
314 const directory = yield* f.service.directory(session());
315 assert.strictEqual(directory.environments[0]?.scopes.includes(AuthFilesystemReadScope), true);
316 assert.strictEqual(directory.environments[0]?.scopes.includes(AuthPreviewOperateScope), true);
317 }),
318);
319
320it.effect("does not expand an explicitly empty machine permission grant", () =>
321 Effect.gen(function* () {
322 const f = yield* fixture(AuthAdministrativeScopes, "empty");
323 yield* f.service.manage({ type: "hub.configure", publicUrl: "https://hub.example/" });
324 assert.strictEqual((yield* f.enroll.pipe(Effect.flip)).reason, "credential");
325 }),
326);