Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
with:
bun-version: "1.4.2"

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}

Expand Down Expand Up @@ -139,7 +139,7 @@ jobs:
VERSION: ${{ github.ref_name }}

- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ matrix.outfile }}
path: dist/${{ matrix.outfile }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/install-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
with:
persist-credentials: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/live-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:

- run: bun install --frozen-lockfile

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "22"

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/reconcile-openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ jobs:
- name: Create pull-request token
if: steps.changes.outputs.changed == 'true'
id: app-token
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
app-id: ${{ secrets.CB_SDK_BOT_APP_ID }}
private-key: ${{ secrets.CB_SDK_BOT_PEM_KEY }}
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
- name: Create release PR approval token
if: steps.rp.outputs.release_created != 'true'
id: release-approval-token
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
app-id: ${{ secrets.CB_SDK_BOT_APP_ID }}
private-key: ${{ secrets.CB_SDK_BOT_PEM_KEY }}
Expand Down Expand Up @@ -186,7 +186,7 @@ jobs:
VERSION: ${{ needs.release-please.outputs.tag_name }}

- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ matrix.outfile }}
path: dist/${{ matrix.outfile }}
Expand All @@ -202,7 +202,7 @@ jobs:
with:
persist-credentials: false

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
path: dist/
merge-multiple: true
Expand Down Expand Up @@ -235,7 +235,7 @@ jobs:
with:
persist-credentials: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "22.23.2"
registry-url: "https://registry.npmjs.org"
Expand Down Expand Up @@ -309,7 +309,7 @@ jobs:
with:
persist-credentials: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "22.23.2"
registry-url: "https://registry.npmjs.org"
Expand Down Expand Up @@ -421,7 +421,7 @@ jobs:
with:
persist-credentials: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Do not introduce additional credential storage paths without an explicit require

Generated API commands enforce two protections:

- **Write gate:** Write operations are blocked on live sites.
- **Write gate:** Write operations on live sites require confirmation: an interactive yes/no prompt, or `-y/--yes`. Without a terminal (or on a CI runner, or in `--json` mode) and without `--yes`, the write is refused with exit code 6. Test sites, reads, and non-mutating POSTs (estimates, exports, hosted-page URLs, PDF downloads) never prompt.
- **Catalog gate:** Product Catalog-specific operations are blocked when they do not match the configured site's catalog version.

