🚀. Socket Launch Week Day 3:Socket Firewall Now Blocks Malicious VS Code and Open VSX Extensions.Learn more
Sign In

dynamic-openapi-cli

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dynamic-openapi-cli - npm Package Compare versions

Comparing version
0.1.4
to
0.1.5
+34
-391
dist/cli.js

@@ -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 @@ }

@@ -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 };

@@ -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": {

@@ -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