Skip to content
Source
Sign inSign up

EnvironmentPublishSignatures.test.ts

194 lines7.1 KB

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