Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e39e53d
feat: support features claim in graph tokens (#2811)
Noroth May 1, 2026
932f4ec
chore(router, controlplane): add engine config hash mapping to node p…
dkorittki May 4, 2026
787d2d0
feat(router): allow split config polling (#2814)
dkorittki May 5, 2026
6bf713b
feat(router): reuse unchanged feature flag muxes on graph server (#2823)
dkorittki May 7, 2026
44be5ab
fix(router): keep websockets + resolver alive for reused graph muxes …
dkorittki May 7, 2026
1ae7d2e
feat: add support for split config in cdn (#2839)
pepol May 7, 2026
ba63680
fix(router): fix unmarshall error of mapper.json during config fetch …
dkorittki May 8, 2026
c4bd8ba
feat(router): add options to configure router config assembling in sp…
Noroth May 8, 2026
e673a39
feat: split router config (#2834)
wilsonrivera May 8, 2026
6a64b7e
feat: add rpc method to recompose feature flags
wilsonrivera May 8, 2026
21c33bb
chore: linting
wilsonrivera May 8, 2026
3bd0039
chore: add audit log and error counts
wilsonrivera May 8, 2026
cc19d99
chore: fix an issue where the mapper included all hashes
wilsonrivera May 8, 2026
3ff366c
chore: add tests
wilsonrivera May 8, 2026
f5da181
chore: add tests for the generated `mapper.json` file
wilsonrivera May 9, 2026
04c41d1
chore: improve `mapper.json` validation
wilsonrivera May 9, 2026
f0b46f2
chore: fix tests
wilsonrivera May 9, 2026
6f47050
chore: drop migrations
JivusAyrus May 11, 2026
3a9ba26
Merge branch 'main' of github.com:wundergraph/cosmo into feat/split-c…
JivusAyrus May 11, 2026
da8729a
chore: add migrations
JivusAyrus May 11, 2026
8fd87f2
fix: lint
JivusAyrus May 11, 2026
499f6ea
fix: update federated graph handling in CompositionService
JivusAyrus May 11, 2026
86db495
fix: coderabbit review
JivusAyrus May 11, 2026
0bdfe84
fix: ensure updated federated graph is used in composition service
JivusAyrus May 11, 2026
0d6cd28
fix: refactor feature flag and graph compatibility version handling w…
JivusAyrus May 11, 2026
545a7c6
chore: update test
Noroth May 11, 2026
bd5528e
chore: fix potential hot reloading issue when graph server creation f…
Noroth May 11, 2026
a519f52
chore(router): remove unused type
dkorittki May 11, 2026
522968a
Merge branch 'feat/split-configs' into wilson/eng-9541-controlplane-a…
wilsonrivera May 11, 2026
fb72e45
chore: generate platform service
wilsonrivera May 11, 2026
925009e
Merge branch 'wilson/eng-9541-controlplane-add-rpc-method-to-recompos…
wilsonrivera May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions cdn-server/cdn/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ declare module 'hono' {
interface ContextVariableMap {
authenticatedOrganizationId: string;
authenticatedFederatedGraphId: string;
authenticatedFeatures: string[];
}
}

Expand Down Expand Up @@ -102,6 +103,19 @@ const jwtMiddleware = (secret: string | ((c: Context) => string)) => {
c.set('authenticatedFederatedGraphId', federatedGraphId as string);
}

const features = result.payload.features;
c.set('authenticatedFeatures', Array.isArray(features) ? (features as string[]) : []);

await next();
};
};

const requireFeature = (feature: string) => {
return async (c: Context, next: Next) => {
const features = c.get('authenticatedFeatures');
if (!features || !features.includes(feature)) {
return c.text('Forbidden - Missing required feature', 403);
}
await next();
};
};
Expand Down Expand Up @@ -353,6 +367,59 @@ const subgraphChecks = (storage: BlobStorage) => {
};
};

const manifestBlob = (storage: BlobStorage, keyBuilder: (orgId: string, graphId: string, c: Context) => string) => {
return async (c: Context) => {
const organizationId = c.get('authenticatedOrganizationId');
const federatedGraphId = c.get('authenticatedFederatedGraphId');

if (organizationId !== c.req.param('organization_id') || federatedGraphId !== c.req.param('federated_graph_id')) {
return c.text('Bad Request', 400);
}

const key = keyBuilder(organizationId, federatedGraphId, c);

const body = await c.req.json();

let isModified = true;

if (body?.version) {
try {
isModified = await storage.headObject({ context: c, key, version: body.version });
} catch (e: any) {
if (e instanceof BlobNotFoundError) {
return c.notFound();
}
throw e;
}
}

if (!isModified) {
return c.body(null, 304);
}

let blobObject: BlobObject;

try {
blobObject = await storage.getObject({ context: c, key, cacheControl: 'no-cache' });

if (blobObject.metadata && blobObject.metadata['signature-sha256']) {
c.header(signatureSha256Header, blobObject.metadata['signature-sha256']);
}
} catch (e: any) {
if (e instanceof BlobNotFoundError) {
return c.notFound();
}
throw e;
}

c.header('Content-Type', 'application/json; charset=UTF-8');

return stream(c, async (stream) => {
await stream.pipe(blobObject.stream);
});
};
};

// eslint-disable-next-line @typescript-eslint/ban-types
export const cdn = <E extends Env, S extends Schema = {}, BasePath extends string = '/'>(
hono: Hono<E, S, BasePath>,
Expand Down Expand Up @@ -391,4 +458,35 @@ export const cdn = <E extends Env, S extends Schema = {}, BasePath extends strin
hono
.use(subgraphChecksPath, jwtMiddleware(opts.authJwtSecret))
.get(subgraphChecksPath, subgraphChecks(opts.blobStorage));

const manifestMapperPath = '/:organization_id/:federated_graph_id/manifest/mapper.json';
hono
.use(manifestMapperPath, jwtMiddleware(opts.authJwtSecret))
.use(manifestMapperPath, requireFeature('split-config-loading'))
.post(
manifestMapperPath,
manifestBlob(opts.blobStorage, (orgId, graphId) => `${orgId}/${graphId}/manifest/mapper.json`),
);

const manifestLatestPath = '/:organization_id/:federated_graph_id/manifest/latest.json';
hono
.use(manifestLatestPath, jwtMiddleware(opts.authJwtSecret))
.use(manifestLatestPath, requireFeature('split-config-loading'))
.post(
manifestLatestPath,
manifestBlob(opts.blobStorage, (orgId, graphId) => `${orgId}/${graphId}/manifest/latest.json`),
);

const manifestFeatureFlagPath =
'/:organization_id/:federated_graph_id/manifest/feature-flags/:feature_flag_name{.+\\.json$}';
hono
.use(manifestFeatureFlagPath, jwtMiddleware(opts.authJwtSecret))
.use(manifestFeatureFlagPath, requireFeature('split-config-loading'))
.post(
manifestFeatureFlagPath,
manifestBlob(
opts.blobStorage,
(orgId, graphId, c) => `${orgId}/${graphId}/manifest/feature-flags/${c.req.param('feature_flag_name')}`,
),
);
};
249 changes: 245 additions & 4 deletions cdn-server/cdn/test/cdn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@ import { BlobStorage, BlobNotFoundError, cdn, BlobObject, signatureSha256Header
const secretKey = 'hunter2';
const secretAdmissionKey = 'hunter3';

const generateToken = async (organizationId: string, federatedGraphId: string | undefined, secret: string) => {
const generateToken = async (
organizationId: string,
federatedGraphId: string | undefined,
secret: string,
features?: string[],
) => {
const secretKey = new TextEncoder().encode(secret);
return await new SignJWT({ organization_id: organizationId, federated_graph_id: federatedGraphId })
.setProtectedHeader({ alg: 'HS256' })
.sign(secretKey);
const payload: Record<string, unknown> = {
organization_id: organizationId,
federated_graph_id: federatedGraphId,
};
if (features !== undefined) {
payload.features = features;
}
return await new SignJWT(payload).setProtectedHeader({ alg: 'HS256' }).sign(secretKey);
};

class InMemoryBlobStorage implements BlobStorage {
Expand Down Expand Up @@ -844,4 +854,235 @@ describe('CDN handlers', () => {
expect(res.status).toBe(404);
});
});

describe('Test manifest endpoints (split-config-loading)', async () => {
const federatedGraphId = 'federatedGraphId';
const organizationId = 'organizationId';
const tokenWithFeature = await generateToken(organizationId, federatedGraphId, secretKey, ['split-config-loading']);
const tokenNoFeature = await generateToken(organizationId, federatedGraphId, secretKey);
const tokenWrongFeature = await generateToken(organizationId, federatedGraphId, secretKey, ['other-feature']);
const blobStorage = new InMemoryBlobStorage();

const mapperContents = JSON.stringify({ graphConfigs: { '': 'hash-base' } });
const latestContents = JSON.stringify({ version: 'v1', engineConfig: {} });
const featureFlagContents = JSON.stringify({ version: 'v1', engineConfig: { featureFlag: true } });

blobStorage.objects.set(`${organizationId}/${federatedGraphId}/manifest/mapper.json`, {
buffer: Buffer.from(mapperContents),
metadata: { version: 'v1', 'signature-sha256': 'sig-mapper' },
});

blobStorage.objects.set(`${organizationId}/${federatedGraphId}/manifest/latest.json`, {
buffer: Buffer.from(latestContents),
metadata: { version: 'v1' },
});

blobStorage.objects.set(`${organizationId}/${federatedGraphId}/manifest/feature-flags/my-flag.json`, {
buffer: Buffer.from(featureFlagContents),
metadata: { version: 'v1' },
});

const app = new Hono();

cdn(app, {
authJwtSecret: secretKey,
authAdmissionJwtSecret: secretAdmissionKey,
blobStorage,
});

describe('feature gate authorization', () => {
test('returns 403 when features claim is missing from JWT', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/mapper.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenNoFeature}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(403);
});

test('returns 403 when features claim does not include split-config-loading', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/mapper.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWrongFeature}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(403);
});

test('returns 403 for latest.json without feature', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/latest.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenNoFeature}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(403);
});

test('returns 403 for feature-flags without feature', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/feature-flags/my-flag.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenNoFeature}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(403);
});
});

