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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `bitmovin account organizations list` shows every visible organization with its `type` (`ROOT_ORGANIZATION` / `SUB_ORGANIZATION`), `parentId`, and whether it is the active one, ordering sub-organizations directly under their parent. `--type root|sub` and `--parent <org-id>` narrow the listing.
- `bitmovin support tickets list | get | create | comment` for Bitmovin support tickets. `--organization <org-id>` (alias `--tenant-org`) scopes any of them to a sub-organization via `X-Tenant-Org-Id`, and `create` always sends a body `organizationId` matching that header because the API rejects a mismatch.
- `create` and `comment` are irreversible: both print the exact payload to stderr — always, including under `--json`, so a scripted run still records what was filed and against which organization — state that Bitmovin support engineers will see it and that it cannot be withdrawn via the API, and require an explicit confirmation. `--yes` / `--confirm` skips the prompt and is required for non-interactive use — without a TTY (or in `--json` mode) the commands refuse to send instead of silently filing a ticket.
- `comment` reads the ticket's `modifiedAt` and sends it as the API-required `updatedStamp` collision stamp, so callers never see the misleading `1004 … Check your JSON syntax` error that a missing stamp produces. The ticket's newest comment is shown in the confirmation, so the stamp corresponds to the state the user actually saw.
- `list` rejects an `--offset` that is not `0` or a multiple of `--limit` (the API silently serves an earlier page otherwise), plus invalid `--search` text, filter values, and `--sort` expressions, before making a request. Filter spacing is normalized, since the API splits on `,` without trimming.
- `get` hides attachment download URLs unless `--show-secrets` is passed: the URL alone grants access to the file to anyone holding the link. Ticket subjects and comment bodies are stripped of control characters before printing, so customer-supplied text cannot rewrite the rendered conversation or forge the `(Bitmovin)` agent attribution.
- An empty `--organization ""` is rejected rather than silently falling back, which would otherwise widen a write from the intended sub-organization to the credential's own organization.
- `--allow-file-access` requires `--category encoding`, and `--body-file` is capped at 65,536 characters with a head-and-tail preview. The API accepts a mismatched category and silently drops the field, so the check has to be local.

### Changed

- `bitmovin config list organizations` now derives sub-organizations from the `parentId` of the flat organization listing instead of calling the per-organization `sub-organizations` endpoint, which reports `1001 An organization with the given id does not exist` for organization ids that the listing returns. The JSON keys are unchanged, but rows are now deduplicated and ordered parent-first, so sub-organizations render under their parent instead of flat at top level.
- `--organization` (alias `--tenant-org`) is now `BaseCommand.tenantOrgFlag` and is honoured by SDK-backed commands as well as the REST-backed support commands: `getClient()` takes a tenant-organization override and `BaseCommand.getApi()` passes it. Previously the flag could only ever work for the support commands, so adding it to any other command would have parsed fine and been silently ignored.
- Credential and organization scope for a command is resolved once in `BaseCommand.requestScope()` instead of each command threading `--api-key` and the organization by hand, so a future credential-affecting base flag is wired up in one place. Tenant resolution moved to `lib/tenant.ts` as a pure function.
- The optional create-ticket fields are declared once in `CREATE_TICKET_FIELDS`, which generates both the oclif flags and the API payload mapping. They were previously listed three times (flags, interface, mapping) behind a cast that hid drift, so a field added to two of the three compiled and never reached the API.
- The destructive-action policy lives in `confirm.ts` as `confirmDestructive()` plus a shared `yesFlag`, instead of being duplicated in each write command. It returns a distinct `unconfirmable` outcome so "the user declined" and "nobody could be asked" keep different exit codes, and `encoding jobs delete`/`stop` can adopt it without inventing a fourth convention.
- Fixed `abbreviate(text, n, 0)` printing the entire text while labelling it truncated: `slice(-0)` is `slice(0)`. It was live in the comment confirmation, where a long support reply pushed the "PUBLIC comment" warning off screen — the opposite of what the head-and-tail preview exists for.
- The ticket-body bound applies to `--body` as well as `--body-file`, and organization listing has a page-count bound so a server that ignores `offset` fails instead of looping.
- `--sort createdAt:desc` is uppercased before it is sent; validation accepted it case-insensitively while the API silently ignored the direction.
- `409` now explains that the resource changed since it was read (the case the comment collision stamp exists to catch) instead of `API error: 409`, and network failures and the request timeout are reported in plain language — the timeout deliberately does not promise a retry is safe, since it fires after the request was sent.
- `config list organizations` no longer nests a sub-organization under an unrelated root when its real parent is not visible to the credential; it is listed at top level and labelled.
- Terminal sanitization extended to the comment confirmation's ticket subject and to the requester and organization names in `tickets get`.
- Organizations are paged through in full. The generated SDK's `organizations.list()` accepts no query parameters and so returned only the API's default first page — on a larger account, sub-organizations whose parent sat on a later page were rendered as roots, and `--parent` reported a visible organization as invisible. A short page while the API reports more now fails loudly rather than returning a truncated list.
- The `403 Access denied` hint now names the organization the failed request was actually scoped to (including one passed via `--organization`) and points at `bitmovin account organizations list`.