For `--code-sample`, the **write gate does not apply** because no API request is executed. The **catalog gate still applies** so generated code matches the site's Product Catalog version.
Expand Down Expand Up @@ -146,7 +146,7 @@ Run live tests with:
bun run test:live
```

Never permit successful writes on live Chargebee sites in automated tests. Live-site customer-create tests must assert the CLI write gate refuses the operation; use a dedicated validation site and read-only key. Successful remote writes are limited to customer creation and cleanup deletion on test sites.
Never permit successful writes on live Chargebee sites in automated tests, and never pass `--yes` to a live-site write in live tests. Live-site customer-create tests must assert the CLI write gate refuses the unconfirmed operation; use a dedicated validation site and read-only key. Successful remote writes are limited to customer creation and cleanup deletion on test sites.

Shared test helpers belong in `src/lib/test-support/` and must never be imported by production entry points or commands.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export CHARGEBEE_API_KEY=test_xxxxxxxxxxxx

Create API keys from **Settings → Configure Chargebee → API Keys** in the Chargebee Dashboard.

> Write operations are blocked on live sites. Use a test site when building or testing integrations.
> Write operations on live sites ask for confirmation before they run. In scripts, CI, or `--json` mode, add `--yes` to confirm; without it the command exits with code `6`. Use a test site when building or testing integrations.

## Quick Example

Expand Down
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"devDependencies": {
"@chargebee/code-sample-generator": "1.1.1",
"@clack/prompts": "^1.8.1",
"@types/bun": "1.4.2",
"@types/bun": "1.4.4",
"chargebee": "^3.34.0",
"commander": "^15.0.0",
"typescript": "^7.0.2"
Expand Down
6 changes: 3 additions & 3 deletions src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,9 @@ ${sectionTitle("EXAMPLES")}
chargebee auth add Connect a site interactively
chargebee auth add --profile staging Save credentials as staging

Writes are blocked on live sites: a site whose name doesn't end in -test,
unless its API key starts with test_. Prefer CHARGEBEE_API_KEY over --api-key
so the secret is not stored in shell history.
Writes on live sites ask for confirmation (or need --yes): a site whose name
doesn't end in -test, unless its API key starts with test_. Prefer
CHARGEBEE_API_KEY over --api-key so the secret is not stored in shell history.

--region (${REGIONS.join(" | ")}) records where the site is hosted and is used to
reach the regional webhook tunnel for \`chargebee listen\`. API requests are
Expand Down
2 changes: 1 addition & 1 deletion src/commands/generated/.openapi-source-sha
Original file line number Diff line number Diff line change
@@ -1 +1 @@
62be342fa9029a5a4a80826fb63b8d1ca6dfd8c5
480b6b1a9a3a38b42ea8d8ea476c5f579b2eb0ae
25 changes: 15 additions & 10 deletions src/commands/generated/addon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ export function registerAddon(parent: Command): void {
.option("-d, --data <pairs...>", "Request parameters in key=value format; repeat for multiple fields")
.option("-s, --code-sample <lang>", "Generate code sample (curl, python, nodejs, go, ruby, java, php, dotnet, list)")
.addOption(new Option("--pc-version <version>", "Product catalog version for the code sample (v1 or v2)").hideHelp())
.option("-y, --yes", "Skip the confirmation prompt for writes on a live site")
.addHelpText("after", "\n\u001b[1mDOCUMENTATION\u001b[0m\n chargebee docs addon copy\n")
.action(async (json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string }, command: Command) => {
.action(async (json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string; yes?: boolean }, command: Command) => {
const fromStdin = readJsonMarker(json, command);
const params = await loadOperationParams(opts.data ?? [], fromStdin, command, { method: "POST", jsonInput: false, opIdV2: "", opIdV1: "copy_an_addon", pcVersionFlag: opts.pcVersion });
if (opts.codeSample !== "list") await ensureCatalogAllowed("pc1", "addon copy");
if (opts.codeSample) return handleCodeSample({ lang: opts.codeSample, opIdV2: "", opIdV1: "copy_an_addon", method: "POST", uri: "/addons/copy", dataFlags: opts.data ?? [], params, pcVersionFlag: opts.pcVersion });
await ensureWriteAllowed("POST");
await ensureWriteAllowed("POST", "addon copy", opts.yes === true);
try {
const client = await getClient();
const result = await (client as any).addon.copy(params);
Expand All @@ -44,13 +45,14 @@ export function registerAddon(parent: Command): void {
.option("-d, --data <pairs...>", "Request parameters in key=value format; repeat for multiple fields")
.option("-s, --code-sample <lang>", "Generate code sample (curl, python, nodejs, go, ruby, java, php, dotnet, list)")
.addOption(new Option("--pc-version <version>", "Product catalog version for the code sample (v1 or v2)").hideHelp())
.option("-y, --yes", "Skip the confirmation prompt for writes on a live site")
.addHelpText("after", "\n\u001b[1mDOCUMENTATION\u001b[0m\n chargebee docs addon create\n")
.action(async (json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string }, command: Command) => {
.action(async (json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string; yes?: boolean }, command: Command) => {
const fromStdin = readJsonMarker(json, command);
const params = await loadOperationParams(opts.data ?? [], fromStdin, command, { method: "POST", jsonInput: false, opIdV2: "", opIdV1: "create_an_addon", pcVersionFlag: opts.pcVersion });
if (opts.codeSample !== "list") await ensureCatalogAllowed("pc1", "addon create");
if (opts.codeSample) return handleCodeSample({ lang: opts.codeSample, opIdV2: "", opIdV1: "create_an_addon", method: "POST", uri: "/addons", dataFlags: opts.data ?? [], params, pcVersionFlag: opts.pcVersion });
await ensureWriteAllowed("POST");
await ensureWriteAllowed("POST", "addon create", opts.yes === true);
try {
const client = await getClient();
const result = await (client as any).addon.create(params);
Expand All @@ -66,15 +68,16 @@ export function registerAddon(parent: Command): void {
.option("-d, --data <pairs...>", "Request parameters in key=value format; repeat for multiple fields")
.option("-s, --code-sample <lang>", "Generate code sample (curl, python, nodejs, go, ruby, java, php, dotnet, list)")
.addOption(new Option("--pc-version <version>", "Product catalog version for the code sample (v1 or v2)").hideHelp())
.option("-y, --yes", "Skip the confirmation prompt for writes on a live site")
.addHelpText("after", "\n\u001b[1mDOCUMENTATION\u001b[0m\n chargebee docs addon delete\n")
.action(async (id: string | undefined, json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string }, command: Command) => {
.action(async (id: string | undefined, json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string; yes?: boolean }, command: Command) => {
const resource = takeResourceId(id, json, command);
const params = await loadOperationParams(opts.data ?? [], resource.fromStdin, command, { method: "POST", jsonInput: false, opIdV2: "", opIdV1: "delete_an_addon", pcVersionFlag: opts.pcVersion });
if (opts.codeSample !== "list") await ensureCatalogAllowed("pc1", "addon delete");
if (opts.codeSample) return handleCodeSample({ lang: opts.codeSample, opIdV2: "", opIdV1: "delete_an_addon", method: "POST", uri: "/addons/{id}/delete", dataFlags: opts.data ?? [], params, resourceId: resource.id, pathParamName: "addon-id", pcVersionFlag: opts.pcVersion });
if (!resource.id) command.error("error: missing required argument 'addon-id'");
assertResourceId(resource.id, command);
await ensureWriteAllowed("POST");
await ensureWriteAllowed("POST", "addon delete", opts.yes === true);
try {
const client = await getClient();
const result = await (client as any).addon.delete(resource.id, params);
Expand Down Expand Up @@ -136,15 +139,16 @@ export function registerAddon(parent: Command): void {
.option("-d, --data <pairs...>", "Request parameters in key=value format; repeat for multiple fields")
.option("-s, --code-sample <lang>", "Generate code sample (curl, python, nodejs, go, ruby, java, php, dotnet, list)")
.addOption(new Option("--pc-version <version>", "Product catalog version for the code sample (v1 or v2)").hideHelp())
.option("-y, --yes", "Skip the confirmation prompt for writes on a live site")
.addHelpText("after", "\n\u001b[1mDOCUMENTATION\u001b[0m\n chargebee docs addon unarchive\n")
.action(async (id: string | undefined, json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string }, command: Command) => {
.action(async (id: string | undefined, json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string; yes?: boolean }, command: Command) => {
const resource = takeResourceId(id, json, command);
const params = await loadOperationParams(opts.data ?? [], resource.fromStdin, command, { method: "POST", jsonInput: false, opIdV2: "", opIdV1: "unarchive_an_addon", pcVersionFlag: opts.pcVersion });
if (opts.codeSample !== "list") await ensureCatalogAllowed("pc1", "addon unarchive");
if (opts.codeSample) return handleCodeSample({ lang: opts.codeSample, opIdV2: "", opIdV1: "unarchive_an_addon", method: "POST", uri: "/addons/{id}/unarchive", dataFlags: opts.data ?? [], params, resourceId: resource.id, pathParamName: "addon-id", pcVersionFlag: opts.pcVersion });
if (!resource.id) command.error("error: missing required argument 'addon-id'");
assertResourceId(resource.id, command);
await ensureWriteAllowed("POST");
await ensureWriteAllowed("POST", "addon unarchive", opts.yes === true);
try {
const client = await getClient();
const result = await (client as any).addon.unarchive(resource.id, params);
Expand All @@ -160,15 +164,16 @@ export function registerAddon(parent: Command): void {
.option("-d, --data <pairs...>", "Request parameters in key=value format; repeat for multiple fields")
.option("-s, --code-sample <lang>", "Generate code sample (curl, python, nodejs, go, ruby, java, php, dotnet, list)")
.addOption(new Option("--pc-version <version>", "Product catalog version for the code sample (v1 or v2)").hideHelp())
.option("-y, --yes", "Skip the confirmation prompt for writes on a live site")
.addHelpText("after", "\n\u001b[1mDOCUMENTATION\u001b[0m\n chargebee docs addon update\n")
.action(async (id: string | undefined, json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string }, command: Command) => {
.action(async (id: string | undefined, json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string; yes?: boolean }, command: Command) => {
const resource = takeResourceId(id, json, command);
const params = await loadOperationParams(opts.data ?? [], resource.fromStdin, command, { method: "POST", jsonInput: false, opIdV2: "", opIdV1: "update_an_addon", pcVersionFlag: opts.pcVersion });
if (opts.codeSample !== "list") await ensureCatalogAllowed("pc1", "addon update");
if (opts.codeSample) return handleCodeSample({ lang: opts.codeSample, opIdV2: "", opIdV1: "update_an_addon", method: "POST", uri: "/addons/{id}", dataFlags: opts.data ?? [], params, resourceId: resource.id, pathParamName: "addon-id", pcVersionFlag: opts.pcVersion });
if (!resource.id) command.error("error: missing required argument 'addon-id'");
assertResourceId(resource.id, command);
await ensureWriteAllowed("POST");
await ensureWriteAllowed("POST", "addon update", opts.yes === true);
try {
const client = await getClient();
const result = await (client as any).addon.update(resource.id, params);
Expand Down
5 changes: 3 additions & 2 deletions src/commands/generated/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ export function registerAddress(parent: Command): void {
.option("-d, --data <pairs...>", "Request parameters in key=value format; repeat for multiple fields")
.option("-s, --code-sample <lang>", "Generate code sample (curl, python, nodejs, go, ruby, java, php, dotnet, list)")
.addOption(new Option("--pc-version <version>", "Product catalog version for the code sample (v1 or v2)").hideHelp())
.option("-y, --yes", "Skip the confirmation prompt for writes on a live site")
.addHelpText("after", "\n\u001b[1mDOCUMENTATION\u001b[0m\n chargebee docs address update\n")
.action(async (json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string }, command: Command) => {
.action(async (json: string | undefined, opts: { data?: string[]; codeSample?: string; pcVersion?: string; yes?: boolean }, command: Command) => {
const fromStdin = readJsonMarker(json, command);
const params = await loadOperationParams(opts.data ?? [], fromStdin, command, { method: "POST", jsonInput: false, opIdV2: "update_an_address", opIdV1: "update_an_address", pcVersionFlag: opts.pcVersion });
if (opts.codeSample) return handleCodeSample({ lang: opts.codeSample, opIdV2: "update_an_address", opIdV1: "update_an_address", method: "POST", uri: "/addresses", dataFlags: opts.data ?? [], params, pcVersionFlag: opts.pcVersion });
await ensureWriteAllowed("POST");
await ensureWriteAllowed("POST", "address update", opts.yes === true);
try {
const client = await getClient();
const result = await (client as any).address.update(params);
Expand Down
Loading
Loading