describe('JWT authentication', () => {
test('returns 401 with no token', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/mapper.json`, {
method: 'POST',
});
expect(res.status).toBe(401);
});

test('returns 401 with invalid token', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/mapper.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature.slice(0, -1)}}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(401);
});

test('returns 401 with expired token', async () => {
const expiredToken = await new SignJWT({
organization_id: organizationId,
federated_graph_id: federatedGraphId,
features: ['split-config-loading'],
exp: Math.floor(Date.now() / 1000) - 60,
})
.setProtectedHeader({ alg: 'HS256' })
.sign(new TextEncoder().encode(secretKey));
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/mapper.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${expiredToken}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(401);
});
});

describe('mapper.json', () => {
test('returns mapper blob content with correct headers', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/mapper.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe('application/json; charset=UTF-8');
expect(res.headers.get(signatureSha256Header)).toBe('sig-mapper');
expect(await res.text()).toBe(mapperContents);
});

test('returns 304 when version matches', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/mapper.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature}` },
body: JSON.stringify({ version: 'v1' }),
});
expect(res.status).toBe(304);
});

test('returns 404 when blob does not exist', async () => {
const emptyStorage = new InMemoryBlobStorage();
const otherApp = new Hono();
cdn(otherApp, {
authJwtSecret: secretKey,
authAdmissionJwtSecret: secretAdmissionKey,
blobStorage: emptyStorage,
});

const res = await otherApp.request(`/${organizationId}/${federatedGraphId}/manifest/mapper.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(404);
});

test('returns 400 when org/graph ID mismatch', async () => {
const res = await app.request(`/wrong-org/${federatedGraphId}/manifest/mapper.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(400);
});
});

