diff --git a/.changeset/typegen-proofkit-token.md b/.changeset/typegen-proofkit-token.md new file mode 100644 index 00000000..a45dc460 --- /dev/null +++ b/.changeset/typegen-proofkit-token.md @@ -0,0 +1,5 @@ +--- +"@proofkit/typegen": patch +--- + +Support transient ProofKit token passthrough. diff --git a/packages/typegen/src/cli.ts b/packages/typegen/src/cli.ts index 89bddfc5..e8a394ee 100644 --- a/packages/typegen/src/cli.ts +++ b/packages/typegen/src/cli.ts @@ -15,6 +15,7 @@ const oldConfigPaths = ["fmschema.config.mjs", "fmschema.config.js"]; interface ConfigArgs { configLocation: string; resetOverrides?: boolean; + proofkitToken?: string; } function init({ configLocation }: ConfigArgs) { @@ -34,7 +35,7 @@ function init({ configLocation }: ConfigArgs) { } } -async function runCodegen({ configLocation, resetOverrides = false }: ConfigArgs) { +async function runCodegen({ configLocation, resetOverrides = false, proofkitToken }: ConfigArgs) { if (!fs.existsSync(configLocation)) { // but check if they have the old config and just need to upgrade... let hasOldConfig = false; @@ -91,6 +92,7 @@ async function runCodegen({ configLocation, resetOverrides = false }: ConfigArgs resetOverrides, postGenerateCommand: configParsed.data.postGenerateCommand, configPath: configLocation, + proofkitToken, }).catch((err: unknown) => { const friendlyError = getFriendlyTypegenError(err); if (friendlyError) { @@ -117,6 +119,7 @@ program .command("generate", { isDefault: true }) .option("--config ", "optional config file name") .option("--env-path ", "optional path to your .env file") + .option("--proofkit-token ", "transient ProofKit token for FM MCP authorization") .option( "--reset-overrides", "Recreate the overrides file(s), even if they already exist. Most useful when upgrading from @proofgeist/fmdapi", @@ -136,6 +139,7 @@ program await runCodegen({ configLocation, resetOverrides: options.resetOverrides, + proofkitToken: options.proofkitToken, }); }); diff --git a/packages/typegen/src/server/app.ts b/packages/typegen/src/server/app.ts index 8c9d62d7..5bbcafcb 100644 --- a/packages/typegen/src/server/app.ts +++ b/packages/typegen/src/server/app.ts @@ -359,7 +359,9 @@ export function createApiApp(context: ApiContext) { // Flatten the nested layout/folder structure into a flat list with full paths const flatLayouts = flattenLayouts(layouts); - logServer(`GET /layouts config=${configIndex} returned ${flatLayouts.length} layout${flatLayouts.length === 1 ? "" : "s"}`); + logServer( + `GET /layouts config=${configIndex} returned ${flatLayouts.length} layout${flatLayouts.length === 1 ? "" : "s"}`, + ); return c.json({ layouts: flatLayouts }); } catch (err) { diff --git a/packages/typegen/src/typegen.ts b/packages/typegen/src/typegen.ts index b999aded..da2f4ec8 100644 --- a/packages/typegen/src/typegen.ts +++ b/packages/typegen/src/typegen.ts @@ -65,6 +65,7 @@ export const generateTypedClients = async ( resetOverrides?: boolean; cwd?: string; configPath?: string; + proofkitToken?: string; fmMcpClientIdentity?: FmMcpClientIdentity; fmMcpIdleTimeoutSeconds?: number; }, @@ -129,6 +130,7 @@ export const generateTypedClients = async ( cwd, configPath: options?.configPath, configIndex: isConfigArray ? configIndex : undefined, + proofkitToken: options?.proofkitToken, fmMcpClientIdentity: options?.fmMcpClientIdentity, fmMcpIdleTimeoutSeconds: options?.fmMcpIdleTimeoutSeconds, }); @@ -166,6 +168,7 @@ const generateTypedClientsSingle = async ( cwd?: string; configPath?: string; configIndex?: number; + proofkitToken?: string; fmMcpClientIdentity?: FmMcpClientIdentity; fmMcpIdleTimeoutSeconds?: number; }, @@ -243,6 +246,7 @@ const generateTypedClientsSingle = async ( let fmMcpConnectedFileName: string | undefined; let fmMcpPersistentToken: string | undefined; let fmMcpSessionId: string | undefined; + const proofkitToken = options?.proofkitToken ?? process.env.FM_MCP_SESSION_ID; if (validationResult.mode === "fmMcp") { fmMcpBaseUrl = normalizeFmMcpBaseUrl(validationResult.baseUrl); @@ -254,7 +258,12 @@ const generateTypedClientsSingle = async ( const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), connectedFilesTimeoutMs); try { + const headers = new Headers(); + if (proofkitToken) { + headers.set("X-ProofKit-Session", proofkitToken); + } const res = await fetch(`${fmMcpBaseUrl}/connectedFiles`, { + headers, signal: controller.signal, }); clearTimeout(timeout); @@ -345,7 +354,7 @@ const generateTypedClientsSingle = async ( connectedFileName: fmMcpConnectedFileName, clientName: fmMcpObj?.clientName ?? sessionClientIdentity.clientName, }, - fmMcpPersistentToken ?? fmMcpObj?.sessionId, + proofkitToken ?? fmMcpPersistentToken ?? fmMcpObj?.sessionId, ); } else { server = validationResult.server; diff --git a/packages/typegen/tests/typegen.test.ts b/packages/typegen/tests/typegen.test.ts index b77168bc..ea069c8a 100644 --- a/packages/typegen/tests/typegen.test.ts +++ b/packages/typegen/tests/typegen.test.ts @@ -41,6 +41,7 @@ describe("typegen unit tests", () => { originalEnv.FM_PASSWORD = process.env.FM_PASSWORD; originalEnv.FM_HTTP_BASE_URL = process.env.FM_HTTP_BASE_URL; originalEnv.FM_CONNECTED_FILE_NAME = process.env.FM_CONNECTED_FILE_NAME; + originalEnv.FM_MCP_SESSION_ID = process.env.FM_MCP_SESSION_ID; // Set mock env values for tests // Use valid Otto API key format (KEY_ prefix for Otto v3) process.env.OTTO_API_KEY = "KEY_test_api_key_12345"; @@ -713,4 +714,64 @@ describe("typegen unit tests", () => { expect(headers.get("X-ProofKit-Session")).toBe("registered-persistent-token"); expect(fetchMock).toHaveBeenCalledTimes(1); }); + + it("uses FM_MCP_SESSION_ID as proofkit token", async () => { + process.env.FM_MCP_BASE_URL = "http://127.0.0.1:1365"; + process.env.FM_CONNECTED_FILE_NAME = "TestFile"; + process.env.FM_MCP_SESSION_ID = "env-proofkit-token"; + + const fetchMock = vi.fn( + createLayoutMetadataMock({ + FmMcpLayout: mockLayoutMetadata["basic-layout"], + }), + ); + vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch); + + const config: Extract, { type: "fmdapi" }> = { + type: "fmdapi", + layouts: [{ layoutName: "FmMcpLayout", schemaName: "fmMcpEnvTokenSchema" }], + path: "unit-typegen-output/fm-mcp-env-token", + generateClient: true, + validator: false, + fmMcp: { enabled: true }, + }; + + await generateTypedClients(config, { cwd: import.meta.dirname }); + + const headers = fetchMock.mock.calls[0]?.[1]?.headers as Headers; + expect(headers.get("X-ProofKit-Session")).toBe("env-proofkit-token"); + + const clientPath = path.join(__dirname, "unit-typegen-output/fm-mcp-env-token/client/fmMcpEnvTokenSchema.ts"); + const content = await fs.readFile(clientPath, "utf-8"); + expect(content).not.toContain("env-proofkit-token"); + }); + + it("uses proofkit token option before FM_MCP_SESSION_ID", async () => { + process.env.FM_MCP_BASE_URL = "http://127.0.0.1:1365"; + process.env.FM_CONNECTED_FILE_NAME = "TestFile"; + process.env.FM_MCP_SESSION_ID = "env-proofkit-token"; + + const fetchMock = vi.fn( + createLayoutMetadataMock({ + FmMcpLayout: mockLayoutMetadata["basic-layout"], + }), + ); + vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch); + + const config: Extract, { type: "fmdapi" }> = { + type: "fmdapi", + layouts: [{ layoutName: "FmMcpLayout", schemaName: "fmMcpFlagTokenSchema" }], + path: "unit-typegen-output/fm-mcp-flag-token", + validator: false, + fmMcp: { enabled: true }, + }; + + await generateTypedClients(config, { + cwd: import.meta.dirname, + proofkitToken: "flag-proofkit-token", + }); + + const headers = fetchMock.mock.calls[0]?.[1]?.headers as Headers; + expect(headers.get("X-ProofKit-Session")).toBe("flag-proofkit-token"); + }); });