| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign. |
| 2 | import * as NodeCrypto from "node:crypto"; |
| 3 | import type { |
| 4 | RelayEnvironmentLinkProofPayload, |
| 5 | RelayEnvironmentLinkRequest, |
| 6 | } from "@t3tools/contracts/relay"; |
| 7 | import { RELAY_LINK_PROOF_TYP } from "@t3tools/shared/relayJwt"; |
| 8 | import { describe, expect, it } from "@effect/vitest"; |
| 9 | import * as DateTime from "effect/DateTime"; |
| 10 | import * as Effect from "effect/Effect"; |
| 11 | import * as Layer from "effect/Layer"; |
| 12 | import * as Redacted from "effect/Redacted"; |
| 13 | import * as Result from "effect/Result"; |
| 14 | import * as Schema from "effect/Schema"; |
| 15 | |
| 16 | import * as DpopProofs from "../auth/DpopProofs.ts"; |
| 17 | import * as RelayTokens from "../auth/RelayTokens.ts"; |
| 18 | import * as EnvironmentCredentials from "./EnvironmentCredentials.ts"; |
| 19 | import * as EnvironmentLinks from "./EnvironmentLinks.ts"; |
| 20 | import * as RelayConfiguration from "../Config.ts"; |
| 21 | import * as EnvironmentLinker from "./EnvironmentLinker.ts"; |
| 22 | import * as ManagedEndpointProvider from "./ManagedEndpointProvider.ts"; |
| 23 | |
| 24 | const relayKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { |
| 25 | privateKeyEncoding: { format: "pem", type: "pkcs8" }, |
| 26 | publicKeyEncoding: { format: "pem", type: "spki" }, |
| 27 | }); |
| 28 | const environmentKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { |
| 29 | privateKeyEncoding: { format: "pem", type: "pkcs8" }, |
| 30 | publicKeyEncoding: { format: "pem", type: "spki" }, |
| 31 | }); |
| 32 | const config = RelayConfiguration.RelayConfiguration.of({ |
| 33 | relayIssuer: "https://relay.example.test", |
| 34 | apns: { |
| 35 | environment: "sandbox", |
| 36 | teamId: "team-id", |
| 37 | keyId: "key-id", |
| 38 | privateKey: Redacted.make("private-key"), |
| 39 | bundleId: "com.t3tools.t3code.dev", |
| 40 | }, |
| 41 | apnsDeliveryJobSigningSecret: Redacted.make("job-secret"), |
| 42 | clerkSecretKey: Redacted.make("clerk-secret"), |
| 43 | clerkPublishableKey: "pk_test_test", |
| 44 | clerkJwtAudience: "t3-code-relay", |
| 45 | cloudMintPrivateKey: Redacted.make(relayKeyPair.privateKey), |
| 46 | cloudMintPublicKey: relayKeyPair.publicKey, |
| 47 | managedEndpointBaseDomain: undefined, |
| 48 | managedEndpointNamespace: undefined, |
| 49 | }); |
| 50 | const isEnvironmentLinkProofInvalid = Schema.is(EnvironmentLinker.EnvironmentLinkProofInvalid); |
| 51 | |
| 52 | function signTestJwt(payload: object, typ: string, privateKey: string): string { |
| 53 | const header = Buffer.from(JSON.stringify({ alg: "EdDSA", typ })).toString("base64url"); |
| 54 | const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url"); |
| 55 | const signingInput = `${header}.${encodedPayload}`; |
| 56 | return `${signingInput}.${NodeCrypto.sign(null, Buffer.from(signingInput), privateKey).toString("base64url")}`; |
| 57 | } |
| 58 | |
| 59 | const makeRequest = Effect.gen(function* () { |
| 60 | const now = yield* DateTime.now; |
| 61 | const expiresAt = DateTime.add(now, { minutes: 5 }); |
| 62 | const relayTokens = yield* RelayTokens.RelayTokens; |
| 63 | const challenge = yield* relayTokens.issueLinkChallenge({ |
| 64 | userId: "user_123", |
| 65 | request: { |
| 66 | notificationsEnabled: true, |
| 67 | liveActivitiesEnabled: true, |
| 68 | managedTunnelsEnabled: true, |
| 69 | }, |
| 70 | jti: "challenge-jti", |
| 71 | issuedAtEpochSeconds: Math.floor(now.epochMilliseconds / 1_000), |
| 72 | expiresAtEpochSeconds: Math.floor(expiresAt.epochMilliseconds / 1_000), |
| 73 | }); |
| 74 | const payload = { |
| 75 | iss: "t3-env:env-link-test", |
| 76 | aud: "https://relay.example.test", |
| 77 | sub: "env-link-test", |
| 78 | jti: "link-proof-jti", |
| 79 | iat: Math.floor(now.epochMilliseconds / 1_000), |
| 80 | exp: Math.floor(expiresAt.epochMilliseconds / 1_000), |
| 81 | challenge, |
| 82 | environmentId: "env-link-test" as RelayEnvironmentLinkProofPayload["environmentId"], |
| 83 | descriptor: { |
| 84 | environmentId: "env-link-test" as RelayEnvironmentLinkProofPayload["environmentId"], |
| 85 | label: "Link Test Environment", |
| 86 | platform: { os: "darwin", arch: "arm64" }, |
| 87 | serverVersion: "0.0.0-test", |
| 88 | capabilities: { repositoryIdentity: true }, |
| 89 | }, |
| 90 | environmentPublicKey: environmentKeyPair.publicKey.trim(), |
| 91 | endpoint: { |
| 92 | httpBaseUrl: "https://env.example.test/", |
| 93 | wsBaseUrl: "wss://env.example.test/", |
| 94 | providerKind: "manual", |
| 95 | }, |
| 96 | origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 }, |
| 97 | scopes: ["agent_activity_notifications", "managed_tunnels"], |
| 98 | } satisfies RelayEnvironmentLinkProofPayload; |
| 99 | return { |
| 100 | request: { |
| 101 | proof: signTestJwt(payload, RELAY_LINK_PROOF_TYP, environmentKeyPair.privateKey), |
| 102 | notificationsEnabled: true, |
| 103 | liveActivitiesEnabled: true, |
| 104 | managedTunnelsEnabled: false, |
| 105 | } satisfies RelayEnvironmentLinkRequest, |
| 106 | payload, |
| 107 | }; |
| 108 | }); |
| 109 | |
| 110 | function layerTest(input?: { |
| 111 | readonly upsert?: EnvironmentLinks.EnvironmentLinks["Service"]["upsert"]; |
| 112 | readonly consume?: DpopProofs.DpopProofReplay["Service"]["consume"]; |
| 113 | readonly deprovision?: ManagedEndpointProvider.ManagedEndpointProvider["Service"]["deprovision"]; |
| 114 | }) { |
| 115 | return EnvironmentLinker.layer.pipe( |
| 116 | Layer.provideMerge(RelayTokens.layer), |
| 117 | Layer.provide( |
| 118 | Layer.mergeAll( |
| 119 | RelayConfiguration.layer(config), |
| 120 | Layer.succeed(DpopProofs.DpopProofReplay, { |
| 121 | verifyAndConsume: () => Effect.die("unexpected DPoP proof verification"), |
| 122 | consume: input?.consume ?? (() => Effect.succeed(true)), |
| 123 | pruneExpired: Effect.void, |
| 124 | }), |
| 125 | Layer.succeed(EnvironmentLinks.EnvironmentLinks, { |
| 126 | upsert: input?.upsert ?? (() => Effect.void), |
| 127 | listDeliveryUsersForEnvironment: () => Effect.succeed([]), |
| 128 | listForUser: () => Effect.succeed([]), |
| 129 | getForUser: () => Effect.succeed(null), |
| 130 | findActiveManagedForEnvironment: () => Effect.succeed([]), |
| 131 | setHoldWebhooksWhileOffline: () => Effect.void, |
| 132 | revokeForUser: () => Effect.succeed(false), |
| 133 | }), |
| 134 | Layer.succeed(EnvironmentCredentials.EnvironmentCredentials, { |
| 135 | create: () => Effect.succeed("t3env_credential_secret"), |
| 136 | authenticate: () => Effect.succeedNone, |
| 137 | revokeForEnvironmentPublicKey: () => Effect.succeed(false), |
| 138 | }), |
| 139 | Layer.succeed(ManagedEndpointProvider.ManagedEndpointProvider, { |
| 140 | reconcileOrigin: () => Effect.succeed("ready"), |
| 141 | prepareDeprovision: () => Effect.succeed(null), |
| 142 | deprovision: input?.deprovision ?? (() => Effect.succeed(true)), |
| 143 | release: () => Effect.succeed(true), |
| 144 | provision: () => |
| 145 | Effect.succeed({ |
| 146 | endpoint: { |
| 147 | httpBaseUrl: "https://managed.example.test/", |
| 148 | wsBaseUrl: "wss://managed.example.test/ws", |
| 149 | providerKind: "cloudflare_tunnel", |
| 150 | }, |
| 151 | runtime: { providerKind: "cloudflare_tunnel", connectorToken: "connector-token" }, |
| 152 | }), |
| 153 | }), |
| 154 | ), |
| 155 | ), |
| 156 | ); |
| 157 | } |
| 158 | |
| 159 | describe("EnvironmentLinker", () => { |
| 160 | it.effect("uses verified JWT claims when linking an environment", () => { |
| 161 | let persistedEnvironmentId: string | null = null; |
| 162 | return Effect.gen(function* () { |
| 163 | const { request, payload } = yield* makeRequest; |
| 164 | const linker = yield* EnvironmentLinker.EnvironmentLinker; |
| 165 | const result = yield* linker.link({ userId: "user_123", request }); |
| 166 | expect(result.environmentId).toBe(payload.environmentId); |
| 167 | expect(result.environmentCredential).toBe("t3env_credential_secret"); |
| 168 | expect(persistedEnvironmentId).toBe(payload.environmentId); |
| 169 | }).pipe( |
| 170 | Effect.provide( |
| 171 | layerTest({ |
| 172 | upsert: (input) => |
| 173 | Effect.sync(() => { |
| 174 | persistedEnvironmentId = input.proof.environmentId; |
| 175 | }), |
| 176 | }), |
| 177 | ), |
| 178 | ); |
| 179 | }); |
| 180 | |
| 181 | it.effect("links a publish-only environment with a non-secure nominal endpoint", () => { |
| 182 | let persistedEndpoint: string | null = null; |
| 183 | let deprovisionedEnvironmentId: string | null = null; |
| 184 | return Effect.gen(function* () { |
| 185 | const now = yield* DateTime.now; |
| 186 | const expiresAt = DateTime.add(now, { minutes: 5 }); |
| 187 | const relayTokens = yield* RelayTokens.RelayTokens; |
| 188 | const challenge = yield* relayTokens.issueLinkChallenge({ |
| 189 | userId: "user_123", |
| 190 | request: { |
| 191 | notificationsEnabled: true, |
| 192 | liveActivitiesEnabled: true, |
| 193 | managedTunnelsEnabled: false, |
| 194 | }, |
| 195 | jti: "publish-only-challenge-jti", |
| 196 | issuedAtEpochSeconds: Math.floor(now.epochMilliseconds / 1_000), |
| 197 | expiresAtEpochSeconds: Math.floor(expiresAt.epochMilliseconds / 1_000), |
| 198 | }); |
| 199 | const payload = { |
| 200 | iss: "t3-env:env-link-test", |
| 201 | aud: "https://relay.example.test", |
| 202 | sub: "env-link-test", |
| 203 | jti: "publish-only-proof-jti", |
| 204 | iat: Math.floor(now.epochMilliseconds / 1_000), |
| 205 | exp: Math.floor(expiresAt.epochMilliseconds / 1_000), |
| 206 | challenge, |
| 207 | environmentId: "env-link-test" as RelayEnvironmentLinkProofPayload["environmentId"], |
| 208 | descriptor: { |
| 209 | environmentId: "env-link-test" as RelayEnvironmentLinkProofPayload["environmentId"], |
| 210 | label: "Link Test Environment", |
| 211 | platform: { os: "darwin", arch: "arm64" }, |
| 212 | serverVersion: "0.0.0-test", |
| 213 | capabilities: { repositoryIdentity: true }, |
| 214 | }, |
| 215 | environmentPublicKey: environmentKeyPair.publicKey.trim(), |
| 216 | endpoint: { |
| 217 | httpBaseUrl: "http://127.0.0.1:3773/", |
| 218 | wsBaseUrl: "ws://127.0.0.1:3773/", |
| 219 | providerKind: "manual", |
| 220 | }, |
| 221 | origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 }, |
| 222 | scopes: ["agent_activity_notifications"], |
| 223 | } satisfies RelayEnvironmentLinkProofPayload; |
| 224 | const request = { |
| 225 | proof: signTestJwt(payload, RELAY_LINK_PROOF_TYP, environmentKeyPair.privateKey), |
| 226 | notificationsEnabled: true, |
| 227 | liveActivitiesEnabled: true, |
| 228 | managedTunnelsEnabled: false, |
| 229 | } satisfies RelayEnvironmentLinkRequest; |
| 230 | const linker = yield* EnvironmentLinker.EnvironmentLinker; |
| 231 | const result = yield* linker.link({ userId: "user_123", request }); |
| 232 | expect(result.environmentCredential).toBe("t3env_credential_secret"); |
| 233 | expect(result.endpointRuntime).toBeNull(); |
| 234 | expect(persistedEndpoint).toBe("http://127.0.0.1:3773/"); |
| 235 | // Downgrading from a managed link must release the previously provisioned |
| 236 | // tunnel; nothing else cleans it up before a full unlink. |
| 237 | expect(deprovisionedEnvironmentId).toBe("env-link-test"); |
| 238 | }).pipe( |
| 239 | Effect.provide( |
| 240 | layerTest({ |
| 241 | upsert: (input) => |
| 242 | Effect.sync(() => { |
| 243 | persistedEndpoint = input.endpoint.httpBaseUrl; |
| 244 | }), |
| 245 | deprovision: (input) => |
| 246 | Effect.sync(() => { |
| 247 | deprovisionedEnvironmentId = input.environmentId; |
| 248 | return true; |
| 249 | }), |
| 250 | }), |
| 251 | ), |
| 252 | ); |
| 253 | }); |
| 254 | |
| 255 | it.effect("rejects a tampered compact proof before persistence", () => { |
| 256 | let persisted = false; |
| 257 | return Effect.gen(function* () { |
| 258 | const { request } = yield* makeRequest; |
| 259 | const segments = request.proof.split("."); |
| 260 | const signature = segments[2]!; |
| 261 | segments[2] = `${signature.startsWith("A") ? "B" : "A"}${signature.slice(1)}`; |
| 262 | const tampered = { ...request, proof: segments.join(".") }; |
| 263 | const linker = yield* EnvironmentLinker.EnvironmentLinker; |
| 264 | const result = yield* Effect.result(linker.link({ userId: "user_123", request: tampered })); |
| 265 | expect(Result.isFailure(result)).toBe(true); |
| 266 | if (Result.isFailure(result)) { |
| 267 | expect(isEnvironmentLinkProofInvalid(result.failure)).toBe(true); |
| 268 | if (isEnvironmentLinkProofInvalid(result.failure)) { |
| 269 | expect(result.failure).toMatchObject({ |
| 270 | userId: "user_123", |
| 271 | environmentId: "env-link-test", |
| 272 | reason: "invalid_signature_or_scope", |
| 273 | stage: "verify_proof", |
| 274 | cause: { _tag: "RelayJwtError" }, |
| 275 | }); |
| 276 | } |
| 277 | } |
| 278 | expect(persisted).toBe(false); |
| 279 | }).pipe( |
| 280 | Effect.provide( |
| 281 | layerTest({ |
| 282 | upsert: () => |
| 283 | Effect.sync(() => { |
| 284 | persisted = true; |
| 285 | }), |
| 286 | }), |
| 287 | ), |
| 288 | ); |
| 289 | }); |
| 290 | |
| 291 | it.effect("rejects replayed JWT ids", () => |
| 292 | Effect.gen(function* () { |
| 293 | const { request } = yield* makeRequest; |
| 294 | const linker = yield* EnvironmentLinker.EnvironmentLinker; |
| 295 | const result = yield* Effect.result(linker.link({ userId: "user_123", request })); |
| 296 | expect(Result.isFailure(result)).toBe(true); |
| 297 | if (Result.isFailure(result)) { |
| 298 | expect(isEnvironmentLinkProofInvalid(result.failure)).toBe(true); |
| 299 | if (isEnvironmentLinkProofInvalid(result.failure)) { |
| 300 | expect(result.failure).toMatchObject({ |
| 301 | userId: "user_123", |
| 302 | environmentId: "env-link-test", |
| 303 | reason: "replayed_nonce", |
| 304 | stage: "consume_proof_nonce", |
| 305 | }); |
| 306 | } |
| 307 | } |
| 308 | }).pipe(Effect.provide(layerTest({ consume: () => Effect.succeed(false) }))), |
| 309 | ); |
| 310 | }); |