## [0.4.0] - 2026-05-26

### Added
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,19 @@ bitmovin encoding templates start ./my-encoding.yaml --watch
| `bitmovin encoding …` | Templates, jobs, inputs/outputs, codecs, manifests, stats |
| `bitmovin player …` | Player licenses, domains, analytics linking |
| `bitmovin analytics …` | Analytics licenses and domains |
| `bitmovin account info` | Account information |
| `bitmovin account …` | Account information, organizations and sub-organizations |
| `bitmovin support tickets …` | List, read, file, and comment on Bitmovin support tickets |

### Support tickets

`bitmovin support tickets create` and `bitmovin support tickets comment` write to
a **real** support ticket that Bitmovin support engineers see and that cannot be
withdrawn via the API. Both print the exact payload to stderr — including under
`--json`, so a scripted run still records what was filed — and require an explicit
confirmation; `--yes` skips the prompt for scripting. The ticket is filed as the
user behind your credentials, whose name and email appear on it.
`--organization <org-id>` targets a sub-organization (list them with
`bitmovin account organizations list`).

Every command documents itself with `--help`. The full command reference with
examples lives in [docs/commands.md](docs/commands.md), and `bitmovin skill`
Expand Down
117 changes: 117 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ for AI assistants.
- [Player](#player)
- [Analytics](#analytics)
- [Account](#account)
- [Support](#support)
- [Output Formats](#output-formats)
- [OAuth Details](#oauth-details)

Expand Down Expand Up @@ -173,6 +174,122 @@ bitmovin analytics domains remove <license-id-or-key-or-name> <domain-id>
bitmovin account info
```

### Organizations

Lists every organization your credentials can see — root organizations and their
sub-organizations in one listing, each row carrying `type`
(`ROOT_ORGANIZATION` / `SUB_ORGANIZATION`), `parentId`, and whether it is the
`active` one (the organization from `bitmovin config set organization`).
Sub-organizations are listed directly beneath their parent.

```bash
bitmovin account organizations list # Everything, parent-first
bitmovin account organizations list --type root # Only root organizations
bitmovin account organizations list --type sub # Only sub-organizations
bitmovin account organizations list --parent <org-id> # Only that parent's sub-organizations
bitmovin account organizations list --json --jq '.[] | select(.parentId != null) | .id'
```

`--type` and `--parent` cannot be combined, and `--parent` requires an organization
your credentials can see — an unknown id exits with an error rather than an empty
list.

This command takes no `--organization`: the listing is scoped by the credentials
themselves, not by `X-Tenant-Org-Id`, so the flag would have no effect. Use
`--parent <org-id>` to narrow it to one organization's sub-organizations.

The hierarchy is derived from the `parentId` of the flat organization listing, which
the CLI pages through in full. The API's per-organization `sub-organizations`
endpoint is deliberately not used: it answers `1001 An organization with the given
id does not exist` for organization ids that the listing plainly returns.

## Support

Support tickets filed with Bitmovin support (the same tickets you see in the
[dashboard](https://dashboard.bitmovin.com)).

```bash
bitmovin support tickets list
bitmovin support tickets list --status open,pending --sort createdAt:DESC
bitmovin support tickets list --category encoding --severity high,medium
bitmovin support tickets list --search "encoding fails"
bitmovin support tickets list --limit 50 --offset 50

bitmovin support tickets get <case-id>

bitmovin support tickets create --category encoding \
--subject "Encoding stuck at 40%" --body "Encoding abc123 does not progress." \
--encoding-id abc123

bitmovin support tickets comment <case-id> --body "Still reproducible on 8.150.0."
```

**Creating a ticket and commenting are irreversible.** Both commands print the
exact payload **to stderr** — always, including under `--json`, so a scripted run
still records what was filed and against which organization — warn that Bitmovin
support engineers will see it and that it cannot be withdrawn via the API, and then
ask for an explicit confirmation. `--yes` (alias `--confirm`) skips the prompt and
is **required** for non-interactive use — without a TTY, or in `--json` mode, the
commands refuse to send anything instead of silently filing a ticket.

The ticket is filed as **the user behind your credentials**, not as the
organization: the API resolves the requester from the authenticated user and that
person's name and email appear on the ticket. A credential that carries no user
identity (some machine keys) is rejected by the API with `400`.

A long `--body-file` is previewed head-and-tail with its character count rather
than in full, so the warning above stays on screen; the body is capped at 65,536
characters.

| Flag | Applies to | Description |
|------|-----------|-------------|
| `--organization <org-id>` (alias `--tenant-org`) | all | Organization to act on, sent as `X-Tenant-Org-Id`. Defaults to `bitmovin config set organization`; omit both to use the organization of your credentials. For `create`, the body's `organizationId` is set to the same value whenever an organization is resolved — the API rejects a mismatch. An **empty** value (`--organization ""`, e.g. an unset shell variable) is rejected rather than quietly falling back, and `bitmovin config set organization ""` is refused for the same reason. |
| `--body <text>` / `--body-file <path>` | `create`, `comment` | Ticket / comment text, inline or from a file. |
| `--html` | `comment` | Send the comment as HTML. By default plain text is escaped and its line breaks are preserved. |
| `--yes` / `-y` | `create`, `comment` | Confirm non-interactively. |
| `--limit` / `--offset` | `list` | Page size (1–100, default 25) and offset (default 0). The offset must be `0` or a multiple of `--limit`; other values make the API silently return an earlier page, so the CLI rejects them. |
| `--show-secrets` | `get` | Print attachment download URLs. They are hidden by default — in `--json` output too, where the `url` field carries a placeholder instead — because the URL alone grants access to the file to anyone holding the link. Comment text itself is printed as authored, so a link someone wrote (or an inline image in `htmlBody`) is shown either way. |
| `--status`, `--category`, `--priority`, `--severity` | `list` | Comma-separated filters. Status: `new, open, pending, hold, solved, closed, deleted`. Category: `encoding, player, analytics, other`. Priority: `blocker, high, medium, low`. Severity: `high, medium, low, minor`. |
| `--search <text>` | `list` | Full-text search, max 100 characters, letters/digits/spaces only (the API rejects punctuation). |
| `--sort <expr>` | `list` | `createdAt` or `modifiedAt`, optionally `:ASC` / `:DESC`. |

`create` requires `--category` (`encoding`, `player`, `analytics`, `other`) and a
body. Some fields are category-gated: `--encoding-id` and `--allow-file-access`
require `--category encoding`, while `--license` and `--page-url` require
`--category player` or `--category analytics`. The CLI rejects a mismatch, because
the API does **not** — it accepts the request and silently drops the field, so a
ticket would look filed while the data never arrived. Run
`bitmovin support tickets create --help` for the full field list
(`--platform`, `--sdk-version`, `--os-details`, `--device-details`,
`--request-type`, `--reproducible-reliably`, …).

Some ticket fields the API accepts have no CLI flag: `collaborators`,
`businessImpact`, `streamId`, `analyticsUserSessionUrl`,
`analyticsCollectorVersion`, `playerConfig`, `playerSourceConfig` and
`playerWorkingVersion`. Use the dashboard when a ticket needs those — support
engineers routinely ask for the player config and version.

`comment` posts a **public** reply. It first reads the ticket to obtain its
`modifiedAt` and sends it as the required `updatedStamp`, which is how the API
detects a concurrent update — so you never have to pass a timestamp yourself. The
ticket's newest comment is shown in the confirmation, so you are replying to the
state the stamp was taken from: if support replies while you are deciding, the API
rejects the post rather than accepting a reply written against stale information.

`list` ordering: with no `--sort` and no filter, the API lists tickets awaiting your
reply first, then newest first — and in that mode its total counts only those, so
the CLI reports the range without a total. Pass `--sort createdAt:DESC` (or any
filter) for a strict ordering and an exact total. `--json` emits just the ticket
array; page by requesting successive offsets until a page returns fewer items than
`--limit`.

If a sub-organization is not granted to your credentials, the API answers
`1003 Access denied`; the CLI reports which organization was targeted and points
at `bitmovin account organizations list`.

Attachments (`uploads`) are not supported by the CLI yet — use the dashboard for
those.

## Output Formats

By default, the CLI outputs human-readable tables when used interactively. For scripting and automation, use `--json` and `--jq`:
Expand Down
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@
},
"account": {
"description": "Account information"
},
"account:organizations": {
"description": "Organizations and sub-organizations"
},
"support": {
"description": "Bitmovin support"
},
"support:tickets": {
"description": "Support ticket management"
}
}
}
Expand Down
78 changes: 78 additions & 0 deletions src/commands/account/organizations/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {Flags} from '@oclif/core';
import chalk from 'chalk';
import {BaseCommand} from '../../../lib/base-command.js';
import {loadConfig} from '../../../lib/config.js';
import {
ORGANIZATION_COLUMNS,
ROOT_ORGANIZATION,
SUB_ORGANIZATION,
listOrganizations,
toOrganizationRows,
} from '../../../lib/organizations.js';

export default class AccountOrganizationsList extends BaseCommand {
static override description =
'List organizations visible to your credentials. Sub-organizations are listed under their parent, with type and parentId shown.';

// Deliberately no `tenantOrgFlag`: `/account/organizations` lists what the
// credential can see and is not scoped by X-Tenant-Org-Id, so `--organization`
// would be declared and then ignored — and, because the flag rejects an empty
// value, `--organization "$UNSET_VAR"` would abort a read-only listing over a
// no-op. Sub-organizations are selected with `--parent` instead.
static override flags = {
...BaseCommand.baseFlags,
parent: Flags.string({
description: 'Show only the sub-organizations of this organization',
helpValue: '<org-id>',
exclusive: ['type'],
}),
type: Flags.string({
description: 'Show only root organizations or only sub-organizations',
options: ['root', 'sub'],
}),
};

static override examples = [
'bitmovin account organizations list',
'bitmovin account organizations list --type sub',
'bitmovin account organizations list --parent 8a7b6c5d-1234-5678-9abc-def012345678',
'bitmovin account organizations list --json --jq ".[] | select(.type == \\"SUB_ORGANIZATION\\") | .id"',
];

async run(): Promise<void> {
const {flags} = await this.parse(AccountOrganizationsList);
const config = loadConfig();
const scope = await this.requestScope();
const orgs = await listOrganizations(scope.apiKey);
Comment thread
SInCE marked this conversation as resolved.
const rows = toOrganizationRows(orgs, config.tenantOrgId);

let selected = rows;
if (flags.parent) {
if (!rows.some((row) => row.id === flags.parent)) {
this.error(
`Organization ${flags.parent} is not visible to these credentials.\n` +
' Run `bitmovin account organizations list` to see the organizations you can access.',
{exit: 2},
);
}

selected = rows.filter((row) => row.parentId === flags.parent);
} else if (flags.type) {
// Filter on the same `type` value the table shows and the --jq example
// selects on, not on parentId — an org the API types as SUB_ORGANIZATION
// without a parentId would otherwise be filtered as a root while its own
// type cell read SUB_ORGANIZATION.
const wanted = flags.type === 'sub' ? SUB_ORGANIZATION : ROOT_ORGANIZATION;
selected = rows.filter((row) => row.type === wanted);
}

await this.outputList(selected, ORGANIZATION_COLUMNS);

if (!(await this.isJsonMode()) && !flags.quiet && selected.length > 0) {
this.log('');
this.log(chalk.dim('Set the default organization: bitmovin config set organization <id>'));
// Named with a command that actually accepts the flag — this one does not.
this.log(chalk.dim('Target one for a single command: bitmovin support tickets list --organization <id>'));
}
}
}
5 changes: 2 additions & 3 deletions src/commands/analytics/domains/list.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Args, Flags} from '@oclif/core';
import {Args} from '@oclif/core';
import {BaseCommand} from '../../../lib/base-command.js';
import {resolveAnalyticsLicense} from '../../../lib/resolve-license.js';

Expand All @@ -11,8 +11,7 @@ export default class AnalyticsDomainList extends BaseCommand {

static override flags = {
...BaseCommand.baseFlags,
limit: Flags.integer({description: 'Max results', default: 25}),
offset: Flags.integer({description: 'Offset for pagination', default: 0}),
...BaseCommand.paginationFlags(),
};

async run(): Promise<void> {
Expand Down
Loading
Loading