🚀. 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.5
to
0.1.7
+172
-62
dist/cli.js

@@ -25,2 +25,113 @@ #!/usr/bin/env node

import { fetchWithRetry } from "dynamic-openapi-tools/utils";
// src/http/curl.ts
var REDACTED_HEADERS = /* @__PURE__ */ new Set([
"authorization",
"proxy-authorization",
"cookie",
"set-cookie",
"x-api-key",
"api-key"
]);
function renderCurl(prepared) {
const parts = [];
parts.push(`curl -X ${prepared.method} ${shellQuote(prepared.url.toString())}`);
const headerEntries = [];
prepared.headers.forEach((value, key) => {
headerEntries.push([key, value]);
});
for (const [key, value] of headerEntries) {
const safeValue = REDACTED_HEADERS.has(key.toLowerCase()) ? "***" : value;
parts.push(` -H ${shellQuote(`${key}: ${safeValue}`)}`);
}
const bodyLines = renderBody(prepared);
for (const line of bodyLines) {
parts.push(` ${line}`);
}
return parts.join(" \\\n");
}
function renderBody(prepared) {
const info = prepared.bodyInfo;
switch (info.kind) {
case "none":
return [];
case "json":
return [`--data ${shellQuote(JSON.stringify(info.value))}`];
case "urlencoded":
return info.pairs.map(([k, v]) => `--data-urlencode ${shellQuote(`${k}=${v}`)}`);
case "multipart":
return info.fields.map((field) => {
if (field.kind === "value") {
return `-F ${shellQuote(`${field.name}=${field.value}`)}`;
}
if (field.path) {
return `-F ${shellQuote(`${field.name}=@${field.path}`)}`;
}
return `-F ${shellQuote(`${field.name}=@${field.filename}`)} # ${field.bytes} bytes, ${field.contentType}`;
});
case "binary":
if (info.filePath) {
return [`--data-binary ${shellQuote(`@${info.filePath}`)}`];
}
return [`--data-binary @- # ${info.bytes} bytes`];
case "text":
return [`--data ${shellQuote(info.value)}`];
}
}
function shellQuote(value) {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
// src/http/safety.ts
var READ_ONLY_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
var DESTRUCTIVE_METHODS = /* @__PURE__ */ new Set(["DELETE"]);
var SAFETY_ENV_VARS = {
dryRun: "DYNAMIC_OPENAPI_DRY_RUN",
noDestructive: "DYNAMIC_OPENAPI_NO_DESTRUCTIVE"
};
var SafetyError = class extends Error {
constructor(message, operationId, sideEffect, reason) {
super(message);
this.operationId = operationId;
this.sideEffect = sideEffect;
this.reason = reason;
this.name = "SafetyError";
}
operationId;
sideEffect;
reason;
};
function classifySideEffect(operation) {
const override = readSideEffectExtension(operation);
if (override) return override;
const method = operation.method.toUpperCase();
if (READ_ONLY_METHODS.has(method)) return "read-only";
if (DESTRUCTIVE_METHODS.has(method)) return "destructive";
return "write";
}
function readSideEffectExtension(operation) {
const bag = operation;
const direct = bag["x-side-effect"];
if (typeof direct === "string" && isSideEffect(direct)) return direct;
if (bag["x-destructive"] === true) return "destructive";
const extensions = bag["extensions"];
if (extensions && typeof extensions === "object") {
const ext = extensions;
const fromBag = ext["x-side-effect"];
if (typeof fromBag === "string" && isSideEffect(fromBag)) return fromBag;
if (ext["x-destructive"] === true) return "destructive";
}
return null;
}
function isSideEffect(value) {
return value === "read-only" || value === "write" || value === "destructive";
}
function isDryRunFloor(env = process.env) {
return env[SAFETY_ENV_VARS.dryRun] === "1";
}
function isNoDestructiveFloor(env = process.env) {
return env[SAFETY_ENV_VARS.noDestructive] === "1";
}
// src/http/client.ts
var RequestError = class extends Error {

@@ -141,2 +252,35 @@ constructor(message, cause) {

const prepared = await prepareRequest(operation, args, config);
const sideEffect = classifySideEffect(operation);
if (sideEffect === "destructive") {
if (isNoDestructiveFloor()) {
throw new SafetyError(
`Destructive operation "${operation.operationId}" blocked by ${SAFETY_ENV_VARS.noDestructive}=1 (floor is absolute).`,
operation.operationId,
sideEffect,
"floor-blocked"
);
}
if (!config.allowDestructive) {
throw new SafetyError(
`Destructive operation "${operation.operationId}" requires consent. Pass --yes (CLI) or allowDestructive: true (programmatic).`,
operation.operationId,
sideEffect,
"no-consent"
);
}
}
if (config.dryRun || isDryRunFloor()) {
const curl = renderCurl(prepared);
const previewResponse = new Response(curl + "\n", {
status: 200,
headers: { "Content-Type": "text/plain; charset=utf-8" }
});
return {
response: previewResponse,
url: prepared.url.toString(),
method: prepared.method,
dryRun: true,
sideEffect
};
}
const init = {

@@ -163,3 +307,3 @@ method: prepared.method,

}
return { response, url: prepared.url.toString(), method: prepared.method };
return { response, url: prepared.url.toString(), method: prepared.method, sideEffect };
}

@@ -621,51 +765,2 @@ function validateRequiredParams(operation, args) {

// src/cli/curl.ts
function renderCurl(prepared) {
const parts = [];
parts.push(`curl -X ${prepared.method} ${shellQuote(prepared.url.toString())}`);
const headerEntries = [];
prepared.headers.forEach((value, key) => {
headerEntries.push([key, value]);
});
for (const [key, value] of headerEntries) {
parts.push(` -H ${shellQuote(`${key}: ${value}`)}`);
}
const bodyLines = renderBody(prepared);
for (const line of bodyLines) {
parts.push(` ${line}`);
}
return parts.join(" \\\n");
}
function renderBody(prepared) {
const info = prepared.bodyInfo;
switch (info.kind) {
case "none":
return [];
case "json":
return [`--data ${shellQuote(JSON.stringify(info.value))}`];
case "urlencoded":
return info.pairs.map(([k, v]) => `--data-urlencode ${shellQuote(`${k}=${v}`)}`);
case "multipart":
return info.fields.map((field) => {
if (field.kind === "value") {
return `-F ${shellQuote(`${field.name}=${field.value}`)}`;
}
if (field.path) {
return `-F ${shellQuote(`${field.name}=@${field.path}`)}`;
}
return `-F ${shellQuote(`${field.name}=@${field.filename}`)} # ${field.bytes} bytes, ${field.contentType}`;
});
case "binary":
if (info.filePath) {
return [`--data-binary ${shellQuote(`@${info.filePath}`)}`];
}
return [`--data-binary @- # ${info.bytes} bytes`];
case "text":
return [`--data ${shellQuote(info.value)}`];
}
}
function shellQuote(value) {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
// src/cli/output.ts

@@ -770,2 +865,8 @@ import { writeFile } from "fs/promises";

default: false
},
yes: {
short: "y",
type: "boolean",
description: "Consent to destructive operations (DELETE, x-side-effect: destructive)",
default: false
}

@@ -780,8 +881,2 @@ };

const auth = resolveAuthWithOAuth2(spec, options.authConfig, options.name);
const httpConfig = {
baseUrl,
auth,
defaultHeaders: options.defaultHeaders,
fetchOptions: options.fetchOptions
};
const { commands, collisions } = buildCommandsFromSpec(spec, {

@@ -795,14 +890,29 @@ handler: async (context, args) => {

};
const dryRun = Boolean(args.options["dry-run"]);
const httpConfig = {
baseUrl,
auth,
defaultHeaders: options.defaultHeaders,
fetchOptions: options.fetchOptions,
dryRun: Boolean(args.options["dry-run"]),
allowDestructive: Boolean(args.options["yes"])
};
try {
const { response, dryRun } = await executeOperation(context.operation, merged, httpConfig);
if (dryRun) {
const prepared = await prepareRequest(context.operation, merged, httpConfig);
process.stdout.write(renderCurl(prepared));
process.stdout.write("\n");
process.stdout.write(await response.text());
return;
}
const { response } = await executeOperation(context.operation, merged, httpConfig);
const code = await renderResponse(response, outputOptions);
if (code !== 0) process.exitCode = code;
} catch (error) {
if (error instanceof SafetyError) {
process.stderr.write(`safety: ${error.message}
`);
if (error.reason === "no-consent") {
process.stderr.write(` retry with --yes, or set ${SAFETY_ENV_VARS.noDestructive}=0 if a floor is in effect.
`);
}
process.exitCode = 3;
return;
}
if (error instanceof ValidationError) {

@@ -880,3 +990,3 @@ process.stderr.write(`${error.message}

for (const [key, value] of Object.entries(args.options)) {
if (key === "output" || key === "raw" || key === "verbose" || key === "dry-run" || key === "body" || key === "body-file") continue;
if (key === "output" || key === "raw" || key === "verbose" || key === "dry-run" || key === "yes" || key === "body" || key === "body-file") continue;
if (value !== void 0) merged[key] = value;

@@ -883,0 +993,0 @@ }

@@ -10,2 +10,42 @@ import { ParsedOperation, ParsedSpec, ParsedServer, OperationFilters } from 'dynamic-openapi-tools/parser';

type SideEffect = 'read-only' | 'write' | 'destructive';
declare const SAFETY_ENV_VARS: {
readonly dryRun: "DYNAMIC_OPENAPI_DRY_RUN";
readonly noDestructive: "DYNAMIC_OPENAPI_NO_DESTRUCTIVE";
};
declare class SafetyError extends Error {
readonly operationId: string;
readonly sideEffect: SideEffect;
readonly reason: 'no-consent' | 'floor-blocked';
constructor(message: string, operationId: string, sideEffect: SideEffect, reason: 'no-consent' | 'floor-blocked');
}
/**
* Classify the side-effect of an operation. HTTP method is the baseline.
* Vendor extensions override:
* x-side-effect: 'read-only' | 'write' | 'destructive'
* x-destructive: true (sugar for x-side-effect: 'destructive')
*
* The extension can sit on the ParsedOperation itself (parser-exposed) or on a
* `.extensions` record. Both shapes are supported defensively because the
* upstream parser does not type-export extensions on every release.
*/
declare function classifySideEffect(operation: ParsedOperation): SideEffect;
declare function isDryRunFloor(env?: NodeJS.ProcessEnv): boolean;
declare function isNoDestructiveFloor(env?: NodeJS.ProcessEnv): boolean;
/**
* Render the resolved request as a multiline curl command, suitable for
* `--dry-run` output. Headers follow a stable order (the Headers object's
* insertion order after `auth.apply`).
*
* Sensitive headers (Authorization, Cookie, api-key variants) are always
* redacted to `***` — tokens never reach stdout, even in dry-run.
*/
declare function renderCurl(prepared: PreparedRequest): string;
/**
* Wrap a string in single quotes, escaping any embedded single quotes the
* POSIX way: `'` → `'\''`.
*/
declare function shellQuote(value: string): string;
interface HttpClientConfig {

@@ -16,2 +56,6 @@ baseUrl: string;

fetchOptions?: FetchWithRetryOptions;
/** Render the curl-equivalent instead of firing the request. */
dryRun?: boolean;
/** Consent for destructive operations (DELETE or x-side-effect: destructive). */
allowDestructive?: boolean;
}

@@ -22,3 +66,61 @@ interface ExecutedRequest {

method: string;
/** True when the response is a synthetic curl preview produced by dry-run. */
dryRun?: boolean;
/** Side-effect classification used at execution time. */
sideEffect?: SideEffect;
}
/**
* Semantic description of the resolved request body. Used both as input to
* `fetch` (via `body` on RequestInit) and as hints for downstream renderers
* like `--dry-run` curl output.
*/
type PreparedBodyInfo = {
kind: 'none';
} | {
kind: 'json';
value: unknown;
contentType: string;
} | {
kind: 'urlencoded';
pairs: Array<[string, string]>;
contentType: string;
} | {
kind: 'multipart';
fields: MultipartField[];
contentType: string;
} | {
kind: 'binary';
contentType: string;
/** Original @path if the body came from a file reference. */
filePath?: string;
/** Original filename (from @path or dataBase64 payload). */
filename?: string;
bytes: number;
} | {
kind: 'text';
value: string;
contentType: string;
};
type MultipartField = {
name: string;
kind: 'value';
value: string;
} | {
name: string;
kind: 'file';
/** Original @path reference (for curl rendering). */
path?: string;
filename: string;
contentType: string;
bytes: number;
};
interface PreparedRequest {
url: URL;
method: string;
headers: Headers;
body: RequestInit['body'];
bodyInfo: PreparedBodyInfo;
operation: ParsedOperation;
}
declare class RequestError extends Error {

@@ -94,2 +196,2 @@ readonly cause?: unknown | undefined;

export { type BuildCliOptions, type ExecutedRequest, type HttpClientConfig, RequestError, ValidationError, buildBundle, buildCli, buildCommandsFromSpec, executeOperation, resolveBaseUrl, resolveServerUrl, runCli };
export { type BuildCliOptions, type ExecutedRequest, type HttpClientConfig, RequestError, SAFETY_ENV_VARS, SafetyError, type SideEffect, ValidationError, buildBundle, buildCli, buildCommandsFromSpec, classifySideEffect, executeOperation, isDryRunFloor, isNoDestructiveFloor, renderCurl, resolveBaseUrl, resolveServerUrl, runCli, shellQuote };

@@ -20,2 +20,113 @@ // src/index.ts

import { fetchWithRetry } from "dynamic-openapi-tools/utils";
// src/http/curl.ts
var REDACTED_HEADERS = /* @__PURE__ */ new Set([
"authorization",
"proxy-authorization",
"cookie",
"set-cookie",
"x-api-key",
"api-key"
]);
function renderCurl(prepared) {
const parts = [];
parts.push(`curl -X ${prepared.method} ${shellQuote(prepared.url.toString())}`);
const headerEntries = [];
prepared.headers.forEach((value, key) => {
headerEntries.push([key, value]);
});
for (const [key, value] of headerEntries) {
const safeValue = REDACTED_HEADERS.has(key.toLowerCase()) ? "***" : value;
parts.push(` -H ${shellQuote(`${key}: ${safeValue}`)}`);
}
const bodyLines = renderBody(prepared);
for (const line of bodyLines) {
parts.push(` ${line}`);
}
return parts.join(" \\\n");
}
function renderBody(prepared) {
const info = prepared.bodyInfo;
switch (info.kind) {
case "none":
return [];
case "json":
return [`--data ${shellQuote(JSON.stringify(info.value))}`];
case "urlencoded":
return info.pairs.map(([k, v]) => `--data-urlencode ${shellQuote(`${k}=${v}`)}`);
case "multipart":
return info.fields.map((field) => {
if (field.kind === "value") {
return `-F ${shellQuote(`${field.name}=${field.value}`)}`;
}
if (field.path) {
return `-F ${shellQuote(`${field.name}=@${field.path}`)}`;
}
return `-F ${shellQuote(`${field.name}=@${field.filename}`)} # ${field.bytes} bytes, ${field.contentType}`;
});
case "binary":
if (info.filePath) {
return [`--data-binary ${shellQuote(`@${info.filePath}`)}`];
}
return [`--data-binary @- # ${info.bytes} bytes`];
case "text":
return [`--data ${shellQuote(info.value)}`];
}
}
function shellQuote(value) {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
// src/http/safety.ts
var READ_ONLY_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
var DESTRUCTIVE_METHODS = /* @__PURE__ */ new Set(["DELETE"]);
var SAFETY_ENV_VARS = {
dryRun: "DYNAMIC_OPENAPI_DRY_RUN",
noDestructive: "DYNAMIC_OPENAPI_NO_DESTRUCTIVE"
};
var SafetyError = class extends Error {
constructor(message, operationId, sideEffect, reason) {
super(message);
this.operationId = operationId;
this.sideEffect = sideEffect;
this.reason = reason;
this.name = "SafetyError";
}
operationId;
sideEffect;
reason;
};
function classifySideEffect(operation) {
const override = readSideEffectExtension(operation);
if (override) return override;
const method = operation.method.toUpperCase();
if (READ_ONLY_METHODS.has(method)) return "read-only";
if (DESTRUCTIVE_METHODS.has(method)) return "destructive";
return "write";
}
function readSideEffectExtension(operation) {
const bag = operation;
const direct = bag["x-side-effect"];
if (typeof direct === "string" && isSideEffect(direct)) return direct;
if (bag["x-destructive"] === true) return "destructive";
const extensions = bag["extensions"];
if (extensions && typeof extensions === "object") {
const ext = extensions;
const fromBag = ext["x-side-effect"];
if (typeof fromBag === "string" && isSideEffect(fromBag)) return fromBag;
if (ext["x-destructive"] === true) return "destructive";
}
return null;
}
function isSideEffect(value) {
return value === "read-only" || value === "write" || value === "destructive";
}
function isDryRunFloor(env = process.env) {
return env[SAFETY_ENV_VARS.dryRun] === "1";
}
function isNoDestructiveFloor(env = process.env) {
return env[SAFETY_ENV_VARS.noDestructive] === "1";
}
// src/http/client.ts
var RequestError = class extends Error {

@@ -136,2 +247,35 @@ constructor(message, cause) {

const prepared = await prepareRequest(operation, args, config);
const sideEffect = classifySideEffect(operation);
if (sideEffect === "destructive") {
if (isNoDestructiveFloor()) {
throw new SafetyError(
`Destructive operation "${operation.operationId}" blocked by ${SAFETY_ENV_VARS.noDestructive}=1 (floor is absolute).`,
operation.operationId,
sideEffect,
"floor-blocked"
);
}
if (!config.allowDestructive) {
throw new SafetyError(
`Destructive operation "${operation.operationId}" requires consent. Pass --yes (CLI) or allowDestructive: true (programmatic).`,
operation.operationId,
sideEffect,
"no-consent"
);
}
}
if (config.dryRun || isDryRunFloor()) {
const curl = renderCurl(prepared);
const previewResponse = new Response(curl + "\n", {
status: 200,
headers: { "Content-Type": "text/plain; charset=utf-8" }
});
return {
response: previewResponse,
url: prepared.url.toString(),
method: prepared.method,
dryRun: true,
sideEffect
};
}
const init = {

@@ -158,3 +302,3 @@ method: prepared.method,

}
return { response, url: prepared.url.toString(), method: prepared.method };
return { response, url: prepared.url.toString(), method: prepared.method, sideEffect };
}

@@ -659,51 +803,2 @@ function validateRequiredParams(operation, args) {

// src/cli/curl.ts
function renderCurl(prepared) {
const parts = [];
parts.push(`curl -X ${prepared.method} ${shellQuote(prepared.url.toString())}`);
const headerEntries = [];
prepared.headers.forEach((value, key) => {
headerEntries.push([key, value]);
});
for (const [key, value] of headerEntries) {
parts.push(` -H ${shellQuote(`${key}: ${value}`)}`);
}
const bodyLines = renderBody(prepared);
for (const line of bodyLines) {
parts.push(` ${line}`);
}
return parts.join(" \\\n");
}
function renderBody(prepared) {
const info = prepared.bodyInfo;
switch (info.kind) {
case "none":
return [];
case "json":
return [`--data ${shellQuote(JSON.stringify(info.value))}`];
case "urlencoded":
return info.pairs.map(([k, v]) => `--data-urlencode ${shellQuote(`${k}=${v}`)}`);
case "multipart":
return info.fields.map((field) => {
if (field.kind === "value") {
return `-F ${shellQuote(`${field.name}=${field.value}`)}`;
}
if (field.path) {
return `-F ${shellQuote(`${field.name}=@${field.path}`)}`;
}
return `-F ${shellQuote(`${field.name}=@${field.filename}`)} # ${field.bytes} bytes, ${field.contentType}`;
});
case "binary":
if (info.filePath) {
return [`--data-binary ${shellQuote(`@${info.filePath}`)}`];
}
return [`--data-binary @- # ${info.bytes} bytes`];
case "text":
return [`--data ${shellQuote(info.value)}`];
}
}
function shellQuote(value) {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
// src/cli/output.ts

@@ -808,2 +903,8 @@ import { writeFile } from "fs/promises";

default: false
},
yes: {
short: "y",
type: "boolean",
description: "Consent to destructive operations (DELETE, x-side-effect: destructive)",
default: false
}

@@ -818,8 +919,2 @@ };

const auth = resolveAuthWithOAuth2(spec, options.authConfig, options.name);
const httpConfig = {
baseUrl,
auth,
defaultHeaders: options.defaultHeaders,
fetchOptions: options.fetchOptions
};
const { commands, collisions } = buildCommandsFromSpec(spec, {

@@ -833,14 +928,29 @@ handler: async (context, args) => {

};
const dryRun = Boolean(args.options["dry-run"]);
const httpConfig = {
baseUrl,
auth,
defaultHeaders: options.defaultHeaders,
fetchOptions: options.fetchOptions,
dryRun: Boolean(args.options["dry-run"]),
allowDestructive: Boolean(args.options["yes"])
};
try {
const { response, dryRun } = await executeOperation(context.operation, merged, httpConfig);
if (dryRun) {
const prepared = await prepareRequest(context.operation, merged, httpConfig);
process.stdout.write(renderCurl(prepared));
process.stdout.write("\n");
process.stdout.write(await response.text());
return;
}
const { response } = await executeOperation(context.operation, merged, httpConfig);
const code = await renderResponse(response, outputOptions);
if (code !== 0) process.exitCode = code;
} catch (error) {
if (error instanceof SafetyError) {
process.stderr.write(`safety: ${error.message}
`);
if (error.reason === "no-consent") {
process.stderr.write(` retry with --yes, or set ${SAFETY_ENV_VARS.noDestructive}=0 if a floor is in effect.
`);
}
process.exitCode = 3;
return;
}
if (error instanceof ValidationError) {

@@ -918,3 +1028,3 @@ process.stderr.write(`${error.message}

for (const [key, value] of Object.entries(args.options)) {
if (key === "output" || key === "raw" || key === "verbose" || key === "dry-run" || key === "body" || key === "body-file") continue;
if (key === "output" || key === "raw" || key === "verbose" || key === "dry-run" || key === "yes" || key === "body" || key === "body-file") continue;
if (value !== void 0) merged[key] = value;

@@ -971,2 +1081,4 @@ }

RequestError,
SAFETY_ENV_VARS,
SafetyError,
ValidationError,

@@ -976,2 +1088,3 @@ buildBundle,

buildCommandsFromSpec,
classifySideEffect,
createOAuth2AuthCodeAuth2 as createOAuth2AuthCodeAuth,

@@ -982,3 +1095,6 @@ detectOAuth2AuthCode2 as detectOAuth2AuthCode,

filterOperations2 as filterOperations,
isDryRunFloor,
isNoDestructiveFloor,
loadSpec,
renderCurl,
resolveAuth2 as resolveAuth,

@@ -989,4 +1105,5 @@ resolveBaseUrl,

resolveSpec,
runCli
runCli,
shellQuote
};
//# sourceMappingURL=index.js.map
{
"name": "dynamic-openapi-cli",
"version": "0.1.5",
"version": "0.1.7",
"description": "Transform any OpenAPI v3 spec into a fully functional CLI",

@@ -5,0 +5,0 @@ "type": "module",

@@ -260,3 +260,3 @@ <div align="center">

Every CLI or bundled shim can print the resolved request as a `curl` command — URL, headers (including the ones resolved by auth), and body — without firing it:
Every CLI or bundled shim can print the resolved request as a `curl` command — URL, headers (including the ones resolved by auth), and body — without firing it. Sensitive headers (`Authorization`, `Cookie`, `X-Api-Key` and friends) are **always redacted to `***`** so tokens never reach stdout:

@@ -267,3 +267,3 @@ ```bash

-H 'accept: application/json' \
-H 'authorization: Bearer sk-…'
-H 'authorization: ***'

@@ -279,2 +279,45 @@ $ petstore create-pet --dry-run --body='{"name":"rex"}'

### Safety: destructive consent and env-var floors
Every operation is classified by HTTP method:
| Side-effect | Methods | Behavior |
|:------------|:--------|:---------|
| `read-only` | `GET`, `HEAD`, `OPTIONS`, `TRACE` | runs without ceremony |
| `write` | `POST`, `PUT`, `PATCH` | runs without ceremony |
| `destructive` | `DELETE` (or `x-side-effect: destructive`) | requires `--yes` / `-y` |
```bash
$ petstore delete-pet 42
safety: Destructive operation "deletePet" requires consent. Pass --yes (CLI) or allowDestructive: true (programmatic).
$ echo $?
3
$ petstore delete-pet 42 --yes # consent given, fires the DELETE
$ petstore delete-pet 42 --yes --dry-run # preview the DELETE, exit 0
```
Two environment variables act as hard floors — they are checked inside `executeOperation`, so they apply to programmatic callers too, not only to the CLI surface:
| Variable | Effect |
|:---------|:-------|
| `DYNAMIC_OPENAPI_DRY_RUN=1` | Forces dry-run for **every** request, ignoring flags. Safe-mode for CI smoke tests against a production spec. |
| `DYNAMIC_OPENAPI_NO_DESTRUCTIVE=1` | Rejects destructive operations even when `--yes` / `allowDestructive` is set. Absolute floor for read-only CI runners. |
Override the classification at the spec level with vendor extensions:
```yaml
paths:
/search:
post:
operationId: searchThings
x-side-effect: read-only # POST that only reads — no --yes needed
/admin/wipe:
get:
operationId: wipeEverything
x-destructive: true # GET that's actually destructive — --yes required
```
Resolution order: `x-side-effect` > `x-destructive` sugar > HTTP method default.
### Piping bodies: `--body=-`, `--body-file`, and `@path`

@@ -316,2 +359,3 @@

| `2` | Validation error or HTTP 4xx |
| `3` | Safety check failed (destructive op without `--yes`, or env-var floor blocked it) |

@@ -558,2 +602,3 @@ ---

--dry-run Print the equivalent curl command instead of firing the request
-y, --yes Consent to destructive operations (DELETE / x-side-effect: destructive)

@@ -589,2 +634,4 @@ Request body (for operations that accept one):

| `OPENAPI_OAUTH2_REDIRECT_URI` | Full redirect URI override |
| `DYNAMIC_OPENAPI_DRY_RUN` | When `1`, every request renders the curl equivalent and exits without firing |
| `DYNAMIC_OPENAPI_NO_DESTRUCTIVE` | When `1`, destructive operations are rejected even when `--yes` is set |

@@ -591,0 +638,0 @@ ---

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display