| 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import { |
| 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"; |
| 17 | import * as Effect from "effect/Effect"; |
| 18 | import * as FileSystem from "effect/FileSystem"; |
| 19 | import * as Layer from "effect/Layer"; |
| 20 | import * as Option from "effect/Option"; |
| 21 | import { HttpClient, HttpClientResponse } from "effect/http"; |
| 22 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; |
| 23 | import * as ServerEnvironment from "./ServerEnvironment.ts"; |
| 24 | import * as Hub from "./Hub.ts"; |
| 25 | import * as OutboundHub from "./OutboundHub.ts"; |
| 26 | import * as HostedAuthConfig from "../auth/HostedAuthConfig.ts"; |
| 27 | |
| 28 | const leafId = EnvironmentId.make("leaf-a"); |
| 29 | const 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 | }; |
| 37 | const session = (scopes: ReadonlyArray<AuthEnvironmentScope> = AuthAdministrativeScopes) => ({ |
| 38 | sessionId: AuthSessionId.make("phone"), |
| 39 | subject: "phone", |
| 40 | method: "bearer-access-token" as const, |
| 41 | scopes, |
| 42 | }); |
| 43 | |
| 44 | const 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 | |
| 166 | it.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 | |
| 186 | it.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 | |
| 197 | it.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 | |
| 229 | it.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 | |
| 247 | it.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 | |
| 263 | it.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 | |
| 282 | it.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 | |
| 302 | it.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 | |
| 320 | it.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 | ); |