wgc
Version:
The official CLI tool to manage the GraphQL Federation Platform Cosmo
121 lines • 5.47 kB
JavaScript
import { existsSync } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import jwtDecode from 'jwt-decode';
import pc from 'picocolors';
import { dirname, join } from 'pathe';
import { config, getBaseHeaders } from '../../core/config.js';
import { makeSignature, safeCompare } from '../../core/signature.js';
export const featureFlagsDir = 'feature-flags';
export const latestFile = 'latest.json';
export const mapperFile = 'mapper.json';
export const routerConfigFile = 'router-config.json';
const invalidCharacters = /[./]/;
export async function getRouterConfigOutputFile(out) {
let output = out;
/**
* If the provided output doesn't end with `.json`, assume it's a directory and append the filename; otherwise,
* if the directory doesn't exist, we need to create before writing the file
*/
if (output.toLowerCase().endsWith('.json')) {
const dir = dirname(output);
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
}
else {
if (!existsSync(output)) {
await mkdir(output, { recursive: true });
}
output = join(out, routerConfigFile);
}
return output;
}
export const fetchRouterConfig = async ({ client, name, namespace, graphSignKey, }) => {
var _a, _b, _c;
const resp = await client.platform.generateRouterToken({
fedGraphName: name,
namespace,
}, {
headers: getBaseHeaders(),
});
if (((_a = resp.response) === null || _a === void 0 ? void 0 : _a.code) !== EnumStatusCode.OK) {
throw new Error(`${pc.red(`Could not fetch the router config for the graph ${pc.bold(name)}`)} \n${pc.red(pc.bold(((_b = resp.response) === null || _b === void 0 ? void 0 : _b.details) || ''))}`);
}
// Try to decode the generated token
let decoded;
try {
decoded = jwtDecode(resp.token);
}
catch {
throw new Error(pc.red('Could not fetch the router config. Please try again'));
}
const baseUrl = new URL(`/${decoded.organization_id}/${decoded.federated_graph_id}/`, config.cdnURL);
if (!((_c = decoded.features) === null || _c === void 0 ? void 0 : _c.includes('split-config-loading'))) {
// Legacy router config fetching
return {
splitConfigLoading: false,
routerConfig: await fetchFileContentFromCdn(new URL('routerconfigs/latest.json', baseUrl), resp.token, graphSignKey),
};
}
// Retrieve the latest router configuration
const result = {
splitConfigLoading: true,
routerConfig: await fetchFileContentFromCdn(new URL('manifest/latest.json', baseUrl), resp.token, graphSignKey),
};
// Retrieve the `mapper.json` file and convert the content to a `Map<string, string>` for validation
const mapperTextContent = await fetchFileContentFromCdn(new URL('manifest/mapper.json', baseUrl), resp.token);
const mapperRecord = JSON.parse(mapperTextContent);
const mapper = typeof mapperRecord === 'object' && !Array.isArray(mapperRecord)
? new Map(Object.entries(mapperRecord))
: new Map();
result.mapper = Object.fromEntries(mapper);
mapper.delete(''); // Delete the federated graph hash
if (mapper.size === 0) {
return result;
}
// Fetch the latest router configuration for each feature flag
result.featureFlags = new Map();
for (const [featureFlagName] of mapper) {
result.featureFlags.set(featureFlagName, await fetchFileContentFromCdn(new URL(`manifest/feature-flags/${featureFlagName}.json`, baseUrl), resp.token, graphSignKey));
}
return result;
};
export async function writeFeatureFlagConfigToFile(basePath, featureFlagName, featureFlagConfig) {
if (invalidCharacters.test(featureFlagName)) {
throw new Error(`The feature flag name "${featureFlagName}" contains invalid characters.`);
}
await writeFile(join(basePath, `${featureFlagName}.json`), featureFlagConfig);
}
async function fetchFileContentFromCdn(url, token, graphSignKey) {
const headers = new Headers();
headers.append('Content-Type', 'application/json; charset=UTF-8');
headers.append('Authorization', 'Bearer ' + token);
headers.append('Accept-Encoding', 'gzip');
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ Version: '' }),
});
if (!response.ok) {
// The fetch failed the file from CDN
throw new Error(pc.red(`Failed to fetch file "${url}": ${response.status} ${response.statusText}`));
}
const body = await response.text();
if (!graphSignKey) {
// No signature key was provided, we don't need to validate the signature header
return body;
}
// Ensure that we got a signature header and that signing the body using the provided signature key matches
// the header value
const signature = response.headers.get('X-Signature-SHA256');
if (!signature) {
throw new Error(pc.red('You provided a signature key, but the router config does not have a signature header.'));
}
const hash = await makeSignature(body, graphSignKey);
if (!safeCompare(hash, signature)) {
throw new Error(pc.red('The signature of the router config does not match the provided signature key.'));
}
return body;
}
//# sourceMappingURL=utils.js.map