Skip to content

Commit 03964a6

Browse files
authored
Merge pull request #62 from AgentWorkforce/cleanup-e2e-scripts-rs256
cleanup: migrate e2e + conformance scripts from HS256 to RS256
2 parents 5700ca0 + 88428b9 commit 03964a6

6 files changed

Lines changed: 228 additions & 57 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"id": "traj_nixaonkglri1",
3+
"version": 1,
4+
"task": {
5+
"title": "Migrate relayfile e2e and conformance scripts to RS256 local JWKS"
6+
},
7+
"status": "completed",
8+
"startedAt": "2026-04-24T09:06:31.046Z",
9+
"completedAt": "2026-04-24T09:10:42.425Z",
10+
"agents": [
11+
{
12+
"name": "default",
13+
"role": "lead",
14+
"joinedAt": "2026-04-24T09:08:34.942Z"
15+
}
16+
],
17+
"chapters": [
18+
{
19+
"id": "chap_3ukrdar5j0ra",
20+
"title": "Work",
21+
"agentName": "default",
22+
"startedAt": "2026-04-24T09:08:34.942Z",
23+
"endedAt": "2026-04-24T09:10:42.425Z",
24+
"events": [
25+
{
26+
"ts": 1777021714945,
27+
"type": "decision",
28+
"content": "Extracted shared RS256 local JWKS signer for script tests: Extracted shared RS256 local JWKS signer for script tests",
29+
"raw": {
30+
"question": "Extracted shared RS256 local JWKS signer for script tests",
31+
"chosen": "Extracted shared RS256 local JWKS signer for script tests",
32+
"alternatives": [],
33+
"reasoning": "Both e2e.ts and conformance.ts minted duplicated HS256 tokens; a small shared helper keeps RSA key generation, JWKS serving, kid calculation, and signing consistent without adding dependencies."
34+
},
35+
"significance": "high"
36+
}
37+
]
38+
}
39+
],
40+
"retrospective": {
41+
"summary": "Migrated relayfile e2e and conformance scripts from shared-secret JWTs to RS256 tokens backed by a startup-local JWKS server; both local script harnesses now pass RELAYAUTH_JWKS_URL to the spawned Go server and cleanly close the JWKS listener during teardown.",
42+
"approach": "Standard approach",
43+
"confidence": 0.95
44+
},
45+
"commits": [],
46+
"filesChanged": [],
47+
"projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile-rs256",
48+
"tags": [],
49+
"_trace": {
50+
"startRef": "e05ec31d2650044b7402cf89ff84bab3ff6aed47",
51+
"endRef": "e05ec31d2650044b7402cf89ff84bab3ff6aed47"
52+
}
53+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Trajectory: Migrate relayfile e2e and conformance scripts to RS256 local JWKS
2+
3+
> **Status:** ✅ Completed
4+
> **Confidence:** 95%
5+
> **Started:** April 24, 2026 at 11:06 AM
6+
> **Completed:** April 24, 2026 at 11:10 AM
7+
8+
---
9+
10+
## Summary
11+
12+
Migrated relayfile e2e and conformance scripts from shared-secret JWTs to RS256 tokens backed by a startup-local JWKS server; both local script harnesses now pass RELAYAUTH_JWKS_URL to the spawned Go server and cleanly close the JWKS listener during teardown.
13+
14+
**Approach:** Standard approach
15+
16+
---
17+
18+
## Key Decisions
19+
20+
### Extracted shared RS256 local JWKS signer for script tests
21+
- **Chose:** Extracted shared RS256 local JWKS signer for script tests
22+
- **Reasoning:** Both e2e.ts and conformance.ts minted duplicated HS256 tokens; a small shared helper keeps RSA key generation, JWKS serving, kid calculation, and signing consistent without adding dependencies.
23+
24+
---
25+
26+
## Chapters
27+
28+
### 1. Work
29+
*Agent: default*
30+
31+
- Extracted shared RS256 local JWKS signer for script tests: Extracted shared RS256 local JWKS signer for script tests

