| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign. |
| 2 | import * as NodeCrypto from "node:crypto"; |
| 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | import type { |
| 5 | RelayAgentActivityPublishProofPayload, |
| 6 | RelayAgentActivityPublishRequest, |
| 7 | RelayAgentActivityState, |
| 8 | } from "@t3tools/contracts/relay"; |
| 9 | import { RELAY_ACTIVITY_PUBLISH_TYP } from "@t3tools/shared/relayJwt"; |
| 10 | import { stableStringify } from "@t3tools/shared/relaySigning"; |
| 11 | import { describe, expect, it } from "@effect/vitest"; |
| 12 | import * as DateTime from "effect/DateTime"; |
| 13 | import * as Effect from "effect/Effect"; |
| 14 | import * as Layer from "effect/Layer"; |
| 15 | import * as Redacted from "effect/Redacted"; |
| 16 | import * as Result from "effect/Result"; |
| 17 | import * as Schema from "effect/Schema"; |
| 18 | |
| 19 | import * as DpopProofs from "../auth/DpopProofs.ts"; |
| 20 | import * as RelayConfiguration from "../Config.ts"; |
| 21 | import * as EnvironmentPublishSignatures from "./EnvironmentPublishSignatures.ts"; |
| 22 | |
| 23 | const keyPair = NodeCrypto.generateKeyPairSync("ed25519", { |
| 24 | privateKeyEncoding: { format: "pem", type: "pkcs8" }, |
| 25 | publicKeyEncoding: { format: "pem", type: "spki" }, |
| 26 | }); |
| 27 | const config = RelayConfiguration.RelayConfiguration.of({ |
| 28 | relayIssuer: "https://relay.example.test", |
| 29 | apns: { |
| 30 | environment: "sandbox", |
| 31 | teamId: "team-id", |
| 32 | keyId: "key-id", |
| 33 | privateKey: Redacted.make("private-key"), |
| 34 | bundleId: "com.t3tools.t3code.dev", |
| 35 | }, |
| 36 | apnsDeliveryJobSigningSecret: Redacted.make("job-secret"), |
| 37 | clerkSecretKey: Redacted.make("clerk-secret"), |
| 38 | clerkPublishableKey: "pk_test_test", |
| 39 | clerkJwtAudience: "t3-code-relay", |
| 40 | cloudMintPrivateKey: Redacted.make(keyPair.privateKey), |
| 41 | cloudMintPublicKey: keyPair.publicKey, |
| 42 | managedEndpointBaseDomain: undefined, |
| 43 | managedEndpointNamespace: undefined, |
| 44 | }); |
| 45 | const state: RelayAgentActivityState = { |
| 46 | environmentId: "env" as RelayAgentActivityState["environmentId"], |
| 47 | threadId: "thread" as RelayAgentActivityState["threadId"], |
| 48 | projectTitle: "Project", |
| 49 | threadTitle: "Thread", |
| 50 | modelTitle: "gpt-5.4", |
| 51 | phase: "running", |
| 52 | headline: "Running", |
| 53 | updatedAt: "2026-05-25T00:00:00.000Z", |
| 54 | deepLink: "/threads/env/thread", |
| 55 | }; |
| 56 | const isEnvironmentPublishSignatureInvalid = Schema.is( |
| 57 | EnvironmentPublishSignatures.EnvironmentPublishSignatureInvalid, |
| 58 | ); |
| 59 | |
| 60 | function signTestJwt(payload: object, privateKey: string): string { |
| 61 | const header = Buffer.from( |
| 62 | JSON.stringify({ alg: "EdDSA", typ: RELAY_ACTIVITY_PUBLISH_TYP }), |
| 63 | ).toString("base64url"); |
| 64 | const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url"); |
| 65 | const signingInput = `${header}.${encodedPayload}`; |
| 66 | return `${signingInput}.${NodeCrypto.sign(null, Buffer.from(signingInput), privateKey).toString("base64url")}`; |
| 67 | } |
| 68 | |
| 69 | const freshRequest = Effect.gen(function* () { |
| 70 | const now = yield* DateTime.now; |
| 71 | const payload = { |
| 72 | iss: "t3-env:env", |
| 73 | aud: "https://relay.example.test", |
| 74 | sub: "env", |
| 75 | jti: "publish-jti", |
| 76 | iat: Math.floor(now.epochMilliseconds / 1_000), |
| 77 | exp: Math.floor(DateTime.add(now, { minutes: 5 }).epochMilliseconds / 1_000), |
| 78 | environmentId: state.environmentId, |
| 79 | threadId: state.threadId, |
| 80 | state, |
| 81 | } satisfies RelayAgentActivityPublishProofPayload; |
| 82 | return { |
| 83 | state, |
| 84 | proof: signTestJwt(payload, keyPair.privateKey), |
| 85 | } satisfies RelayAgentActivityPublishRequest; |
| 86 | }); |
| 87 | |
| 88 | function layer(replay?: Partial<DpopProofs.DpopProofReplay["Service"]>) { |
| 89 | return EnvironmentPublishSignatures.layer.pipe( |
| 90 | Layer.provide( |
| 91 | Layer.merge( |
| 92 | RelayConfiguration.layer(config), |
| 93 | Layer.succeed(DpopProofs.DpopProofReplay, { |
| 94 | verifyAndConsume: |
| 95 | replay?.verifyAndConsume ?? (() => Effect.die("unexpected DPoP proof verification")), |
| 96 | consume: replay?.consume ?? (() => Effect.succeed(true)), |
| 97 | pruneExpired: replay?.pruneExpired ?? Effect.void, |
| 98 | }), |
| 99 | ), |
| 100 | ), |
| 101 | Layer.provideMerge(NodeServices.layer), |
| 102 | ); |
| 103 | } |
| 104 | |
| 105 | describe("EnvironmentPublishSignatures", () => { |
| 106 | it.effect("verifies activity JWTs and scopes replay storage to the environment key", () => { |
| 107 | let replayThumbprint: string | null = null; |
| 108 | return Effect.gen(function* () { |
| 109 | const request = yield* freshRequest; |
| 110 | const signatures = yield* EnvironmentPublishSignatures.EnvironmentPublishSignatures; |
| 111 | yield* signatures.verify({ |
| 112 | environmentId: state.environmentId, |
| 113 | environmentPublicKey: keyPair.publicKey, |
| 114 | threadId: state.threadId, |
| 115 | request, |
| 116 | }); |
| 117 | expect(replayThumbprint).toBe( |
| 118 | `env-publish:${NodeCrypto.createHash("sha256") |
| 119 | .update( |
| 120 | stableStringify({ |
| 121 | environmentId: state.environmentId, |
| 122 | environmentPublicKey: keyPair.publicKey, |
| 123 | }), |
| 124 | ) |
| 125 | .digest("base64url")}`, |
| 126 | ); |
| 127 | }).pipe( |
| 128 | Effect.provide( |
| 129 | layer({ |
| 130 | consume: (input) => |
| 131 | Effect.sync(() => { |
| 132 | replayThumbprint = input.thumbprint; |
| 133 | return true; |
| 134 | }), |
| 135 | }), |
| 136 | ), |
| 137 | ); |
| 138 | }); |
| 139 | |
| 140 | it.effect("rejects top-level state tampering", () => |
| 141 | Effect.gen(function* () { |
| 142 | const request = yield* freshRequest; |
| 143 | const signatures = yield* EnvironmentPublishSignatures.EnvironmentPublishSignatures; |
| 144 | const result = yield* Effect.result( |
| 145 | signatures.verify({ |
| 146 | environmentId: state.environmentId, |
| 147 | environmentPublicKey: keyPair.publicKey, |
| 148 | threadId: state.threadId, |
| 149 | request: { ...request, state: { ...state, headline: "Tampered" } }, |
| 150 | }), |
| 151 | ); |
| 152 | expect(Result.isFailure(result)).toBe(true); |
| 153 | if (Result.isFailure(result)) { |
| 154 | expect(isEnvironmentPublishSignatureInvalid(result.failure)).toBe(true); |
| 155 | if (isEnvironmentPublishSignatureInvalid(result.failure)) { |
| 156 | expect(result.failure).toMatchObject({ |
| 157 | environmentId: state.environmentId, |
| 158 | threadId: state.threadId, |
| 159 | reason: "invalid_signature_or_payload", |
| 160 | stage: "validate_claims", |
| 161 | }); |
| 162 | } |
| 163 | } |
| 164 | }).pipe(Effect.provide(layer())), |
| 165 | ); |
| 166 | |
| 167 | it.effect("preserves the JWT verification failure", () => |
| 168 | Effect.gen(function* () { |
| 169 | const request = yield* freshRequest; |
| 170 | const segments = request.proof.split("."); |
| 171 | const signature = segments[2]!; |
| 172 | segments[2] = `${signature.startsWith("A") ? "B" : "A"}${signature.slice(1)}`; |
| 173 | const signatures = yield* EnvironmentPublishSignatures.EnvironmentPublishSignatures; |
| 174 | const result = yield* Effect.result( |
| 175 | signatures.verify({ |
| 176 | environmentId: state.environmentId, |
| 177 | environmentPublicKey: keyPair.publicKey, |
| 178 | threadId: state.threadId, |
| 179 | request: { ...request, proof: segments.join(".") }, |
| 180 | }), |
| 181 | ); |
| 182 | |
| 183 | expect(Result.isFailure(result)).toBe(true); |
| 184 | if (Result.isFailure(result)) { |
| 185 | expect(isEnvironmentPublishSignatureInvalid(result.failure)).toBe(true); |
| 186 | if (isEnvironmentPublishSignatureInvalid(result.failure)) { |
| 187 | expect(result.failure).toMatchObject({ |
| 188 | environmentId: state.environmentId, |
| 189 | threadId: state.threadId, |
| 190 | reason: "invalid_signature_or_payload", |
| 191 | stage: "verify_proof", |
| 192 | cause: { _tag: "RelayJwtError" }, |
| 193 | }); |
| 194 | } |
| 195 | } |
| 196 | }).pipe(Effect.provide(layer())), |
| 197 | ); |
| 198 | |
| 199 | it.effect("rejects replayed activity JWT ids", () => |
| 200 | Effect.gen(function* () { |
| 201 | const request = yield* freshRequest; |
| 202 | const signatures = yield* EnvironmentPublishSignatures.EnvironmentPublishSignatures; |
| 203 | const result = yield* Effect.result( |
| 204 | signatures.verify({ |
| 205 | environmentId: state.environmentId, |
| 206 | environmentPublicKey: keyPair.publicKey, |
| 207 | threadId: state.threadId, |
| 208 | request, |
| 209 | }), |
| 210 | ); |
| 211 | expect(Result.isFailure(result)).toBe(true); |
| 212 | if (Result.isFailure(result)) { |
| 213 | expect(isEnvironmentPublishSignatureInvalid(result.failure)).toBe(true); |
| 214 | if (isEnvironmentPublishSignatureInvalid(result.failure)) { |
| 215 | expect(result.failure).toMatchObject({ |
| 216 | environmentId: state.environmentId, |
| 217 | threadId: state.threadId, |
| 218 | reason: "replayed_nonce", |
| 219 | stage: "consume_nonce", |
| 220 | }); |
| 221 | } |
| 222 | } |
| 223 | }).pipe(Effect.provide(layer({ consume: () => Effect.succeed(false) }))), |
| 224 | ); |
| 225 | }); |