describe('latest.json', () => {
test('returns latest manifest blob content', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/latest.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe('application/json; charset=UTF-8');
expect(await res.text()).toBe(latestContents);
});

test('returns 304 when version matches', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/latest.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature}` },
body: JSON.stringify({ version: 'v1' }),
});
expect(res.status).toBe(304);
});

test('returns 404 when blob does not exist', async () => {
const emptyStorage = new InMemoryBlobStorage();
const otherApp = new Hono();
cdn(otherApp, {
authJwtSecret: secretKey,
authAdmissionJwtSecret: secretAdmissionKey,
blobStorage: emptyStorage,
});

const res = await otherApp.request(`/${organizationId}/${federatedGraphId}/manifest/latest.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(404);
});
});

describe('feature-flags/:name.json', () => {
test('returns feature flag blob content', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/feature-flags/my-flag.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature}` },
body: JSON.stringify({ version: '' }),
});
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe('application/json; charset=UTF-8');
expect(await res.text()).toBe(featureFlagContents);
});

test('returns 304 when version matches', async () => {
const res = await app.request(`/${organizationId}/${federatedGraphId}/manifest/feature-flags/my-flag.json`, {
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature}` },
body: JSON.stringify({ version: 'v1' }),
});
expect(res.status).toBe(304);
});

test('returns 404 when blob does not exist', async () => {
const res = await app.request(
`/${organizationId}/${federatedGraphId}/manifest/feature-flags/nonexistent.json`,
{
method: 'POST',
headers: { Authorization: `Bearer ${tokenWithFeature}` },
body: JSON.stringify({ version: '' }),
},
);
expect(res.status).toBe(404);
});
});
});
});
Loading
Loading