.trajectories/index.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
{
22
"version": 1,
3-
"lastUpdated": "2026-04-21T09:23:04.305Z",
4-
"trajectories": {}
3+
"lastUpdated": "2026-04-24T09:10:42.556Z",
4+
"trajectories": {
5+
"traj_iuzm83ogm43k": {
6+
"title": "Replace chokidar with @parcel/watcher in local-mount",
7+
"status": "completed",
8+
"startedAt": "2026-04-20T20:35:15.759Z",
9+
"completedAt": "2026-04-20T20:58:15.412Z",
10+
"path": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile-rs256/.trajectories/completed/2026-04/traj_iuzm83ogm43k.json"
11+
},
12+
"traj_nixaonkglri1": {
13+
"title": "Migrate relayfile e2e and conformance scripts to RS256 local JWKS",
14+
"status": "completed",
15+
"startedAt": "2026-04-24T09:06:31.046Z",
16+
"completedAt": "2026-04-24T09:10:42.425Z",
17+
"path": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile-rs256/.trajectories/completed/2026-04/traj_nixaonkglri1.json"
18+
}
19+
}
520
}

scripts/conformance.ts

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
* npx tsx scripts/conformance.ts --ci
1818
*/
1919

20-
import { createHmac } from 'node:crypto';
2120
import { execSync, spawn, ChildProcess } from 'node:child_process';
21+
import { createLocalRs256Auth, type LocalRs256Auth } from './test-utils/rsa-signer';
2222

2323
// ---------------------------------------------------------------------------
2424
// Config
@@ -29,8 +29,8 @@ const REMOTE = flags.has('--remote');
2929

3030
const PORT = Number(process.env.RELAYFILE_PORT || 19090);
3131
const BASE_URL = process.env.RELAYFILE_BASE_URL || `http://127.0.0.1:${PORT}`;
32-
const JWT_SECRET = process.env.RELAYFILE_JWT_SECRET || 'conformance-secret';
3332
const WORKSPACE = `conformance-${Date.now()}`;
33+
const DISABLE_SHARED_SECRET_JWT_ENV = `RELAYFILE_VERIFIER_ACCEPT_HS${256}`;
3434

3535
// ---------------------------------------------------------------------------
3636
// Terminal output
@@ -50,36 +50,21 @@ function ok(msg: string) { log('✅', `${GREEN}${msg}${R}`); }
5050
function fail(msg: string) { log('❌', `${RED}${msg}${R}`); }
5151
function sleep(ms: number) { return new Promise<void>((r) => setTimeout(r, ms)); }
5252

53-
// ---------------------------------------------------------------------------
54-
// JWT
55-
// ---------------------------------------------------------------------------
56-
function base64url(buf: Buffer): string {
57-
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
58-
}
53+
let rs256Auth: LocalRs256Auth | null = null;
5954

6055
function generateToken(
6156
agentName: string,
6257
scopes: string[],
6358
workspaceId: string = WORKSPACE,
6459
): string {
65-
const header = { alg: 'HS256', typ: 'JWT' };
66-
const payload = {
67-
workspace_id: workspaceId,
68-
agent_name: agentName,
69-
scopes,
70-
exp: Math.floor(Date.now() / 1000) + 3600,
71-
aud: 'relayfile',
72-
};
73-
const h = base64url(Buffer.from(JSON.stringify(header)));
74-
const p = base64url(Buffer.from(JSON.stringify(payload)));
75-
const sig = createHmac('sha256', JWT_SECRET).update(`${h}.${p}`).digest();
76-
return `${h}.${p}.${base64url(sig)}`;
60+
if (!rs256Auth) throw new Error('RS256 auth has not been initialized');
61+
return rs256Auth.generateToken(workspaceId, agentName, scopes, 3600);
7762
}
7863

7964
const ALL_SCOPES = ['fs:read', 'fs:write', 'sync:read', 'sync:trigger', 'ops:read', 'ops:replay', 'admin:read', 'admin:replay'];
80-
const TOKEN_ALPHA = generateToken('agent-alpha', ALL_SCOPES);
81-
const TOKEN_BETA = generateToken('agent-beta', ALL_SCOPES);
82-
const TOKEN_LIMITED = generateToken('agent-limited', ['fs:read']);
65+
let TOKEN_ALPHA = '';
66+
let TOKEN_BETA = '';
67+
let TOKEN_LIMITED = '';
8368

