dynamic-openapi-cli
Advanced tools
+34
-391
@@ -5,378 +5,21 @@ #!/usr/bin/env node | ||
| import { pathToFileURL } from "url"; | ||
| import { | ||
| createOAuth2AuthCodeAuth as createOAuth2AuthCodeAuth2, | ||
| detectOAuth2AuthCode as detectOAuth2AuthCode2 | ||
| } from "dynamic-openapi-tools/auth"; | ||
| import { loadSpec, resolveSpec } from "dynamic-openapi-tools/parser"; | ||
| // src/auth/oauth2-auth-code.ts | ||
| import { createHash as createHash2 } from "crypto"; | ||
| // src/auth/browser.ts | ||
| import { spawn } from "child_process"; | ||
| async function openBrowser(url) { | ||
| const opener = pickOpener(); | ||
| if (!opener) return false; | ||
| return new Promise((resolve) => { | ||
| try { | ||
| const child = spawn(opener.command, [...opener.args, url], { | ||
| detached: true, | ||
| stdio: "ignore" | ||
| }); | ||
| child.on("error", () => resolve(false)); | ||
| child.unref(); | ||
| setTimeout(() => resolve(true), 50); | ||
| } catch { | ||
| resolve(false); | ||
| } | ||
| }); | ||
| } | ||
| function pickOpener() { | ||
| if (process.env["BROWSER"]) { | ||
| return { command: process.env["BROWSER"], args: [] }; | ||
| } | ||
| switch (process.platform) { | ||
| case "darwin": | ||
| return { command: "open", args: [] }; | ||
| case "win32": | ||
| return { command: "cmd", args: ["/c", "start", '""'] }; | ||
| default: | ||
| return { command: "xdg-open", args: [] }; | ||
| } | ||
| } | ||
| // src/auth/loopback-server.ts | ||
| import { createServer } from "http"; | ||
| function captureCallback(options) { | ||
| const host = options.host ?? "127.0.0.1"; | ||
| const callbackPath = options.path ?? "/callback"; | ||
| const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3; | ||
| const successBody = options.successBody ?? "<html><body><h1>Login complete</h1><p>You can close this tab and return to your terminal.</p></body></html>"; | ||
| const errorBody = options.errorBody ?? "<html><body><h1>Login failed</h1><p>See your terminal for details.</p></body></html>"; | ||
| return new Promise((resolve, reject) => { | ||
| const server = createServer((req, res) => { | ||
| const url = new URL(req.url ?? "/", `http://${host}:${options.port}`); | ||
| if (url.pathname !== callbackPath) { | ||
| res.writeHead(404, { "Content-Type": "text/plain" }); | ||
| res.end("Not Found"); | ||
| return; | ||
| } | ||
| const params = url.searchParams; | ||
| const code = params.get("code") ?? void 0; | ||
| const state = params.get("state") ?? void 0; | ||
| const error = params.get("error") ?? void 0; | ||
| const errorDescription = params.get("error_description") ?? void 0; | ||
| if (error) { | ||
| res.writeHead(400, { | ||
| "Content-Type": "text/html; charset=utf-8", | ||
| Connection: "close" | ||
| }); | ||
| res.end(errorBody); | ||
| } else { | ||
| res.writeHead(200, { | ||
| "Content-Type": "text/html; charset=utf-8", | ||
| Connection: "close" | ||
| }); | ||
| res.end(successBody); | ||
| } | ||
| clearTimeout(timeout); | ||
| const result = {}; | ||
| if (code !== void 0) result.code = code; | ||
| if (state !== void 0) result.state = state; | ||
| if (error !== void 0) result.error = error; | ||
| if (errorDescription !== void 0) result.errorDescription = errorDescription; | ||
| server.closeAllConnections?.(); | ||
| server.close(() => resolve(result)); | ||
| }); | ||
| const timeout = setTimeout(() => { | ||
| server.close(() => reject(new Error(`OAuth2 callback timed out after ${timeoutMs}ms`))); | ||
| }, timeoutMs); | ||
| server.on("error", (err) => { | ||
| clearTimeout(timeout); | ||
| reject(err); | ||
| }); | ||
| server.listen(options.port, host); | ||
| }); | ||
| } | ||
| // src/auth/pkce.ts | ||
| import { createHash, randomBytes } from "crypto"; | ||
| function generatePkce() { | ||
| const verifier = base64UrlEncode(randomBytes(32)); | ||
| const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest()); | ||
| return { verifier, challenge, method: "S256" }; | ||
| } | ||
| function generateState() { | ||
| return base64UrlEncode(randomBytes(16)); | ||
| } | ||
| function base64UrlEncode(bytes) { | ||
| return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); | ||
| } | ||
| // src/auth/token-cache.ts | ||
| import { mkdir, readFile, rm, writeFile } from "fs/promises"; | ||
| import { homedir } from "os"; | ||
| import path from "path"; | ||
| function tokenCacheDir() { | ||
| const xdg = process.env["XDG_DATA_HOME"]; | ||
| if (xdg && xdg.length > 0) { | ||
| return path.join(xdg, "dynamic-openapi-cli", "tokens"); | ||
| } | ||
| return path.join(homedir(), ".local", "share", "dynamic-openapi-cli", "tokens"); | ||
| } | ||
| function tokenCachePath(key) { | ||
| return path.join(tokenCacheDir(), `${sanitizeKey(key)}.json`); | ||
| } | ||
| async function readTokenCache(key) { | ||
| try { | ||
| const raw = await readFile(tokenCachePath(key), "utf-8"); | ||
| const parsed = JSON.parse(raw); | ||
| if (!parsed || typeof parsed !== "object") return null; | ||
| if (typeof parsed.access_token !== "string") return null; | ||
| if (typeof parsed.expires_at !== "number") return null; | ||
| if (typeof parsed.token_type !== "string") return null; | ||
| if (!Array.isArray(parsed.scopes)) return null; | ||
| return parsed; | ||
| } catch (error) { | ||
| if (error.code === "ENOENT") return null; | ||
| return null; | ||
| } | ||
| } | ||
| async function writeTokenCache(key, token) { | ||
| const dir = tokenCacheDir(); | ||
| await mkdir(dir, { recursive: true, mode: 448 }); | ||
| const file = tokenCachePath(key); | ||
| await writeFile(file, JSON.stringify(token, null, 2), { mode: 384 }); | ||
| } | ||
| async function deleteTokenCache(key) { | ||
| try { | ||
| await rm(tokenCachePath(key), { force: true }); | ||
| } catch { | ||
| } | ||
| } | ||
| function sanitizeKey(key) { | ||
| return key.replace(/[^A-Za-z0-9._-]+/g, "-"); | ||
| } | ||
| // src/auth/oauth2-auth-code.ts | ||
| var DEFAULT_REDIRECT_PORT = 7999; | ||
| var DEFAULT_REFRESH_BUFFER_SECONDS = 30; | ||
| var OAuth2AuthCodeFlow = class { | ||
| config; | ||
| cacheKey; | ||
| cached; | ||
| pendingTokenRequest; | ||
| constructor(config) { | ||
| this.config = config; | ||
| this.cacheKey = deriveCacheKey(config); | ||
| } | ||
| async apply(_url, init) { | ||
| const token = await this.getAccessToken(); | ||
| const headers = new Headers(init.headers); | ||
| headers.set("Authorization", `Bearer ${token}`); | ||
| return { ...init, headers }; | ||
| } | ||
| async refresh(_url, init) { | ||
| this.cached = void 0; | ||
| await deleteTokenCache(this.cacheKey); | ||
| const token = await this.getAccessToken(); | ||
| const headers = new Headers(init.headers); | ||
| headers.set("Authorization", `Bearer ${token}`); | ||
| return { ...init, headers }; | ||
| } | ||
| /** Force a fresh login, bypassing the cache. Used by the `login` subcommand. */ | ||
| async forceLogin() { | ||
| this.cached = void 0; | ||
| await deleteTokenCache(this.cacheKey); | ||
| const token = await this.runLoginFlow(); | ||
| return token; | ||
| } | ||
| /** Wipe the cached token. Used by the `logout` subcommand. */ | ||
| async logout() { | ||
| this.cached = void 0; | ||
| await deleteTokenCache(this.cacheKey); | ||
| } | ||
| async getAccessToken() { | ||
| if (this.pendingTokenRequest) return this.pendingTokenRequest; | ||
| this.pendingTokenRequest = this.resolveToken().finally(() => { | ||
| this.pendingTokenRequest = void 0; | ||
| }); | ||
| return this.pendingTokenRequest; | ||
| } | ||
| async resolveToken() { | ||
| if (!this.cached) this.cached = await readTokenCache(this.cacheKey) ?? void 0; | ||
| const buffer = (this.config.refreshBufferSeconds ?? DEFAULT_REFRESH_BUFFER_SECONDS) * 1e3; | ||
| const now = Date.now(); | ||
| if (this.cached && this.cached.expires_at - buffer > now) { | ||
| return this.cached.access_token; | ||
| } | ||
| if (this.cached?.refresh_token) { | ||
| try { | ||
| const refreshed = await this.runRefreshFlow(this.cached.refresh_token); | ||
| return refreshed.access_token; | ||
| } catch (error) { | ||
| process.stderr.write( | ||
| `oauth2 ${this.config.schemeName}: refresh failed (${describeError(error)}), falling back to interactive login | ||
| ` | ||
| ); | ||
| } | ||
| } | ||
| const token = await this.runLoginFlow(); | ||
| return token.access_token; | ||
| } | ||
| async runLoginFlow() { | ||
| const port = this.config.redirectPort ?? DEFAULT_REDIRECT_PORT; | ||
| const redirectUri = this.config.redirectUri ?? `http://127.0.0.1:${port}/callback`; | ||
| const pkce = generatePkce(); | ||
| const state = generateState(); | ||
| const authUrl = buildAuthorizationUrl(this.config, redirectUri, pkce.challenge, state); | ||
| process.stderr.write(`oauth2 ${this.config.schemeName}: opening browser for login | ||
| `); | ||
| process.stderr.write(` If your browser does not open, visit: | ||
| ${authUrl} | ||
| `); | ||
| const [, callback] = await Promise.all([ | ||
| openBrowser(authUrl), | ||
| captureCallback({ port, host: "127.0.0.1", path: "/callback" }) | ||
| ]); | ||
| if (callback.error) { | ||
| throw new Error( | ||
| `OAuth2 login rejected: ${callback.error}${callback.errorDescription ? ` \u2014 ${callback.errorDescription}` : ""}` | ||
| ); | ||
| } | ||
| if (!callback.code) { | ||
| throw new Error("OAuth2 login did not return an authorization code"); | ||
| } | ||
| if (!callback.state || callback.state !== state) { | ||
| throw new Error("OAuth2 login state mismatch \u2014 possible CSRF, aborting"); | ||
| } | ||
| const tokens = await this.exchangeCode(callback.code, redirectUri, pkce.verifier); | ||
| const cached = toCachedToken(tokens, this.config.scopes); | ||
| this.cached = cached; | ||
| await writeTokenCache(this.cacheKey, cached); | ||
| return cached; | ||
| } | ||
| async runRefreshFlow(refreshToken) { | ||
| const body = new URLSearchParams(); | ||
| body.set("grant_type", "refresh_token"); | ||
| body.set("refresh_token", refreshToken); | ||
| body.set("client_id", this.config.clientId); | ||
| if (this.config.clientSecret) body.set("client_secret", this.config.clientSecret); | ||
| const tokens = await this.postTokenEndpoint(body); | ||
| if (!tokens.refresh_token) tokens.refresh_token = refreshToken; | ||
| const cached = toCachedToken(tokens, this.config.scopes); | ||
| this.cached = cached; | ||
| await writeTokenCache(this.cacheKey, cached); | ||
| return cached; | ||
| } | ||
| async exchangeCode(code, redirectUri, verifier) { | ||
| const body = new URLSearchParams(); | ||
| body.set("grant_type", "authorization_code"); | ||
| body.set("code", code); | ||
| body.set("redirect_uri", redirectUri); | ||
| body.set("client_id", this.config.clientId); | ||
| body.set("code_verifier", verifier); | ||
| if (this.config.clientSecret) body.set("client_secret", this.config.clientSecret); | ||
| return this.postTokenEndpoint(body); | ||
| } | ||
| async postTokenEndpoint(body) { | ||
| const response = await fetch(this.config.tokenUrl, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/x-www-form-urlencoded", | ||
| Accept: "application/json" | ||
| }, | ||
| body | ||
| }); | ||
| if (!response.ok) { | ||
| const text = await response.text().catch(() => ""); | ||
| throw new Error(`OAuth2 token endpoint returned ${response.status}${text ? `: ${text}` : ""}`); | ||
| } | ||
| const json = await response.json(); | ||
| if (!json || typeof json.access_token !== "string") { | ||
| throw new Error("OAuth2 token endpoint did not return an access_token"); | ||
| } | ||
| return json; | ||
| } | ||
| }; | ||
| function buildAuthorizationUrl(config, redirectUri, codeChallenge, state) { | ||
| const url = new URL(config.authorizationUrl); | ||
| url.searchParams.set("response_type", "code"); | ||
| url.searchParams.set("client_id", config.clientId); | ||
| url.searchParams.set("redirect_uri", redirectUri); | ||
| url.searchParams.set("state", state); | ||
| url.searchParams.set("code_challenge", codeChallenge); | ||
| url.searchParams.set("code_challenge_method", "S256"); | ||
| if (config.scopes.length > 0) url.searchParams.set("scope", config.scopes.join(" ")); | ||
| if (config.extraAuthParams) { | ||
| for (const [k, v] of Object.entries(config.extraAuthParams)) { | ||
| url.searchParams.set(k, v); | ||
| } | ||
| } | ||
| return url.toString(); | ||
| } | ||
| function toCachedToken(response, fallbackScopes) { | ||
| const expiresIn = typeof response.expires_in === "number" ? response.expires_in : 3600; | ||
| const scopes = typeof response.scope === "string" && response.scope.length > 0 ? response.scope.split(/\s+/) : fallbackScopes; | ||
| const token = { | ||
| access_token: response.access_token, | ||
| token_type: response.token_type ?? "Bearer", | ||
| expires_at: Date.now() + expiresIn * 1e3, | ||
| scopes | ||
| }; | ||
| if (response.refresh_token) token.refresh_token = response.refresh_token; | ||
| return token; | ||
| } | ||
| function deriveCacheKey(config) { | ||
| const hash = createHash2("sha256").update(config.clientId).update("|").update(config.tokenUrl).update("|").update([...config.scopes].sort().join(" ")).digest("hex").slice(0, 16); | ||
| return `${config.schemeName}-${hash}`; | ||
| } | ||
| function describeError(error) { | ||
| if (error instanceof Error) return error.message; | ||
| return String(error); | ||
| } | ||
| // src/auth/resolve.ts | ||
| function detectOAuth2AuthCode(securitySchemes, env = process.env) { | ||
| for (const [name, scheme] of Object.entries(securitySchemes)) { | ||
| if (!scheme || scheme.type !== "oauth2") continue; | ||
| const flow = scheme.flows?.authorizationCode; | ||
| if (!flow) continue; | ||
| const schemeEnv = envKeyFor(name); | ||
| const clientId = env[`OPENAPI_AUTH_${schemeEnv}_CLIENT_ID`] ?? env["OPENAPI_OAUTH2_CLIENT_ID"]; | ||
| if (!clientId) continue; | ||
| const clientSecret = env[`OPENAPI_AUTH_${schemeEnv}_CLIENT_SECRET`] ?? env["OPENAPI_OAUTH2_CLIENT_SECRET"]; | ||
| const scopeOverride = env[`OPENAPI_AUTH_${schemeEnv}_SCOPES`] ?? env["OPENAPI_OAUTH2_SCOPES"]; | ||
| const portEnv = env[`OPENAPI_AUTH_${schemeEnv}_PORT`] ?? env["OPENAPI_OAUTH2_PORT"]; | ||
| const redirectUri = env[`OPENAPI_AUTH_${schemeEnv}_REDIRECT_URI`] ?? env["OPENAPI_OAUTH2_REDIRECT_URI"]; | ||
| const scopes = scopeOverride ? scopeOverride.split(/[\s,]+/).filter(Boolean) : Object.keys(flow.scopes ?? {}); | ||
| const config = { | ||
| schemeName: name, | ||
| clientId, | ||
| authorizationUrl: flow.authorizationUrl, | ||
| tokenUrl: flow.tokenUrl, | ||
| scopes | ||
| }; | ||
| if (clientSecret) config.clientSecret = clientSecret; | ||
| if (redirectUri) config.redirectUri = redirectUri; | ||
| if (portEnv) { | ||
| const port = Number.parseInt(portEnv, 10); | ||
| if (!Number.isNaN(port) && port > 0) config.redirectPort = port; | ||
| } | ||
| return { schemeName: name, config }; | ||
| } | ||
| return null; | ||
| } | ||
| function createOAuth2AuthCodeAuth(config) { | ||
| return new OAuth2AuthCodeFlow(config); | ||
| } | ||
| function envKeyFor(schemeName) { | ||
| return schemeName.toUpperCase().replace(/[^A-Z0-9]+/g, "_"); | ||
| } | ||
| // src/cli/app.ts | ||
| import { readFile as readFile3 } from "fs/promises"; | ||
| import { readFile as readFile2 } from "fs/promises"; | ||
| import { createCLI, formatErrors } from "dynamic-openapi-tools/cli"; | ||
| import { resolveAuth } from "dynamic-openapi-tools/auth"; | ||
| import { | ||
| resolveAuth, | ||
| createOAuth2AuthCodeAuth, | ||
| detectOAuth2AuthCode | ||
| } from "dynamic-openapi-tools/auth"; | ||
| import { filterOperations } from "dynamic-openapi-tools/parser"; | ||
| // src/http/client.ts | ||
| import { readFile as readFile2 } from "fs/promises"; | ||
| import path2 from "path"; | ||
| import { readFile } from "fs/promises"; | ||
| import path from "path"; | ||
| import { fetchWithRetry } from "dynamic-openapi-tools/utils"; | ||
@@ -666,5 +309,5 @@ var RequestError = class extends Error { | ||
| if (fileRef) { | ||
| const fileContents = await readFile2(fileRef.path); | ||
| const fileContents = await readFile(fileRef.path); | ||
| const bytes = Buffer.from(fileContents); | ||
| const filename = path2.basename(fileRef.path); | ||
| const filename = path.basename(fileRef.path); | ||
| const blob = new Blob([bytes], { type: "application/octet-stream" }); | ||
@@ -734,3 +377,3 @@ form.append(key, blob, filename); | ||
| if (fileRef) { | ||
| const bytes = await readFile2(fileRef.path); | ||
| const bytes = await readFile(fileRef.path); | ||
| return { | ||
@@ -742,3 +385,3 @@ body: bytes, | ||
| filePath: fileRef.path, | ||
| filename: path2.basename(fileRef.path), | ||
| filename: path.basename(fileRef.path), | ||
| bytes: bytes.byteLength | ||
@@ -1029,3 +672,3 @@ } | ||
| // src/cli/output.ts | ||
| import { writeFile as writeFile2 } from "fs/promises"; | ||
| import { writeFile } from "fs/promises"; | ||
| var INLINE_BINARY_LIMIT = 256 * 1024; | ||
@@ -1046,3 +689,3 @@ async function renderResponse(response, options = {}) { | ||
| const bytes2 = new Uint8Array(await response.arrayBuffer()); | ||
| await writeFile2(options.outputFile, bytes2); | ||
| await writeFile(options.outputFile, bytes2); | ||
| process.stderr.write(`wrote ${bytes2.byteLength} bytes to ${options.outputFile} | ||
@@ -1137,3 +780,3 @@ `); | ||
| const baseUrl = resolveBaseUrl(spec, options.baseUrl, options.serverIndex); | ||
| const auth = resolveAuthWithOAuth2(spec, options.authConfig); | ||
| const auth = resolveAuthWithOAuth2(spec, options.authConfig, options.name); | ||
| const httpConfig = { | ||
@@ -1244,3 +887,3 @@ baseUrl, | ||
| if (bodyFile) { | ||
| const text = await readFile3(bodyFile, "utf-8"); | ||
| const text = await readFile2(bodyFile, "utf-8"); | ||
| merged["body"] = tryParseJson(text); | ||
@@ -1256,6 +899,6 @@ } else if (bodyRaw === "-") { | ||
| } | ||
| function resolveAuthWithOAuth2(spec, authConfig) { | ||
| function resolveAuthWithOAuth2(spec, authConfig, appName) { | ||
| const resolved = resolveAuth(authConfig, spec.securitySchemes); | ||
| if (resolved) return resolved; | ||
| const detected = detectOAuth2AuthCode(spec.securitySchemes); | ||
| const detected = detectOAuth2AuthCode(spec.securitySchemes, { appName }); | ||
| if (detected) return createOAuth2AuthCodeAuth(detected.config); | ||
@@ -1454,3 +1097,3 @@ return null; | ||
| // src/cli/bundle.ts | ||
| import path3 from "path"; | ||
| import path2 from "path"; | ||
| import { createParser } from "dynamic-openapi-tools/cli"; | ||
@@ -1531,3 +1174,3 @@ import { buildBundle as toolsBuildBundle } from "dynamic-openapi-tools/bundle"; | ||
| name: String(result.options["name"]), | ||
| out: path3.resolve(String(result.options["out"])), | ||
| out: path2.resolve(String(result.options["out"])), | ||
| appVersion: pickString2(result.options["app-version"]), | ||
@@ -1635,7 +1278,7 @@ description: pickString2(result.options["description"]) | ||
| if (bootstrap.rest[0] === "login") { | ||
| await runLogin(spec.securitySchemes); | ||
| await runLogin(spec.securitySchemes, bootstrap.name); | ||
| return; | ||
| } | ||
| if (bootstrap.rest[0] === "logout") { | ||
| await runLogout(spec.securitySchemes); | ||
| await runLogout(spec.securitySchemes, bootstrap.name); | ||
| return; | ||
@@ -1666,4 +1309,4 @@ } | ||
| } | ||
| async function runLogin(securitySchemes) { | ||
| const detected = detectOAuth2AuthCode(securitySchemes); | ||
| async function runLogin(securitySchemes, appName) { | ||
| const detected = detectOAuth2AuthCode2(securitySchemes, { appName }); | ||
| if (!detected) { | ||
@@ -1675,11 +1318,11 @@ process.stderr.write( | ||
| } | ||
| const auth = createOAuth2AuthCodeAuth(detected.config); | ||
| const auth = createOAuth2AuthCodeAuth2(detected.config); | ||
| const token = await auth.forceLogin(); | ||
| process.stderr.write( | ||
| `login: cached token for scheme "${detected.schemeName}" (expires at ${new Date(token.expires_at).toISOString()}) | ||
| `login: cached token for scheme "${detected.schemeName}" in "${detected.config.appName}.env" (expires at ${new Date(token.expires_at).toISOString()}) | ||
| ` | ||
| ); | ||
| } | ||
| async function runLogout(securitySchemes) { | ||
| const detected = detectOAuth2AuthCode(securitySchemes); | ||
| async function runLogout(securitySchemes, appName) { | ||
| const detected = detectOAuth2AuthCode2(securitySchemes, { appName }); | ||
| if (!detected) { | ||
@@ -1689,5 +1332,5 @@ process.stderr.write("logout: no OAuth2 authorization-code flow is configured; nothing to remove.\n"); | ||
| } | ||
| const auth = createOAuth2AuthCodeAuth(detected.config); | ||
| const auth = createOAuth2AuthCodeAuth2(detected.config); | ||
| await auth.logout(); | ||
| process.stderr.write(`logout: removed cached token for scheme "${detected.schemeName}" | ||
| process.stderr.write(`logout: removed cached token for scheme "${detected.schemeName}" from "${detected.config.appName}.env" | ||
| `); | ||
@@ -1694,0 +1337,0 @@ } |
+3
-69
@@ -1,5 +0,5 @@ | ||
| import { OpenAPIV3, ParsedOperation, ParsedSpec, ParsedServer, OperationFilters } from 'dynamic-openapi-tools/parser'; | ||
| import { ParsedOperation, ParsedSpec, ParsedServer, OperationFilters } from 'dynamic-openapi-tools/parser'; | ||
| export { ExternalDocs, OperationFilter, OperationFilters, ParsedOperation, ParsedParameter, ParsedRequestBody, ParsedResponse, ParsedServer, ParsedServerVariable, ParsedSpec, ParsedTag, filterOperations, loadSpec, resolveSource, resolveSpec } from 'dynamic-openapi-tools/parser'; | ||
| import { ResolvedAuth, AuthConfig } from 'dynamic-openapi-tools/auth'; | ||
| export { AuthConfig, ResolvedAuth, TokenExchangeApplyConfig, TokenExchangeAuthConfig, TokenExchangeRequestConfig, TokenExchangeResponseConfig, resolveAuth } from 'dynamic-openapi-tools/auth'; | ||
| export { AuthConfig, OAuth2AuthCodeConfig, OAuth2AuthCodeFlow, ResolvedAuth, TokenExchangeApplyConfig, TokenExchangeAuthConfig, TokenExchangeRequestConfig, TokenExchangeResponseConfig, createOAuth2AuthCodeAuth, detectOAuth2AuthCode, resolveAuth } from 'dynamic-openapi-tools/auth'; | ||
| import { FetchWithRetryOptions } from 'dynamic-openapi-tools/utils'; | ||
@@ -10,68 +10,2 @@ export { FetchWithRetryOptions, RetryPolicy, fetchWithRetry } from 'dynamic-openapi-tools/utils'; | ||
| interface CachedToken { | ||
| access_token: string; | ||
| refresh_token?: string; | ||
| token_type: string; | ||
| expires_at: number; | ||
| scopes: string[]; | ||
| } | ||
| interface OAuth2AuthCodeConfig { | ||
| /** Scheme name from the spec (used for user-facing messages and cache key). */ | ||
| schemeName: string; | ||
| clientId: string; | ||
| /** Optional for public clients (when the authorization server accepts PKCE alone). */ | ||
| clientSecret?: string; | ||
| authorizationUrl: string; | ||
| tokenUrl: string; | ||
| scopes: string[]; | ||
| /** Loopback port for the redirect listener. Defaults to 7999. */ | ||
| redirectPort?: number; | ||
| /** Explicit redirect URI override (some providers require pre-registration). */ | ||
| redirectUri?: string; | ||
| /** Extra query params added to the authorization URL (audience, prompt, …). */ | ||
| extraAuthParams?: Record<string, string>; | ||
| /** Seconds to subtract from `expires_in` when deciding if a token is still fresh. */ | ||
| refreshBufferSeconds?: number; | ||
| } | ||
| /** | ||
| * OAuth2 authorization-code flow with PKCE. Caches tokens on disk under | ||
| * $XDG_DATA_HOME/dynamic-openapi-cli/tokens/, refreshes them transparently, | ||
| * and triggers a browser login on first use or when refresh fails. | ||
| */ | ||
| declare class OAuth2AuthCodeFlow implements ResolvedAuth { | ||
| private config; | ||
| private cacheKey; | ||
| private cached?; | ||
| private pendingTokenRequest?; | ||
| constructor(config: OAuth2AuthCodeConfig); | ||
| apply(_url: URL, init: RequestInit): Promise<RequestInit>; | ||
| refresh(_url: URL, init: RequestInit): Promise<RequestInit>; | ||
| /** Force a fresh login, bypassing the cache. Used by the `login` subcommand. */ | ||
| forceLogin(): Promise<CachedToken>; | ||
| /** Wipe the cached token. Used by the `logout` subcommand. */ | ||
| logout(): Promise<void>; | ||
| private getAccessToken; | ||
| private resolveToken; | ||
| private runLoginFlow; | ||
| private runRefreshFlow; | ||
| private exchangeCode; | ||
| private postTokenEndpoint; | ||
| } | ||
| interface DetectedOAuth2AuthCode { | ||
| schemeName: string; | ||
| config: OAuth2AuthCodeConfig; | ||
| } | ||
| /** | ||
| * Scan `securitySchemes` for an OAuth2 scheme whose `authorizationCode` flow | ||
| * is configured and for which a client id is available in the environment. | ||
| * Returns the first match (specs almost never declare more than one). | ||
| */ | ||
| declare function detectOAuth2AuthCode(securitySchemes: Record<string, OpenAPIV3.SecuritySchemeObject>, env?: NodeJS.ProcessEnv): DetectedOAuth2AuthCode | null; | ||
| declare function createOAuth2AuthCodeAuth(config: OAuth2AuthCodeConfig): ResolvedAuth & { | ||
| forceLogin(): Promise<unknown>; | ||
| logout(): Promise<void>; | ||
| }; | ||
| interface HttpClientConfig { | ||
@@ -158,2 +92,2 @@ baseUrl: string; | ||
| export { type BuildCliOptions, type ExecutedRequest, type HttpClientConfig, type OAuth2AuthCodeConfig, OAuth2AuthCodeFlow, RequestError, ValidationError, buildBundle, buildCli, buildCommandsFromSpec, createOAuth2AuthCodeAuth, detectOAuth2AuthCode, executeOperation, resolveBaseUrl, resolveServerUrl, runCli }; | ||
| export { type BuildCliOptions, type ExecutedRequest, type HttpClientConfig, RequestError, ValidationError, buildBundle, buildCli, buildCommandsFromSpec, executeOperation, resolveBaseUrl, resolveServerUrl, runCli }; |
+27
-385
@@ -8,375 +8,13 @@ // src/index.ts | ||
| } from "dynamic-openapi-tools/parser"; | ||
| import { resolveAuth as resolveAuth2 } from "dynamic-openapi-tools/auth"; | ||
| // src/auth/oauth2-auth-code.ts | ||
| import { createHash as createHash2 } from "crypto"; | ||
| // src/auth/browser.ts | ||
| import { spawn } from "child_process"; | ||
| async function openBrowser(url) { | ||
| const opener = pickOpener(); | ||
| if (!opener) return false; | ||
| return new Promise((resolve) => { | ||
| try { | ||
| const child = spawn(opener.command, [...opener.args, url], { | ||
| detached: true, | ||
| stdio: "ignore" | ||
| }); | ||
| child.on("error", () => resolve(false)); | ||
| child.unref(); | ||
| setTimeout(() => resolve(true), 50); | ||
| } catch { | ||
| resolve(false); | ||
| } | ||
| }); | ||
| } | ||
| function pickOpener() { | ||
| if (process.env["BROWSER"]) { | ||
| return { command: process.env["BROWSER"], args: [] }; | ||
| } | ||
| switch (process.platform) { | ||
| case "darwin": | ||
| return { command: "open", args: [] }; | ||
| case "win32": | ||
| return { command: "cmd", args: ["/c", "start", '""'] }; | ||
| default: | ||
| return { command: "xdg-open", args: [] }; | ||
| } | ||
| } | ||
| // src/auth/loopback-server.ts | ||
| import { createServer } from "http"; | ||
| function captureCallback(options) { | ||
| const host = options.host ?? "127.0.0.1"; | ||
| const callbackPath = options.path ?? "/callback"; | ||
| const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3; | ||
| const successBody = options.successBody ?? "<html><body><h1>Login complete</h1><p>You can close this tab and return to your terminal.</p></body></html>"; | ||
| const errorBody = options.errorBody ?? "<html><body><h1>Login failed</h1><p>See your terminal for details.</p></body></html>"; | ||
| return new Promise((resolve, reject) => { | ||
| const server = createServer((req, res) => { | ||
| const url = new URL(req.url ?? "/", `http://${host}:${options.port}`); | ||
| if (url.pathname !== callbackPath) { | ||
| res.writeHead(404, { "Content-Type": "text/plain" }); | ||
| res.end("Not Found"); | ||
| return; | ||
| } | ||
| const params = url.searchParams; | ||
| const code = params.get("code") ?? void 0; | ||
| const state = params.get("state") ?? void 0; | ||
| const error = params.get("error") ?? void 0; | ||
| const errorDescription = params.get("error_description") ?? void 0; | ||
| if (error) { | ||
| res.writeHead(400, { | ||
| "Content-Type": "text/html; charset=utf-8", | ||
| Connection: "close" | ||
| }); | ||
| res.end(errorBody); | ||
| } else { | ||
| res.writeHead(200, { | ||
| "Content-Type": "text/html; charset=utf-8", | ||
| Connection: "close" | ||
| }); | ||
| res.end(successBody); | ||
| } | ||
| clearTimeout(timeout); | ||
| const result = {}; | ||
| if (code !== void 0) result.code = code; | ||
| if (state !== void 0) result.state = state; | ||
| if (error !== void 0) result.error = error; | ||
| if (errorDescription !== void 0) result.errorDescription = errorDescription; | ||
| server.closeAllConnections?.(); | ||
| server.close(() => resolve(result)); | ||
| }); | ||
| const timeout = setTimeout(() => { | ||
| server.close(() => reject(new Error(`OAuth2 callback timed out after ${timeoutMs}ms`))); | ||
| }, timeoutMs); | ||
| server.on("error", (err) => { | ||
| clearTimeout(timeout); | ||
| reject(err); | ||
| }); | ||
| server.listen(options.port, host); | ||
| }); | ||
| } | ||
| // src/auth/pkce.ts | ||
| import { createHash, randomBytes } from "crypto"; | ||
| function generatePkce() { | ||
| const verifier = base64UrlEncode(randomBytes(32)); | ||
| const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest()); | ||
| return { verifier, challenge, method: "S256" }; | ||
| } | ||
| function generateState() { | ||
| return base64UrlEncode(randomBytes(16)); | ||
| } | ||
| function base64UrlEncode(bytes) { | ||
| return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); | ||
| } | ||
| // src/auth/token-cache.ts | ||
| import { mkdir, readFile, rm, writeFile } from "fs/promises"; | ||
| import { homedir } from "os"; | ||
| import path from "path"; | ||
| function tokenCacheDir() { | ||
| const xdg = process.env["XDG_DATA_HOME"]; | ||
| if (xdg && xdg.length > 0) { | ||
| return path.join(xdg, "dynamic-openapi-cli", "tokens"); | ||
| } | ||
| return path.join(homedir(), ".local", "share", "dynamic-openapi-cli", "tokens"); | ||
| } | ||
| function tokenCachePath(key) { | ||
| return path.join(tokenCacheDir(), `${sanitizeKey(key)}.json`); | ||
| } | ||
| async function readTokenCache(key) { | ||
| try { | ||
| const raw = await readFile(tokenCachePath(key), "utf-8"); | ||
| const parsed = JSON.parse(raw); | ||
| if (!parsed || typeof parsed !== "object") return null; | ||
| if (typeof parsed.access_token !== "string") return null; | ||
| if (typeof parsed.expires_at !== "number") return null; | ||
| if (typeof parsed.token_type !== "string") return null; | ||
| if (!Array.isArray(parsed.scopes)) return null; | ||
| return parsed; | ||
| } catch (error) { | ||
| if (error.code === "ENOENT") return null; | ||
| return null; | ||
| } | ||
| } | ||
| async function writeTokenCache(key, token) { | ||
| const dir = tokenCacheDir(); | ||
| await mkdir(dir, { recursive: true, mode: 448 }); | ||
| const file = tokenCachePath(key); | ||
| await writeFile(file, JSON.stringify(token, null, 2), { mode: 384 }); | ||
| } | ||
| async function deleteTokenCache(key) { | ||
| try { | ||
| await rm(tokenCachePath(key), { force: true }); | ||
| } catch { | ||
| } | ||
| } | ||
| function sanitizeKey(key) { | ||
| return key.replace(/[^A-Za-z0-9._-]+/g, "-"); | ||
| } | ||
| // src/auth/oauth2-auth-code.ts | ||
| var DEFAULT_REDIRECT_PORT = 7999; | ||
| var DEFAULT_REFRESH_BUFFER_SECONDS = 30; | ||
| var OAuth2AuthCodeFlow = class { | ||
| config; | ||
| cacheKey; | ||
| cached; | ||
| pendingTokenRequest; | ||
| constructor(config) { | ||
| this.config = config; | ||
| this.cacheKey = deriveCacheKey(config); | ||
| } | ||
| async apply(_url, init) { | ||
| const token = await this.getAccessToken(); | ||
| const headers = new Headers(init.headers); | ||
| headers.set("Authorization", `Bearer ${token}`); | ||
| return { ...init, headers }; | ||
| } | ||
| async refresh(_url, init) { | ||
| this.cached = void 0; | ||
| await deleteTokenCache(this.cacheKey); | ||
| const token = await this.getAccessToken(); | ||
| const headers = new Headers(init.headers); | ||
| headers.set("Authorization", `Bearer ${token}`); | ||
| return { ...init, headers }; | ||
| } | ||
| /** Force a fresh login, bypassing the cache. Used by the `login` subcommand. */ | ||
| async forceLogin() { | ||
| this.cached = void 0; | ||
| await deleteTokenCache(this.cacheKey); | ||
| const token = await this.runLoginFlow(); | ||
| return token; | ||
| } | ||
| /** Wipe the cached token. Used by the `logout` subcommand. */ | ||
| async logout() { | ||
| this.cached = void 0; | ||
| await deleteTokenCache(this.cacheKey); | ||
| } | ||
| async getAccessToken() { | ||
| if (this.pendingTokenRequest) return this.pendingTokenRequest; | ||
| this.pendingTokenRequest = this.resolveToken().finally(() => { | ||
| this.pendingTokenRequest = void 0; | ||
| }); | ||
| return this.pendingTokenRequest; | ||
| } | ||
| async resolveToken() { | ||
| if (!this.cached) this.cached = await readTokenCache(this.cacheKey) ?? void 0; | ||
| const buffer = (this.config.refreshBufferSeconds ?? DEFAULT_REFRESH_BUFFER_SECONDS) * 1e3; | ||
| const now = Date.now(); | ||
| if (this.cached && this.cached.expires_at - buffer > now) { | ||
| return this.cached.access_token; | ||
| } | ||
| if (this.cached?.refresh_token) { | ||
| try { | ||
| const refreshed = await this.runRefreshFlow(this.cached.refresh_token); | ||
| return refreshed.access_token; | ||
| } catch (error) { | ||
| process.stderr.write( | ||
| `oauth2 ${this.config.schemeName}: refresh failed (${describeError(error)}), falling back to interactive login | ||
| ` | ||
| ); | ||
| } | ||
| } | ||
| const token = await this.runLoginFlow(); | ||
| return token.access_token; | ||
| } | ||
| async runLoginFlow() { | ||
| const port = this.config.redirectPort ?? DEFAULT_REDIRECT_PORT; | ||
| const redirectUri = this.config.redirectUri ?? `http://127.0.0.1:${port}/callback`; | ||
| const pkce = generatePkce(); | ||
| const state = generateState(); | ||
| const authUrl = buildAuthorizationUrl(this.config, redirectUri, pkce.challenge, state); | ||
| process.stderr.write(`oauth2 ${this.config.schemeName}: opening browser for login | ||
| `); | ||
| process.stderr.write(` If your browser does not open, visit: | ||
| ${authUrl} | ||
| `); | ||
| const [, callback] = await Promise.all([ | ||
| openBrowser(authUrl), | ||
| captureCallback({ port, host: "127.0.0.1", path: "/callback" }) | ||
| ]); | ||
| if (callback.error) { | ||
| throw new Error( | ||
| `OAuth2 login rejected: ${callback.error}${callback.errorDescription ? ` \u2014 ${callback.errorDescription}` : ""}` | ||
| ); | ||
| } | ||
| if (!callback.code) { | ||
| throw new Error("OAuth2 login did not return an authorization code"); | ||
| } | ||
| if (!callback.state || callback.state !== state) { | ||
| throw new Error("OAuth2 login state mismatch \u2014 possible CSRF, aborting"); | ||
| } | ||
| const tokens = await this.exchangeCode(callback.code, redirectUri, pkce.verifier); | ||
| const cached = toCachedToken(tokens, this.config.scopes); | ||
| this.cached = cached; | ||
| await writeTokenCache(this.cacheKey, cached); | ||
| return cached; | ||
| } | ||
| async runRefreshFlow(refreshToken) { | ||
| const body = new URLSearchParams(); | ||
| body.set("grant_type", "refresh_token"); | ||
| body.set("refresh_token", refreshToken); | ||
| body.set("client_id", this.config.clientId); | ||
| if (this.config.clientSecret) body.set("client_secret", this.config.clientSecret); | ||
| const tokens = await this.postTokenEndpoint(body); | ||
| if (!tokens.refresh_token) tokens.refresh_token = refreshToken; | ||
| const cached = toCachedToken(tokens, this.config.scopes); | ||
| this.cached = cached; | ||
| await writeTokenCache(this.cacheKey, cached); | ||
| return cached; | ||
| } | ||
| async exchangeCode(code, redirectUri, verifier) { | ||
| const body = new URLSearchParams(); | ||
| body.set("grant_type", "authorization_code"); | ||
| body.set("code", code); | ||
| body.set("redirect_uri", redirectUri); | ||
| body.set("client_id", this.config.clientId); | ||
| body.set("code_verifier", verifier); | ||
| if (this.config.clientSecret) body.set("client_secret", this.config.clientSecret); | ||
| return this.postTokenEndpoint(body); | ||
| } | ||
| async postTokenEndpoint(body) { | ||
| const response = await fetch(this.config.tokenUrl, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/x-www-form-urlencoded", | ||
| Accept: "application/json" | ||
| }, | ||
| body | ||
| }); | ||
| if (!response.ok) { | ||
| const text = await response.text().catch(() => ""); | ||
| throw new Error(`OAuth2 token endpoint returned ${response.status}${text ? `: ${text}` : ""}`); | ||
| } | ||
| const json = await response.json(); | ||
| if (!json || typeof json.access_token !== "string") { | ||
| throw new Error("OAuth2 token endpoint did not return an access_token"); | ||
| } | ||
| return json; | ||
| } | ||
| }; | ||
| function buildAuthorizationUrl(config, redirectUri, codeChallenge, state) { | ||
| const url = new URL(config.authorizationUrl); | ||
| url.searchParams.set("response_type", "code"); | ||
| url.searchParams.set("client_id", config.clientId); | ||
| url.searchParams.set("redirect_uri", redirectUri); | ||
| url.searchParams.set("state", state); | ||
| url.searchParams.set("code_challenge", codeChallenge); | ||
| url.searchParams.set("code_challenge_method", "S256"); | ||
| if (config.scopes.length > 0) url.searchParams.set("scope", config.scopes.join(" ")); | ||
| if (config.extraAuthParams) { | ||
| for (const [k, v] of Object.entries(config.extraAuthParams)) { | ||
| url.searchParams.set(k, v); | ||
| } | ||
| } | ||
| return url.toString(); | ||
| } | ||
| function toCachedToken(response, fallbackScopes) { | ||
| const expiresIn = typeof response.expires_in === "number" ? response.expires_in : 3600; | ||
| const scopes = typeof response.scope === "string" && response.scope.length > 0 ? response.scope.split(/\s+/) : fallbackScopes; | ||
| const token = { | ||
| access_token: response.access_token, | ||
| token_type: response.token_type ?? "Bearer", | ||
| expires_at: Date.now() + expiresIn * 1e3, | ||
| scopes | ||
| }; | ||
| if (response.refresh_token) token.refresh_token = response.refresh_token; | ||
| return token; | ||
| } | ||
| function deriveCacheKey(config) { | ||
| const hash = createHash2("sha256").update(config.clientId).update("|").update(config.tokenUrl).update("|").update([...config.scopes].sort().join(" ")).digest("hex").slice(0, 16); | ||
| return `${config.schemeName}-${hash}`; | ||
| } | ||
| function describeError(error) { | ||
| if (error instanceof Error) return error.message; | ||
| return String(error); | ||
| } | ||
| // src/auth/resolve.ts | ||
| function detectOAuth2AuthCode(securitySchemes, env = process.env) { | ||
| for (const [name, scheme] of Object.entries(securitySchemes)) { | ||
| if (!scheme || scheme.type !== "oauth2") continue; | ||
| const flow = scheme.flows?.authorizationCode; | ||
| if (!flow) continue; | ||
| const schemeEnv = envKeyFor(name); | ||
| const clientId = env[`OPENAPI_AUTH_${schemeEnv}_CLIENT_ID`] ?? env["OPENAPI_OAUTH2_CLIENT_ID"]; | ||
| if (!clientId) continue; | ||
| const clientSecret = env[`OPENAPI_AUTH_${schemeEnv}_CLIENT_SECRET`] ?? env["OPENAPI_OAUTH2_CLIENT_SECRET"]; | ||
| const scopeOverride = env[`OPENAPI_AUTH_${schemeEnv}_SCOPES`] ?? env["OPENAPI_OAUTH2_SCOPES"]; | ||
| const portEnv = env[`OPENAPI_AUTH_${schemeEnv}_PORT`] ?? env["OPENAPI_OAUTH2_PORT"]; | ||
| const redirectUri = env[`OPENAPI_AUTH_${schemeEnv}_REDIRECT_URI`] ?? env["OPENAPI_OAUTH2_REDIRECT_URI"]; | ||
| const scopes = scopeOverride ? scopeOverride.split(/[\s,]+/).filter(Boolean) : Object.keys(flow.scopes ?? {}); | ||
| const config = { | ||
| schemeName: name, | ||
| clientId, | ||
| authorizationUrl: flow.authorizationUrl, | ||
| tokenUrl: flow.tokenUrl, | ||
| scopes | ||
| }; | ||
| if (clientSecret) config.clientSecret = clientSecret; | ||
| if (redirectUri) config.redirectUri = redirectUri; | ||
| if (portEnv) { | ||
| const port = Number.parseInt(portEnv, 10); | ||
| if (!Number.isNaN(port) && port > 0) config.redirectPort = port; | ||
| } | ||
| return { schemeName: name, config }; | ||
| } | ||
| return null; | ||
| } | ||
| function createOAuth2AuthCodeAuth(config) { | ||
| return new OAuth2AuthCodeFlow(config); | ||
| } | ||
| function envKeyFor(schemeName) { | ||
| return schemeName.toUpperCase().replace(/[^A-Z0-9]+/g, "_"); | ||
| } | ||
| // src/index.ts | ||
| import { | ||
| resolveAuth as resolveAuth2, | ||
| OAuth2AuthCodeFlow, | ||
| detectOAuth2AuthCode as detectOAuth2AuthCode2, | ||
| createOAuth2AuthCodeAuth as createOAuth2AuthCodeAuth2 | ||
| } from "dynamic-openapi-tools/auth"; | ||
| import { fetchWithRetry as fetchWithRetry2 } from "dynamic-openapi-tools/utils"; | ||
| // src/http/client.ts | ||
| import { readFile as readFile2 } from "fs/promises"; | ||
| import path2 from "path"; | ||
| import { readFile } from "fs/promises"; | ||
| import path from "path"; | ||
| import { fetchWithRetry } from "dynamic-openapi-tools/utils"; | ||
@@ -666,5 +304,5 @@ var RequestError = class extends Error { | ||
| if (fileRef) { | ||
| const fileContents = await readFile2(fileRef.path); | ||
| const fileContents = await readFile(fileRef.path); | ||
| const bytes = Buffer.from(fileContents); | ||
| const filename = path2.basename(fileRef.path); | ||
| const filename = path.basename(fileRef.path); | ||
| const blob = new Blob([bytes], { type: "application/octet-stream" }); | ||
@@ -734,3 +372,3 @@ form.append(key, blob, filename); | ||
| if (fileRef) { | ||
| const bytes = await readFile2(fileRef.path); | ||
| const bytes = await readFile(fileRef.path); | ||
| return { | ||
@@ -742,3 +380,3 @@ body: bytes, | ||
| filePath: fileRef.path, | ||
| filename: path2.basename(fileRef.path), | ||
| filename: path.basename(fileRef.path), | ||
| bytes: bytes.byteLength | ||
@@ -980,3 +618,3 @@ } | ||
| // src/cli/bundle.ts | ||
| import path3 from "path"; | ||
| import path2 from "path"; | ||
| import { createParser } from "dynamic-openapi-tools/cli"; | ||
@@ -1014,5 +652,9 @@ import { buildBundle as toolsBuildBundle } from "dynamic-openapi-tools/bundle"; | ||
| // src/cli/app.ts | ||
| import { readFile as readFile3 } from "fs/promises"; | ||
| import { readFile as readFile2 } from "fs/promises"; | ||
| import { createCLI, formatErrors } from "dynamic-openapi-tools/cli"; | ||
| import { resolveAuth } from "dynamic-openapi-tools/auth"; | ||
| import { | ||
| resolveAuth, | ||
| createOAuth2AuthCodeAuth, | ||
| detectOAuth2AuthCode | ||
| } from "dynamic-openapi-tools/auth"; | ||
| import { filterOperations } from "dynamic-openapi-tools/parser"; | ||
@@ -1070,3 +712,3 @@ | ||
| // src/cli/output.ts | ||
| import { writeFile as writeFile2 } from "fs/promises"; | ||
| import { writeFile } from "fs/promises"; | ||
| var INLINE_BINARY_LIMIT = 256 * 1024; | ||
@@ -1087,3 +729,3 @@ async function renderResponse(response, options = {}) { | ||
| const bytes2 = new Uint8Array(await response.arrayBuffer()); | ||
| await writeFile2(options.outputFile, bytes2); | ||
| await writeFile(options.outputFile, bytes2); | ||
| process.stderr.write(`wrote ${bytes2.byteLength} bytes to ${options.outputFile} | ||
@@ -1178,3 +820,3 @@ `); | ||
| const baseUrl = resolveBaseUrl(spec, options.baseUrl, options.serverIndex); | ||
| const auth = resolveAuthWithOAuth2(spec, options.authConfig); | ||
| const auth = resolveAuthWithOAuth2(spec, options.authConfig, options.name); | ||
| const httpConfig = { | ||
@@ -1285,3 +927,3 @@ baseUrl, | ||
| if (bodyFile) { | ||
| const text = await readFile3(bodyFile, "utf-8"); | ||
| const text = await readFile2(bodyFile, "utf-8"); | ||
| merged["body"] = tryParseJson(text); | ||
@@ -1297,6 +939,6 @@ } else if (bodyRaw === "-") { | ||
| } | ||
| function resolveAuthWithOAuth2(spec, authConfig) { | ||
| function resolveAuthWithOAuth2(spec, authConfig, appName) { | ||
| const resolved = resolveAuth(authConfig, spec.securitySchemes); | ||
| if (resolved) return resolved; | ||
| const detected = detectOAuth2AuthCode(spec.securitySchemes); | ||
| const detected = detectOAuth2AuthCode(spec.securitySchemes, { appName }); | ||
| if (detected) return createOAuth2AuthCodeAuth(detected.config); | ||
@@ -1336,4 +978,4 @@ return null; | ||
| buildCommandsFromSpec, | ||
| createOAuth2AuthCodeAuth, | ||
| detectOAuth2AuthCode, | ||
| createOAuth2AuthCodeAuth2 as createOAuth2AuthCodeAuth, | ||
| detectOAuth2AuthCode2 as detectOAuth2AuthCode, | ||
| executeOperation, | ||
@@ -1340,0 +982,0 @@ fetchWithRetry2 as fetchWithRetry, |
+2
-2
| { | ||
| "name": "dynamic-openapi-cli", | ||
| "version": "0.1.4", | ||
| "version": "0.1.5", | ||
| "description": "Transform any OpenAPI v3 spec into a fully functional CLI", | ||
@@ -30,3 +30,3 @@ "type": "module", | ||
| "dependencies": { | ||
| "dynamic-openapi-tools": "^1.1.1" | ||
| "dynamic-openapi-tools": "^1.1.4" | ||
| }, | ||
@@ -33,0 +33,0 @@ "devDependencies": { |
+3
-1
@@ -504,4 +504,6 @@ <div align="center"> | ||
| When the spec declares an OAuth2 `authorizationCode` flow and `OPENAPI_OAUTH2_CLIENT_ID` is present, the CLI triggers a browser-based login on the first request — PKCE + loopback server on `127.0.0.1:7999` — and caches the token under `$XDG_DATA_HOME/dynamic-openapi-cli/tokens/`. Subsequent calls reuse the cached token and refresh it transparently when it expires. | ||
| When the spec declares an OAuth2 `authorizationCode` flow and `OPENAPI_OAUTH2_CLIENT_ID` is present, the CLI triggers a browser-based login on the first request — PKCE + loopback server on `127.0.0.1:7999` — and caches the token under `$XDG_DATA_HOME/dynamic-openapi-cli/<app>.env`. Subsequent calls reuse the cached token and refresh it transparently when it expires. | ||
| The cache file is one per application: the raw dynamic CLI writes to `global.env`, a bundle's `--name my-pet-store` writes to `my-pet-store.env`. Contents are AES-256-GCM encrypted with a key derived from the application name — **symbolic at-rest protection**: it stops `cat`, grep, and backup indexers from seeing plaintext tokens, but anyone with the source and the filename can decrypt it. Real protection relies on the `0600` file mode and your filesystem permissions. | ||
| ```bash | ||
@@ -508,0 +510,0 @@ export OPENAPI_OAUTH2_CLIENT_ID=cli-public |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
770
0.26%15
-40%0
-100%275610
-23.53%2402
-24.11%Updated