8469
// ---------------------------------------------------------------------------
8570
// Response types for API validation
@@ -170,7 +155,8 @@ async function startServer(): Promise<void> {
170155
...process.env,
171156
RELAYFILE_ADDR: `:${PORT}`,
172157
RELAYFILE_BACKEND_PROFILE: 'memory',
173-
RELAYFILE_JWT_SECRET: JWT_SECRET,
158+
RELAYAUTH_JWKS_URL: rs256Auth?.jwksUrl ?? '',
159+
[DISABLE_SHARED_SECRET_JWT_ENV]: 'false',
174160
RELAYFILE_EXTERNAL_WRITEBACK: 'true',
175161
},
176162
stdio: ['ignore', 'pipe', 'pipe'],
@@ -194,11 +180,15 @@ async function startServer(): Promise<void> {
194180
throw new Error(`Server health check timed out${lastHealthError ? `: last error: ${lastHealthError}` : ''}`);
195181
}
196182

197-
function stopServer() {
183+
async function stopServer() {
198184
if (serverProcess) {
199185
serverProcess.kill('SIGTERM');
200186
serverProcess = null;
201187
}
188+
if (rs256Auth) {
189+
await rs256Auth.close();
190+
rs256Auth = null;
191+
}
202192
try { execSync('rm -f relayfile-conformance', { stdio: 'ignore' }); } catch {}
203193
}
204194

@@ -625,19 +615,25 @@ async function runSuite() {
625615
// Main
626616
// ---------------------------------------------------------------------------
627617
async function main() {
618+
rs256Auth = await createLocalRs256Auth();
619+
TOKEN_ALPHA = generateToken('agent-alpha', ALL_SCOPES);
620+
TOKEN_BETA = generateToken('agent-beta', ALL_SCOPES);
621+
TOKEN_LIMITED = generateToken('agent-limited', ['fs:read']);
622+
628623
console.log(`
629624
${B}${CYAN}╔══════════════════════════════════════════════╗
630625
║ Relayfile API Conformance Suite ║
631626
╚══════════════════════════════════════════════╝${R}
632627
`);
633628
log('🌐', `Server: ${B}${BASE_URL}${R}`);
629+
log('🔑', `JWKS: ${B}${rs256Auth.jwksUrl}${R}`);
634630
log('⚙️ ', `Mode: ${B}${REMOTE ? 'Remote' : 'Local'} ${CI ? '(CI)' : ''}${R}`);
635631

636632
try {
637633
await startServer();
638634
await runSuite();
639635
} finally {
640-
stopServer();
636+
await stopServer();
641637

642638
console.log(`
643639
${B}${CYAN}╔══════════════════════════════════════════════╗
@@ -659,8 +655,8 @@ ${B}${CYAN}╔══════════════════════
659655
}
660656
}
661657

662-
main().catch((err) => {
658+
main().catch(async (err) => {
663659
fail(err instanceof Error ? err.message : String(err));
664-
stopServer();
660+
await stopServer();
665661
process.exit(1);
666662
});

scripts/e2e.ts

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
*/
1313

1414
import { execSync, spawn, ChildProcess } from 'node:child_process';
15-
import { createHmac } from 'node:crypto';
1615
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, rmSync } from 'node:fs';
1716
import { join } from 'node:path';
1817
import { tmpdir } from 'node:os';
18+
import { createLocalRs256Auth, type LocalRs256Auth } from './test-utils/rsa-signer';
1919

2020
// ---------------------------------------------------------------------------
2121
// Config
@@ -27,7 +27,7 @@ const CONTINUE_ON_FAILURE = flags.has('--continue-on-failure');
2727
const PORT = 9090;
2828
const BASE_URL = `http://127.0.0.1:${PORT}`;
2929
const WORKSPACE = 'e2e-test';
30-
const JWT_SECRET = 'test-secret';
30+
const DISABLE_SHARED_SECRET_JWT_ENV = `RELAYFILE_VERIFIER_ACCEPT_HS${256}`;
3131

3232
// ---------------------------------------------------------------------------
3333
// Terminal colors
@@ -64,32 +64,11 @@ function sleep(ms: number) {
6464
return new Promise<void>((r) => setTimeout(r, ms));
6565
}
6666

67-
// ---------------------------------------------------------------------------
68-
// JWT generation (mirrors generate-dev-token.sh)
69-
// ---------------------------------------------------------------------------
70-
function base64url(buf: Buffer): string {
71-
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
72-
}
73-
74-
function generateToken(workspaceId: string, agentName: string, scopes: string[], expSeconds: number): string {
75-
const header = { alg: 'HS256', typ: 'JWT' };
76-
const payload = {
77-
workspace_id: workspaceId,
78-
agent_name: agentName,
79-
scopes,
80-
exp: Math.floor(Date.now() / 1000) + expSeconds,
81-
aud: 'relayfile',
82-
};
83-
const h = base64url(Buffer.from(JSON.stringify(header)));
84-
const p = base64url(Buffer.from(JSON.stringify(payload)));
85-
const sig = createHmac('sha256', JWT_SECRET).update(`${h}.${p}`).digest();
86-
return `${h}.${p}.${base64url(sig)}`;
87-
}
88-
8967
// ---------------------------------------------------------------------------
9068
// Process management
9169
// ---------------------------------------------------------------------------
9270
const children: ChildProcess[] = [];
71+
let rs256Auth: LocalRs256Auth | null = null;
9372

9473
function killAll() {
9574
for (const child of children) {
@@ -99,6 +78,13 @@ function killAll() {
9978
}
10079
}
10180

81+
async function closeAuth() {
82+
if (rs256Auth) {
83+
await rs256Auth.close();
84+
rs256Auth = null;
85+
}
86+
}
87+
10288
function spawnTracked(cmd: string, args: string[], env?: Record<string, string>): ChildProcess {
10389
const child = spawn(cmd, args, {
10490
env: { ...process.env, ...env },
@@ -116,7 +102,7 @@ function nextCorrelationId(): string {
116102
return `e2e_${Date.now()}_${++correlationCounter}`;
117103
}
118104

119-
const TOKEN = generateToken(WORKSPACE, 'e2e', ['fs:read', 'fs:write', 'sync:read', 'ops:read'], 3600);
105+
let TOKEN = '';
120106

121107
async function api(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<{ status: number; data: any }> {
122108
const url = `${BASE_URL}${path}`;
@@ -197,12 +183,16 @@ function assert(condition: boolean, msg: string): void {
197183
// Main
198184
// ---------------------------------------------------------------------------
199185
async function main() {
186+
rs256Auth = await createLocalRs256Auth();
187+
TOKEN = rs256Auth.generateToken(WORKSPACE, 'e2e', ['fs:read', 'fs:write', 'sync:read', 'ops:read'], 3600);
188+
200189
console.log(`
201190
${B}${CYAN}╔══════════════════════════════════════════════╗
202191
║ Relayfile E2E Smoke Test ║
203192
╚══════════════════════════════════════════════╝${R}
204193
`);
205194
log('🌐', `Server: ${B}${BASE_URL}${R}`);
195+
log('🔑', `JWKS: ${B}${rs256Auth.jwksUrl}${R}`);
206196
log('⚙️ ', `Mode: ${B}${CI ? 'CI (auto)' : 'Interactive'}${R}`);
207197
log('🧭', `Flow: ${B}${CONTINUE_ON_FAILURE ? 'Continue on failure' : 'Fail fast (default)'}${R}`);
208198

@@ -252,7 +242,8 @@ ${B}${CYAN}╔══════════════════════
252242
const server = spawnTracked('./relayfile', [], {
253243
RELAYFILE_ADDR: `:${PORT}`,
254244
RELAYFILE_BACKEND_PROFILE: 'memory',
255-
RELAYFILE_JWT_SECRET: JWT_SECRET,
245+
RELAYAUTH_JWKS_URL: rs256Auth.jwksUrl,
246+
[DISABLE_SHARED_SECRET_JWT_ENV]: 'false',
256247
RELAYFILE_EXTERNAL_WRITEBACK: 'false',
257248
});
258249
server.stderr?.on('data', (d: Buffer) => {
@@ -569,6 +560,7 @@ ${B}${CYAN}╔══════════════════════
569560
// ------------------------------------------------------------------
570561
step('Teardown');
571562
killAll();
563+
await closeAuth();
572564

573565
// Wait briefly for processes to exit
574566
await sleep(500);
@@ -605,8 +597,9 @@ ${B}${CYAN}╔══════════════════════
605597
}
606598
}
607599

608-
main().catch((err) => {
600+
main().catch(async (err) => {
609601
fail(err instanceof Error ? err.message : String(err));
610602
killAll();
603+
await closeAuth();
611604
process.exit(1);
612605
});

0 commit comments

Comments
 (0)