diff --git a/docs/_includes/plugin-flow.html b/docs/_includes/plugin-flow.html new file mode 100644 index 000000000..407366223 --- /dev/null +++ b/docs/_includes/plugin-flow.html @@ -0,0 +1,531 @@ +
+ + +
+
+ + +
+
+ + + +
+
+
+ + +
+
+

+
+ + + + + + + +
+
+
+

+

+
+
+
+

Keyboard: → next, ← previous, Space play or pause, + R restart. Play stops by itself at the last step. +

+ + +
diff --git a/docs/assets/js/plugin-flow.js b/docs/assets/js/plugin-flow.js new file mode 100644 index 000000000..fe56e577a --- /dev/null +++ b/docs/assets/js/plugin-flow.js @@ -0,0 +1,1908 @@ + (function () { + 'use strict'; + // --------------------------------------------------------------------------- + // Components. Each one sits in a zone (machine, services, auth0) at a column + // and a row of a virtual grid. A step shows only the components it uses, and + // the empty columns and rows are squeezed out when the step is drawn. + // trust: trusted | untrusted | neutral. parent: the CLI box a check lives in. + // --------------------------------------------------------------------------- + var NODES = { + user: { name: 'User / Agent', sub: 'person or AI agent', trust: 'neutral', zone: 'machine', col: 0, row: 0 }, + project: { name: 'Plugin project', sub: 'plugin.json + build', trust: 'untrusted', zone: 'machine', col: 0, row: 1 }, + cli: { name: 'auth0 CLI', trust: 'trusted', zone: 'machine', col: 1, container: true }, + cache: { name: 'Registry cache', sub: 'index.json + .sig', trust: 'trusted', zone: 'machine', col: 2, row: 0 }, + approvals: { name: 'Approvals file', sub: 'approvals.json', trust: 'trusted', zone: 'machine', col: 2, row: 1 }, + store: { name: 'Store + lockfile', sub: 'store/ + plugins.lock.json', trust: 'trusted', zone: 'machine', col: 2, row: 2 }, + devlinks: { name: 'Dev links file', sub: 'dev-links.json', trust: 'trusted', zone: 'machine', col: 2, row: 3 }, + proxy: { name: 'Loopback proxy', sub: 'per-run cert + secret', trust: 'trusted', zone: 'machine', col: 2, row: 0 }, + plugin: { name: 'Plugin process', sub: 'untrusted, runs as you', trust: 'untrusted', zone: 'machine', col: 2, row: 1 }, + repo: { name: 'Plugin repo', sub: 'GitHub', trust: 'untrusted', zone: 'services', col: 3, row: 1 }, + actions: { name: 'Release workflow', sub: 'GitHub Actions', trust: 'untrusted', zone: 'services', col: 4, row: 1 }, + release: { name: 'GitHub release', sub: 'archives + checksums', trust: 'untrusted', zone: 'services', col: 5, row: 1 }, + registry: { name: 'Signed registry', sub: 'index.json + .sig', trust: 'trusted', zone: 'services', col: 5, row: 0 }, + mgmt: { name: 'Management API', sub: 'acme.us.auth0.com', trust: 'neutral', zone: 'auth0', col: 6, row: 0 }, + 'c-prescan': { name: 'Pre-scan flags', parent: 'cli', trust: 'trusted', chip: true }, + 'c-verify': { name: 'Verify binary', parent: 'cli', trust: 'trusted', chip: true }, + 'c-registry': { name: 'Registry check', parent: 'cli', trust: 'trusted', chip: true }, + 'c-call': { name: 'Call check', parent: 'cli', trust: 'trusted', chip: true }, + 'c-approval': { name: 'Approval check', parent: 'cli', trust: 'trusted', chip: true }, + 'c-session': { name: 'Session / login', parent: 'cli', trust: 'trusted', chip: true }, + 'c-proxy': { name: 'Proxy start', parent: 'cli', trust: 'trusted', chip: true }, + 'c-spawn': { name: 'Spawn plugin', parent: 'cli', trust: 'trusted', chip: true }, + 'c-consent': { name: 'Consent screen', parent: 'cli', trust: 'trusted', chip: true }, + 'c-scaffold': { name: 'Scaffold project', parent: 'cli', trust: 'trusted', chip: true }, + 'c-manifest': { name: 'Manifest check', parent: 'cli', trust: 'trusted', chip: true }, + 'c-link': { name: 'Link / unlink', parent: 'cli', trust: 'trusted', chip: true } + }; + var ZONE_ORDER = ['machine', 'services', 'auth0']; + var ZONE_LABEL = { machine: 'Your machine', services: 'Trusted services', auth0: 'Auth0' }; + // Links between components. Only the ones whose two ends are both on screen are drawn. + var EDGES = [ + ['repo', 'actions'], ['actions', 'release'], ['project', 'repo'], ['cli', 'registry'], ['cli', 'release'], + ['user', 'cli'], ['user', 'project'], ['cli', 'project'], ['cli', 'devlinks'], ['cli', 'cache'], + ['cli', 'store'], ['cli', 'approvals'], ['cli', 'proxy'], ['cli', 'plugin'], ['proxy', 'plugin'], ['proxy', 'mgmt'] + ]; + // --------------------------------------------------------------------------- + // Helpers for the data boxes. A line is a string or {k, t}. + // --------------------------------------------------------------------------- + function ok(t) { return { k: 'ok', t: t }; } + function no(t) { return { k: 'no', t: t }; } + function del(t) { return { k: 'del', t: t }; } + function add(t) { return { k: 'add', t: t }; } + function hl(t) { return { k: 'hl', t: t }; } + function note(t) { return { k: 'note', t: t }; } + var TENANT = 'acme.us.auth0.com'; + var OTHER = 'example.us.auth0.com'; + var APPROVE = 'auth0 plugin approve auth0/checkmate --tenant '; + var RUN_CMD = 'auth0 checkmate --validators a --tenant ' + TENANT; + var DEV = 'AUTH0_CLI_PLUGIN_DEV_MODE=true '; + var NOTICE = 'Each command runs with only the permissions it needs. Hosts and files are declared by the publisher and are not enforced by the CLI.'; + var FIRST_SCREEN = [ + 'This plugin asks to use your Auth0 tenant ' + TENANT + ':', + '', + ' Can read clients, connections, tenant settings', + ' Hosts api.github.com Check SDK versions against GitHub advisories', + ' Files $CWD (write) Save the audit report', + ' Interactive no', + '', + NOTICE, + '', + 'Approve for ' + TENANT + '? [y/N/d] (d shows the exact scopes) y' + ]; + var FETCH_NOTE = 'Install and update need a current index. If it cannot be refreshed and the saved copy is out of date, the command fails and says what to run once you are online.'; + var AGENT_NOTE = 'In agent mode the CLI prints no human lines (they would be on stderr). The result is JSON on stdout and a failure is one JSON error on stderr.'; + // --------------------------------------------------------------------------- + // Shared steps (the same object is reused by several scenarios). + // --------------------------------------------------------------------------- + var RUN_START = { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: RUN_CMD, + title: 'A person or an agent runs the plugin command', + text: 'Installed plugins appear as normal auth0 commands. The CLI sees the whole command line first, so it can decide what is safe before any plugin code starts.', + data: [{ label: 'Command line', lines: ['$ ' + RUN_CMD] }] + }; + var RUN_PRESCAN = { + actors: ['c-prescan'], from: 'c-prescan', to: 'c-prescan', + title: 'Pre-scan removes the CLI-owned flags', + text: 'The CLI takes out only the flags it owns, such as `--tenant`, and applies them itself. Everything else goes to the plugin untouched, so a plugin flag can never shadow a CLI flag.', + data: [{ + label: 'Arguments', lines: [ + 'before: --validators a --tenant ' + TENANT, + del('--tenant ' + TENANT), + hl('plugin gets: --validators a'), + ok('--tenant becomes AUTH0_PLUGIN_TENANT=' + TENANT), + note('Everything after a bare -- is passed as it was typed.')] + }] + }; + var RUN_VERIFY = { + actors: ['c-verify', 'store'], from: 'cli', to: 'store', label: 'read lockfile, hash the files', + title: 'Store.Verify checks the files against the lockfile', + text: 'Before anything else, the CLI hashes the executable and plugin.json on disk and compares them with the hashes it recorded at install time. This happens on every run, before any login, so a swapped binary never gets a token.', + data: [{ + label: 'Checks', lines: [ + ok('sha256 of the executable equals binSha256 in plugins.lock.json'), + ok('sha256 of plugin.json equals manifestSha256'), + ok('real files only: no symlinks, nothing others can write to, a single hard link'), + note('A second check runs after the session is open. If anything changed in between, the run stops.')] + }] + }; + var RUN_REGISTRY = { + actors: ['c-registry', 'cache'], from: 'cli', to: 'cache', label: 'load the verified index', + title: 'Index check: not revoked, still usable', + text: 'The CLI reads its cached index, which was verified when it was saved. Revocation is checked here, so a withdrawn version stops as soon as the CLI has seen the new index.', + data: [{ + label: 'Checks', lines: [ + ok('auth0/checkmate 1.0.0 is not on the revocation list'), + ok('the index is not expired and is inside the 30 day offline grace period'), + note('A copy older than 24 hours is refreshed for up to 10 seconds. If that fails, the saved copy runs with a warning while it is still inside the grace period.')] + }] + }; + var RUN_CALLCHECK = { + actors: ['c-call'], from: 'c-call', to: 'c-call', + title: 'The command can run the way it was called', + text: 'The manifest says what a command supports. A command declared interactive needs a terminal, and `--json`, `--csv` and agent mode work only if the command declares them.', + data: [{ + label: 'Checks', lines: [ + ok('wizard-free command: not interactive, so no terminal is needed'), + ok('the output the caller asked for is declared in the manifest')] + }] + }; + var RUN_APPROVAL = { + actors: ['c-approval', 'approvals'], from: 'cli', to: 'approvals', label: 'tenant + plugin + permissions hash', + title: 'Approval check for this tenant and these permissions', + text: 'A person must have approved exactly these permissions on exactly this tenant. Nothing has been logged in yet, so a plugin that is not approved can never cause a token to be fetched.', + data: [{ + label: 'approvals.json lookup', lines: [ + 'tenant: ' + TENANT, + 'plugin: auth0/checkmate', + 'hash: consent hash of the installed manifest', + ok('the recorded hash equals the current hash'), + note('A pin in AUTH0_CLI_PLUGIN_APPROVALS also counts, see the CI flow.')] + }] + }; + var RUN_SESSION = { + actors: ['c-session'], from: 'c-session', to: 'c-session', + title: 'Only now: session and token', + text: 'The CLI uses the session it already has for this tenant, or logs in, and checks that it is the approved tenant. The access token stays in CLI memory. It is not written to the plugins folder and it is not handed to the plugin.', + data: [{ + label: 'Token handling', lines: [ + ok('token held in CLI memory for the proxy'), + no('not in argv, not in the plugin environment, not on disk'), + note('If the session is for another tenant, the run stops with plugin_tenant_mismatch.')] + }] + }; + var RUN_PROXY = { + actors: ['c-proxy', 'proxy'], from: 'cli', to: 'proxy', label: 'start with per-run cert + secret', + title: 'Start the loopback proxy for this run', + text: 'The proxy gets a fresh certificate and a random secret, and it is given only the scopes of the command being run. It denies by default, using a scope table and a secret field list that come from the signed index.', + data: [{ + label: 'Proxy for this run', lines: [ + 'listens on https://127.0.0.1:', + 'per-run TLS certificate (CA file) and a random secret', + hl('scopes: read:clients, read:connections, read:tenant_settings'), + note('Only the scopes of the command that runs, not the union shown on the consent screen.')] + }] + }; + var RUN_SPAWN = { + actors: ['c-spawn', 'plugin'], from: 'cli', to: 'plugin', label: 'spawn with a small environment', + title: 'Spawn the plugin with a small environment', + text: 'The plugin receives a short allowlist, the proxy address and the per-run secret, and nothing else. Its output goes straight to your terminal. It runs as you, in its own process group, with the timeout of the command (10m here).', + data: [ + { + label: 'Environment the plugin gets', lines: [ + ok('PATH, HOME, LANG, LC_ALL, LC_CTYPE, TERM, TMPDIR when set'), + ok('HTTP_PROXY, HTTPS_PROXY, NO_PROXY and the lower case forms; NO_PROXY also names loopback'), + ok('AUTH0_PLUGIN_DOMAIN=127.0.0.1: (the proxy, not your tenant)'), + ok('AUTH0_PLUGIN_MANAGEMENT_TOKEN='), + ok('AUTH0_PLUGIN_CA_CERT, SSL_CERT_FILE, NODE_EXTRA_CA_CERTS = '), + ok('AUTH0_PLUGIN_TENANT=' + TENANT), + ok('AUTH0_PLUGIN_JSON, _JSON_COMPACT, _CSV, _AGENT_MODE, _NO_INPUT, _NO_COLOR, _DEBUG'), + ok('variables the command declared in permissions.env (none for checkmate)'), + note('On Windows the list also has SystemRoot, TEMP, TMP, USERPROFILE, APPDATA, LOCALAPPDATA, PATHEXT and ComSpec.')] + }, + { + label: 'Environment it does NOT get', lines: [ + no('AUTH0_CLIENT_SECRET, AUTH0_API_TOKEN, AUTH0_DOMAIN'), + no('every other AUTH0_ name from your shell, even if a manifest asks for it'), + no('AWS_PROFILE and anything else not declared in permissions.env'), + no('the real access token'), + note('Plugins run as your user. The CLI limits the Auth0 calls they make, but they are not sandboxed, so they can read the files your user can read.')] + } + ] + }; + var RUN_API = { + actors: ['plugin', 'proxy'], from: 'plugin', to: 'proxy', label: 'GET /api/v2/clients (Bearer per-run secret)', + title: 'The plugin calls the proxy with the per-run secret', + text: 'The official SDK is pointed at `AUTH0_PLUGIN_DOMAIN` and uses the per-run secret as its token. The plugin never talks to the tenant directly and never sees a real token.', + data: [{ + label: 'Request', lines: [ + 'GET /api/v2/clients?per_page=50 HTTP/1.1', + 'Host: 127.0.0.1:', + 'Authorization: Bearer '] + }] + }; + // --------------------------------------------------------------------------- + // Scenarios. Step fields: actors, from, to, label (shown on the wire), title, + // text, data, status (ok | denied | fail), dim (components shown as not reached + // for this step only). + // A scenario with neverReached lists what a terminal failure protected. + // --------------------------------------------------------------------------- + var SCENARIOS = [ + // ===== Install and update ===== + // ===== Install and update ===== + { + id: 'install', group: 'Install and update', name: 'Install a plugin', + command: 'auth0 plugin install checkmate', + summary: 'Fetch and verify the index, show the permissions, download from an allowed host, verify the hash and the manifest, then write the lockfile and the approval.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 plugin install checkmate', + title: 'Run the install command', + text: 'A person asks the CLI to install checkmate. The name can be `checkmate`, `auth0/checkmate` or `checkmate@1.0.0`. Install needs no login, only a tenant name (' + TENANT + ' here, the default tenant) so the approval can be recorded for it. Nothing has been downloaded yet.', + data: [{ label: 'Command', lines: ['$ auth0 plugin install checkmate', 'tenant: ' + TENANT + ' (default tenant, or --tenant)'] }] + }, + { + actors: ['c-registry', 'registry'], from: 'cli', to: 'registry', label: 'GET index.json and index.json.sig', + title: 'Fetch the signed index and its signature', + text: 'The CLI downloads the registry index and the signature from the registry. It trusts neither yet. Plugin records can sit in files that the index pins by hash, and those are checked against that hash as well.', + data: [{ + label: 'Requests (default registry URL)', lines: [ + 'GET https://raw.githubusercontent.com/auth0/auth0-cli-plugins/registry/index.json', + 'GET https://raw.githubusercontent.com/auth0/auth0-cli-plugins/registry/index.json.sig', + note(FETCH_NOTE)] + }] + }, + { + actors: ['c-registry'], from: 'c-registry', to: 'c-registry', + title: 'Verify the signature, the expiry and rollback', + text: 'The Ed25519 signature is checked against public keys compiled into the CLI. No environment variable or config file can add a key. An expired index, or one older than the newest the CLI has accepted, is refused.', + data: [{ + label: 'Checks', lines: [ + ok('signature valid for the keyId in index.json.sig'), + ok('the index is not expired'), + ok('the index is not older than the newest one accepted before (state.json)'), + no('never falls back to an index that was not verified'), + note('On failure: "The auth0 registry index failed signature verification."')] + }] + }, + { + actors: ['c-registry', 'cache'], from: 'c-registry', to: 'cache', label: 'index + signature + state.json', + title: 'Cache the verified index and pick the version', + text: 'Only a verified index is written to the registry cache. The CLI then picks the latest version of checkmate that is not revoked, has a build for this machine, and accepts this CLI version.', + data: [{ + label: 'Resolution', lines: [ + ok('auth0/checkmate 1.0.0 is not on the revocation list'), + ok('an archive exists for this OS and CPU'), + ok('the engines range accepts this CLI version'), + note('The cache also lets installed plugins run offline for up to 30 days, with a warning.')] + }] + }, + { + actors: ['c-consent', 'user'], from: 'cli', to: 'user', label: 'Permissions requested for ' + TENANT, + title: 'Show the permissions before downloading anything', + text: 'The permissions come from the signed index, so a person who says no has used no bandwidth. Scopes are enforced by the proxy. Hosts and files are declared by the publisher, and the screen says the CLI does not enforce them.', + data: [{ + label: 'Output (stderr and terminal)', lines: [ + 'auth0/checkmate 1.0.0 A plugin for the Auth0 CLI', + 'Download: 8.5 MB (darwin-arm64)'].concat(FIRST_SCREEN.slice(0, 8), [ + 'Approve for ' + TENANT + '? [y/N/d] (d shows the exact scopes)']) + }] + }, + { + actors: ['user', 'c-consent'], from: 'user', to: 'cli', label: 'y', + title: 'A person approves for this tenant', + text: 'Only a person at a terminal can approve. Answering `d` first lists the exact scopes. With `--no-input`, in agent mode or without a terminal the plugin still installs, but no approval is recorded and the CLI says which command a person must run. There is no flag that approves for you. If your login may lack a scope the plugin asks for, the CLI warns you here, before the download. It never starts a login for that.', + data: [{ + label: 'Prompt', lines: [ + 'Approve for ' + TENANT + '? [y/N/d] (d shows the exact scopes) y', + note('The answer is kept in memory. It is written to disk only after the install succeeded.')] + }] + }, + { + actors: ['cli', 'release'], from: 'cli', to: 'release', label: 'GET checkmate_1.0.0_darwin-arm64.tar.gz', + title: 'Download from an allowed host', + text: 'The URL comes from the signed index. The CLI follows only https URLs on GitHub release hosts and checks the host again on every redirect. The file is streamed into a temporary folder with a size cap, never straight into the store.', + data: [{ + label: 'Download', lines: [ + ok('https, port 443, no user name in the URL'), + ok('host is github.com, objects.githubusercontent.com or release-assets.githubusercontent.com'), + ok('size and SHA-256 are checked while streaming, saved under plugins/tmp/'), + note('The file name here follows the release template. The URL itself is whatever the signed index lists.')] + }] + }, + { + actors: ['c-verify'], from: 'c-verify', to: 'c-verify', + title: 'Check the SHA-256 against the index', + text: 'The CLI hashes the file it received and compares it with the hash in the signed index. Nothing is unpacked or made executable until the two match, and a mismatch discards the file.', + data: [{ + label: 'Hash check', lines: [ + 'expected (signed index): sha256:', + 'computed (downloaded): sha256:', + ok('equal'), + note('On mismatch: "Downloaded file does not match the registry hash. The file was discarded."')] + }] + }, + { + actors: ['c-verify'], from: 'c-verify', to: 'c-verify', + title: 'Compare the manifest inside with the index record', + text: 'The archive must hold exactly the executable and plugin.json, with no links and no extra files. The manifest inside is compared with the signed record, including the permissions the person just approved, so a binary cannot ask for more than the index promised.', + data: [{ + label: 'Compared with the index record', lines: [ + ok('id and version'), ok('platforms and engines'), ok('commands and whether each is interactive'), ok('permissions'), + note('On mismatch: "The artifact does not match the registry record. Nothing was installed."')] + }] + }, + { + actors: ['cli', 'store'], from: 'cli', to: 'store', label: 'tmp/ to store/ and plugins.lock.json', + title: 'Move into the store and write the lockfile', + text: 'The verified files are moved into the version folder, and the lockfile is rewritten through a temporary file and a rename. An install either fully applies or leaves what was there before.', + data: [{ + label: 'On disk (next to config.json)', lines: [ + 'plugins/store/auth0/checkmate/1.0.0/checkmate (the executable)', + 'plugins/store/auth0/checkmate/1.0.0/plugin.json', + 'plugins/plugins.lock.json (0600): hashes of the executable, the manifest and the archive, the permissions hash, active = 1.0.0', + note('The active version is a lockfile field, not a symlink.')] + }] + }, + { + actors: ['cli', 'approvals'], from: 'cli', to: 'approvals', label: TENANT + ' + permissions hash', + title: 'Record the approval, after the install succeeded', + text: 'The approval is written only now. It is keyed to the hash of the permissions, not to the version, so a release with the same permissions does not ask again.', + data: [{ label: 'approvals.json (0600)', lines: ['tenant ' + TENANT + ', plugin auth0/checkmate, permissionsHash sha256:'] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'Installed auth0/checkmate 1.0.0.', + title: 'Installed', + text: 'The plugin is on disk and approved for ' + TENANT + '. No login or token was needed, and no plugin code has run yet. With `--json` the result is printed as JSON.', + data: [{ label: 'Output', lines: ['Installed auth0/checkmate 1.0.0.'] }] + } + ] + }, + { + id: 'update-same', group: 'Install and update', name: 'Update, same permissions', + command: 'auth0 plugin update checkmate', + summary: 'A newer version with an unchanged permissions hash installs without a question. The old version stays on disk for a rollback.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 plugin update checkmate', + title: 'Run the update command', + text: 'Update moves one plugin to the latest version in the index and clears its pin. `--all` updates every plugin that is not pinned. It never goes backwards: if the active version is the latest, nothing is downloaded or asked.', + data: [{ label: 'Command', lines: ['$ auth0 plugin update checkmate'] }] + }, + { + actors: ['c-registry', 'registry'], from: 'cli', to: 'registry', label: 'current, verified index', + title: 'Fetch and verify a current index', + text: 'Update needs a current index, and it goes through the same signature, expiry and rollback checks as an install. The verified index is cached.', + data: [{ label: 'Same checks as install', lines: [ok('signature, expiry, not older than the newest accepted'), note(FETCH_NOTE)] }] + }, + { + actors: ['c-registry', 'store'], from: 'cli', to: 'store', label: 'compare permissions hash', + title: 'Find 1.1.0 and compare the permissions hash', + text: 'The index lists 1.1.0 (an example), newer than the active 1.0.0 and not revoked. Its permissions hash equals the one the person approved, so no consent screen is shown.', + data: [{ + label: 'Comparison', lines: [ + 'active 1.0.0 permissions hash sha256:

', + 'latest 1.1.0 permissions hash sha256:

', + ok('equal, so the update asks nothing')] + }] + }, + { + actors: ['cli', 'release'], from: 'cli', to: 'release', label: 'GET checkmate_1.1.0_darwin-arm64.tar.gz', + title: 'Download the new version from an allowed host', + text: 'The old version keeps working until the new one is ready. The download follows the same host allowlist, size cap and redirect checks as an install.', + data: [{ label: 'Download', lines: [ok('https on a GitHub release host, checked on every redirect'), ok('streamed to plugins/tmp/')] }] + }, + { + actors: ['c-verify'], from: 'c-verify', to: 'c-verify', + title: 'Verify the hash and the embedded manifest', + text: 'The archive hash must match the signed index, and the manifest inside must match the signed record. Only then is anything unpacked.', + data: [{ label: 'Checks', lines: [ok('sha256 equals the index'), ok('manifest equals the index record, same permissions hash')] }] + }, + { + actors: ['cli', 'store'], from: 'cli', to: 'store', label: 'switch active to 1.1.0', + title: 'Switch the lockfile to the new version', + text: 'The lockfile is rewritten through a temporary file and a rename, so the update either fully applies or leaves 1.0.0 in place. The version that was active stays on disk as the previous one, for a rollback. Older versions are deleted.', + data: [{ + label: 'plugins.lock.json', lines: [ + del('active: 1.0.0'), add('active: 1.1.0'), add('previous: 1.0.0 (kept on disk)'), + 'pinned: false'] + }] + }, + { + actors: ['cli', 'approvals'], from: 'cli', to: 'approvals', label: 'no change', + title: 'The approval still applies', + text: 'Approvals are keyed to the permissions hash, so the existing record for ' + TENANT + ' matches the new version. Nothing needs to change and the next run passes the approval check.', + data: [{ label: 'approvals.json', lines: ['unchanged: ' + TENANT + ' / auth0/checkmate / sha256:

'] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'Updated auth0/checkmate from 1.0.0 to 1.1.0.', + title: 'Updated', + text: 'The CLI reports the new active version. The next `auth0 checkmate` runs 1.1.0 with the usual checks.', + data: [{ label: 'Output', lines: ['Updated auth0/checkmate from 1.0.0 to 1.1.0.'] }] + } + ] + }, + { + id: 'update-changed', group: 'Install and update', name: 'Update, changed permissions', + command: 'auth0 plugin update checkmate', + summary: 'A release that asks for different permissions shows only the difference, and is not applied without a person.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 plugin update checkmate', + title: 'Run the update command', + text: 'The same command as before, but the new release changes what the plugin may do. Consent for the new permissions is asked before anything is downloaded.', + data: [{ label: 'Command', lines: ['$ auth0 plugin update checkmate'] }] + }, + { + actors: ['c-registry', 'registry'], from: 'cli', to: 'registry', label: 'current, verified index', + title: 'Fetch and verify a current index', + text: 'The signature, expiry and rollback checks run exactly as in an install. The index record for 1.1.0 carries its permissions, so the CLI can compare before it downloads.', + data: [{ label: 'Same checks as install', lines: [ok('signature, expiry, not older than the newest accepted')] }] + }, + { + actors: ['c-registry', 'store'], from: 'cli', to: 'store', label: 'read installed plugin.json', + title: 'The permissions hash differs from the installed version', + text: 'The CLI reads the installed manifest and compares its permissions with the new record. They differ, so an approval is needed for this tenant.', + data: [{ + label: 'Comparison', lines: [ + 'active 1.0.0 permissions hash sha256:

', + 'latest 1.1.0 permissions hash sha256:

', + hl('different, so ask for ' + TENANT)] + }] + }, + { + actors: ['c-consent', 'user'], from: 'cli', to: 'user', label: 'only what changed', + title: 'The consent screen shows only what changed', + text: 'This tenant approved the installed version, so only the difference is shown. If it had not, the person would see the full screen.', + data: [{ + label: 'Consent screen (example change)', lines: [ + 'auth0/checkmate 1.1.0 asks for different permissions on tenant ' + TENANT, + ' Added can read logs', + ' Added host api.osv.dev Look up known vulnerabilities', + NOTICE, + '', + 'Approve for ' + TENANT + '? [y/N/d] (d shows the exact scopes)'] + }] + }, + { + actors: ['user', 'c-consent'], from: 'user', to: 'cli', label: 'y', + title: 'A person approves the change', + text: 'The answer is kept in memory. It is written to disk after the update succeeded, so a failed update is never approved.', + data: [{ label: 'Prompt', lines: ['Approve for ' + TENANT + '? [y/N/d] (d shows the exact scopes) y'] }] + }, + { + actors: ['cli', 'release'], from: 'cli', to: 'release', label: 'download and verify 1.1.0', + title: 'Download and verify the new version', + text: 'The usual allowlist, size cap, hash and manifest checks apply. The permissions in the archive must equal the ones the person just approved.', + data: [{ label: 'Checks', lines: [ok('sha256 equals the index'), ok('manifest permissions hash equals sha256:

')] }] + }, + { + actors: ['cli', 'store'], from: 'cli', to: 'store', label: 'active 1.1.0, previous 1.0.0', + title: 'Switch the lockfile', + text: 'The lockfile switches to 1.1.0 in one step, and 1.0.0 stays on disk as the previous version.', + data: [{ label: 'plugins.lock.json', lines: [del('active: 1.0.0'), add('active: 1.1.0'), add('previous: 1.0.0')] }] + }, + { + actors: ['cli', 'approvals'], from: 'cli', to: 'approvals', label: 'replace H1 with H2', + title: 'Record the new approval', + text: 'The new hash replaces the old one for this tenant. Other tenants are asked at their next run.', + data: [{ label: 'approvals.json', lines: [del('sha256:

'), add('sha256:

')] }] + }, + { + actors: ['cli'], from: 'cli', to: 'user', label: 'error, exit 1', status: 'fail', dim: ['release', 'store', 'approvals'], + title: 'The same update with no person: the old version stays active', + text: 'In agent mode, with `--no-input` or without a terminal, nobody can approve, so nothing is downloaded and nothing changes. The command exits 1 and says why.', + data: [{ + label: 'Message (agent mode: the same text, in a JSON error)', lines: [ + 'checkmate 1.1.0 asks for permissions that are not approved, and an agent cannot approve plugins. checkmate was not updated, and 1.0.0 stays active. Run this in a terminal to review them: auth0 plugin update checkmate', + note('With no terminal the reason reads "there is no terminal to ask in".')] + }] + } + ] + }, + { + id: 'rollback', group: 'Install and update', name: 'Roll back to the previous version', + command: 'auth0 plugin install checkmate@1.0.0', + summary: 'Going back reuses the version already on disk. If its permissions differ from what the tenant approved, the person is asked again.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 plugin install checkmate@1.0.0', + title: 'Install the exact earlier version', + text: 'Version 1.1.0 is active and 1.0.0 is still on disk from the last update. Asking for an exact version pins the plugin, so `update --all` skips it.', + data: [{ label: 'State before', lines: ['active: 1.1.0 (approved hash H2 on ' + TENANT + ')', 'previous: 1.0.0 (permissions hash H1)'] }] + }, + { + actors: ['c-registry', 'registry'], from: 'cli', to: 'registry', label: 'current, verified index', + title: 'Fetch and verify a current index', + text: 'Install needs a current index even for a rollback. 1.0.0 must still be listed and must not be revoked.', + data: [{ label: 'Same checks as install', lines: [ok('signature, expiry, not older than the newest accepted'), ok('1.0.0 is listed and not revoked')] }] + }, + { + actors: ['c-registry', 'store'], from: 'cli', to: 'store', label: 'compare H1 with active H2', + title: 'Compare the permissions hash with the active version', + text: 'An approval is asked for when the plugin is new or its permissions differ from the active version. H1 differs from H2, so the rollback needs one.', + data: [{ + label: 'Rule', lines: [ + 'active 1.1.0: H2 target 1.0.0: H1', + hl('H1 is different from H2, so ask'), + note('If the hashes were equal, there would be no screen and the approval would stay as it is.')] + }] + }, + { + actors: ['c-consent', 'user'], from: 'cli', to: 'user', label: 'only what changed vs 1.1.0', + title: 'Ask again, because the hash is not approved', + text: 'The tenant has only H2 on record, so the older permissions count as a new request, shown as the difference from the installed version.', + data: [{ + label: 'Consent screen (example change)', lines: [ + 'auth0/checkmate 1.0.0 asks for different permissions on tenant ' + TENANT, + ' Removed can read logs', + ' Removed host api.osv.dev Look up known vulnerabilities', + note('Without a terminal, or in agent mode, the rollback still installs, no approval is recorded, and running it needs auth0 plugin approve.')] + }] + }, + { + actors: ['user', 'c-consent'], from: 'user', to: 'cli', label: 'y', + title: 'A person approves', + text: 'Only a person at a terminal can approve. The answer is held in memory until the install has succeeded.', + data: [{ label: 'Prompt', lines: ['Approve for ' + TENANT + '? [y/N/d] (d shows the exact scopes) y'] }] + }, + { + actors: ['c-verify', 'store'], from: 'cli', to: 'store', label: 'reuse 1.0.0 on disk, no download', + title: 'Reuse the version on disk and switch the lockfile', + text: 'A version that is on disk and verifies is not downloaded again. Reuse needs the stored archive hash and permissions hash to equal the index record. Then the lockfile switches to it.', + data: [{ + label: 'plugins.lock.json', lines: [ + ok('archive hash and permissions hash equal the index record'), + del('active: 1.1.0'), add('active: 1.0.0'), add('previous: 1.1.0'), add('pinned: true')] + }] + }, + { + actors: ['cli', 'approvals'], from: 'cli', to: 'approvals', label: 'replace H2 with H1', + title: 'Record the approval for the older permissions', + text: 'The approval is written after the switch, and it replaces H2 with H1 for this tenant. Moving forward to 1.1.0 later will therefore ask again.', + data: [{ label: 'approvals.json', lines: [del('sha256:

'), add('sha256:

')] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'Switched auth0/checkmate from 1.1.0 to 1.0.0.', + title: 'Rolled back', + text: 'The plugin runs 1.0.0 again and shows as pinned in `plugin list`. `plugin update checkmate` by name moves it forward and clears the pin.', + data: [{ + label: 'Output', lines: [ + 'Switched auth0/checkmate from 1.1.0 to 1.0.0.', + 'auth0/checkmate is pinned to 1.0.0, so auth0 plugin update --all leaves it alone. Move it to the latest version with: auth0 plugin update checkmate'] + }] + } + ] + }, + // ===== Run ===== + { + id: 'run', group: 'Run', name: 'Run, happy path', + command: RUN_CMD, + summary: 'Every check runs before a token exists. The plugin only ever talks to the proxy.', + steps: [ + RUN_START, RUN_PRESCAN, RUN_VERIFY, RUN_REGISTRY, RUN_CALLCHECK, RUN_APPROVAL, RUN_SESSION, RUN_PROXY, RUN_SPAWN, RUN_API, + { + actors: ['proxy'], from: 'proxy', to: 'proxy', + title: 'The proxy checks the path, the method and the scope', + text: 'The path must already be in its canonical form. The method must be on the allowlist. Then the call is looked up in a table that comes from the signed index. Anything not on the table, or not covered by the scopes of this command, is denied.', + data: [{ + label: 'Proxy decision', lines: [ + ok('path is canonical and under /api/v2: ".." and encoded separators are rejected, not repaired'), + ok('method GET is on the allowlist'), + ok('rule: GET /api/v2/clients needs read:clients (the most specific rule wins)'), + ok('read:clients is among the scopes of this run, so: allow')] + }] + }, + { + actors: ['proxy', 'mgmt'], from: 'proxy', to: 'mgmt', label: 'GET /api/v2/clients (real token)', + title: 'The proxy swaps in the real token and forwards', + text: 'Only the proxy holds the access token. It drops the plugin\'s Authorization header, keeps a short list of other headers, adds the real token and sends the call to the tenant. It does not follow redirects.', + data: [{ + label: 'Outgoing request', lines: [ + 'GET https://' + TENANT + '/api/v2/clients?per_page=50', + 'Authorization: Bearer (never visible to the plugin)'] + }] + }, + { + actors: ['mgmt', 'proxy'], from: 'mgmt', to: 'proxy', label: '200 OK with client_secret', + title: 'The tenant answers', + text: 'The raw response can contain secrets, such as a client secret. At this point it reaches only the proxy.', + data: [{ label: 'Raw response (inside the CLI only)', lines: ['200 OK', '[{ "client_id": "abc", "name": "Acme Web", "client_secret": "" }]'] }] + }, + { + actors: ['proxy', 'plugin'], from: 'proxy', to: 'plugin', label: '200 OK, secret fields removed', + title: 'Secret fields are stripped before the plugin sees the response', + text: 'The proxy removes the secret field names listed in the signed index, wherever they appear in the JSON. Plugins that need secrets stay core CLI features.', + data: [{ label: 'Response to the plugin', lines: ['200 OK', '[{ "client_id": "abc", "name": "Acme Web" }]', del('"client_secret": ""')] }] + }, + { + actors: ['plugin', 'cli'], from: 'plugin', to: 'cli', label: 'exit 0', + title: 'The plugin finishes', + text: 'The plugin writes its report to the current folder (a declared permission, not enforced) and exits 0. Its output went straight to your terminal. The CLI never captured or rewrote it.', + data: [{ label: 'Result', lines: ['exit code 0'] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'summary of denied calls', + title: 'The CLI prints one line about denied calls', + text: 'After each run the CLI reports the calls the proxy denied, so a plugin that swallows errors cannot hide missing results. Nothing was denied in this run, so nothing is printed. The denied-call flow shows the line.', + data: [{ label: 'stderr', lines: ['(empty: no call was denied)'] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'hint about a newer version', + title: 'A hint, at most once a day', + text: 'After a successful run of an installed plugin, the CLI compares the installed version with the saved registry index. If a newer version can run, or this one is deprecated, it prints one line on stderr and notes the time in `hint.json`. It stays quiet for 24 hours, in agent mode, with `--no-input`, with a machine output format, when the output is not a terminal, for linked builds, and when `AUTH0_CLI_PLUGIN_UPDATE_HINT` is false. It reads the saved index only and never changes the result of the run.', + data: [{ label: 'stderr (only when there is something to say)', lines: ['checkmate 1.1.0 is available. Run: auth0 plugin update checkmate'] }] + }, + { + actors: ['cli', 'proxy'], from: 'cli', to: 'proxy', label: 'close', + title: 'The proxy closes', + text: 'The CLI closes the proxy and the per-run certificate goes with it. The secret stops working, so a copy kept by the plugin is useless afterwards. For plugins of the built-in registry the CLI then sends one usage event, unless you set `AUTH0_CLI_ANALYTICS=false`.', + data: [{ + label: 'After the run', lines: [ + no('per-run secret is dead'), + no('per-run certificate is gone'), + note('The event holds the plugin name and version, the CLI version, OS and CPU, whether the run succeeded, and for a failed run its reason code (or "other" when it is not one of the known codes). It goes out with the install ID that other CLI events use. Nothing about the arguments, the tenant or the output.'), + note('Only plugins of the auth0 registry are counted. Linked builds are never counted, and nothing is sent when AUTH0_CLI_ANALYTICS=false is set.')] + }] + } + ] + }, + { + id: 'denied', group: 'Run', name: 'Denied call: missing scope', + command: 'DELETE /api/v2/clients/abc from a plugin that declared only read scopes', + summary: 'The proxy answers 403 without ever contacting the tenant, and the CLI mentions it after the run.', + steps: [ + { + actors: ['cli', 'proxy', 'plugin'], from: 'cli', to: 'plugin', label: 'plugin running', + title: 'A run is in progress with only the declared scopes', + text: 'This continues the happy path. The proxy was started with the scopes of the command being run, which are read scopes only. `delete:clients` is not among them.', + data: [{ label: 'Scopes given to this run', lines: [ok('read:clients, read:connections, read:tenant_settings'), no('delete:clients')] }] + }, + { + actors: ['plugin', 'proxy'], from: 'plugin', to: 'proxy', label: 'DELETE /api/v2/clients/abc', + title: 'The plugin asks to delete a client', + text: 'The call is well formed and carries the right per-run secret. Whether it is allowed depends on the scope table, not on the secret.', + data: [{ label: 'Request', lines: ['DELETE /api/v2/clients/abc HTTP/1.1', 'Authorization: Bearer '] }] + }, + { + actors: ['proxy'], from: 'proxy', to: 'proxy', status: 'denied', dim: ['mgmt'], + title: 'No scope covers this call', + text: 'The rule for this endpoint needs `delete:clients`. The plugin did not declare it, so the proxy denies the call itself. The tenant is never contacted, even though the real token of the CLI might have allowed it.', + data: [{ + label: 'Proxy decision', lines: [ + 'rule: DELETE /api/v2/clients/{id} needs delete:clients', + no('delete:clients is not in the scopes of this run: deny'), + note('Calls missing from the table are denied too (no_scope_rule), and so are paths outside /api/v2 (forbidden_path).')] + }] + }, + { + actors: ['proxy', 'plugin'], from: 'proxy', to: 'plugin', label: '403 insufficient_scope', status: 'denied', + title: 'The plugin gets a 403 that names the missing scope', + text: 'The error body is shaped like a Management API error, so SDKs raise a normal 403. It tells the author exactly what to add to the manifest.', + data: [{ + label: 'Response', lines: [ + '403 Forbidden', + '{ "statusCode": 403, "error": "Forbidden",', + ' "message": "Plugin auth0/checkmate did not declare scope \\"delete:clients\\" required for DELETE /api/v2/clients/abc. Add it to permissions.scopes in plugin.json.",', + ' "errorCode": "insufficient_scope" }'] + }] + }, + { + actors: ['plugin', 'cli'], from: 'plugin', to: 'cli', label: 'exit 0 (error swallowed)', + title: 'The plugin swallows the error and finishes', + text: 'Plugins are not trusted to report their own failures. This one exits 0 as if nothing had happened.', + data: [{ label: 'Result', lines: ['exit code 0'] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: '1 API call was denied', + title: 'The CLI summary says it anyway', + text: 'Because the proxy counted the denial, the CLI prints one line on stderr after the run, with the scope to add. In agent mode the line is not printed. When the run fails, the same text is in `error.details.deniedCalls` of the JSON error.', + data: [{ label: 'stderr', lines: ['Plugin auth0/checkmate: 1 API call was denied (delete:clients for DELETE /api/v2/clients/abc). Review permissions.scopes in plugin.json.'] }] + } + ] + }, + { + id: 'strip', group: 'Run', name: 'Secret stripping', + command: 'GET /api/v2/clients/abc from a running plugin', + summary: 'The tenant answers with a client secret. The proxy removes it before the plugin sees the response.', + steps: [ + { + actors: ['plugin', 'proxy'], from: 'plugin', to: 'proxy', label: 'GET /api/v2/clients/abc', + title: 'The plugin reads one client', + text: 'The call is covered by `read:clients`, so the proxy allows it. Allowing the call does not mean allowing everything in the answer.', + data: [{ label: 'Request', lines: ['GET /api/v2/clients/abc HTTP/1.1', 'Authorization: Bearer ', ok('rule: GET /api/v2/clients/{id} needs read:clients')] }] + }, + { + actors: ['proxy', 'mgmt'], from: 'proxy', to: 'mgmt', label: 'GET /api/v2/clients/abc (real token)', + title: 'The proxy forwards with the real token', + text: 'The per-run secret is replaced by the real access token, which only the proxy holds.', + data: [{ label: 'Outgoing request', lines: ['GET https://' + TENANT + '/api/v2/clients/abc', 'Authorization: Bearer '] }] + }, + { + actors: ['mgmt', 'proxy'], from: 'mgmt', to: 'proxy', label: '200 OK', + title: 'The tenant answers with a secret in the body', + text: 'The Management API returns the client secret to a token that may read clients. This is the data the plugin must never receive.', + data: [{ + label: 'Raw response (inside the CLI only)', lines: [ + '200 OK', + '{ "client_id": "abc",', ' "name": "Acme Web",', hl(' "client_secret": ""'), '}'] + }] + }, + { + actors: ['proxy'], from: 'proxy', to: 'proxy', + title: 'The proxy removes the secret fields', + text: 'The secret field names in the signed index are matched without regard to case, at any depth, and the member is removed with its value. The list is data in the signed index, so it can grow without a CLI release.', + data: [{ + label: 'Redaction', lines: [ + '{ "client_id": "abc",', ' "name": "Acme Web",', del(' "client_secret": ""'), '}', + note('A response must be exactly one JSON value to be inspected. Anything else is refused (unsupported_response).')] + }] + }, + { + actors: ['proxy', 'plugin'], from: 'proxy', to: 'plugin', label: '200 OK, client_secret removed', + title: 'The plugin receives the redacted response', + text: 'The plugin gets everything it needs for an audit except the secret. Even if it asks for the field by name, it is removed.', + data: [{ label: 'Response to the plugin', lines: ['200 OK', '{ "client_id": "abc", "name": "Acme Web" }'] }] + } + ] + }, + { + id: 'timeout', group: 'Run', name: 'Timeout', + command: RUN_CMD, + summary: 'The plugin outlives its timeout. The CLI stops the whole process tree and reports a structured error.', + steps: [ + { + actors: ['c-spawn', 'plugin'], from: 'cli', to: 'plugin', label: 'spawn, timeout 10m, own process group', + title: 'All checks passed and the plugin starts with a timeout', + text: 'The timeout is the `timeout` of the command in the manifest (10m here), or the registry default of 10 minutes. A command that is not interactive runs in its own process group on Unix, and in a job object on Windows, so everything it starts can be stopped with it.', + data: [{ + label: 'Spawn', lines: [ + 'timeout: 10m (manifest)', + 'Unix, not interactive: own process group', + 'Windows: the plugin and what it starts belong to a job object', + note('An interactive command stays in the group of the CLI, so the terminal keeps working. On Unix a timeout then stops only the plugin process itself.')] + }] + }, + { + actors: ['plugin'], from: 'plugin', to: 'plugin', + title: 'The plugin hangs', + text: 'Suppose a validator waits forever on a slow network, and a helper process it started keeps running as well. The CLI is still waiting and the clock is running.', + data: [{ label: 'State', lines: ['plugin process: running', 'child of the plugin: running', 'elapsed: 10m'] }] + }, + { + actors: ['cli', 'plugin'], from: 'cli', to: 'plugin', label: 'SIGTERM to the process group', status: 'fail', + title: 'Timeout reached: ask the whole group to stop', + text: 'The CLI asks the plugin and everything it started to stop. Ctrl-C or SIGTERM sent to the CLI is passed on the same way.', + data: [{ label: 'Signal', lines: ['SIGTERM, to the process group', note('On Windows the request is a Ctrl-Break to the group, on a best effort basis.')] }] + }, + { + actors: ['plugin'], from: 'plugin', to: 'plugin', status: 'fail', + title: 'A 5 second grace period', + text: 'A well behaved plugin gets a few seconds to clean up. This one ignores the signal.', + data: [{ label: 'Grace', lines: ['5 s, plugin still running'] }] + }, + { + actors: ['cli', 'plugin'], from: 'cli', to: 'plugin', label: 'SIGKILL to the process group', status: 'fail', + title: 'Kill the whole group', + text: 'The group is killed, including the helper process, so nothing keeps running after the CLI reports the failure. On Windows the CLI ends the job object.', + data: [{ label: 'Signal', lines: ['SIGKILL, to the process group', ok('plugin and its child are gone')] }] + }, + { + actors: ['cli', 'proxy'], from: 'cli', to: 'proxy', label: 'close', + title: 'The proxy closes even on failure', + text: 'The per-run secret and certificate are discarded, so nothing the plugin left behind can reach the tenant.', + data: [{ label: 'After the run', lines: [no('per-run secret is dead')] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'error, exit 1', status: 'fail', + title: 'A structured error with a reason', + text: 'The CLI exits 1. The error class is "plugin" and the reason says what happened, so scripts can tell a plugin failure from a CLI failure.', + data: [ + { label: 'Human mode (stderr)', lines: ['Checkmate 1.0.0 did not finish within 10m0s and was stopped.'] }, + { label: 'Agent mode (stderr)', lines: ['{"error":{"code":"plugin","reason":"plugin_timeout","message":"checkmate 1.0.0 did not finish within 10m0s and was stopped","details":{"pluginId":"auth0/checkmate","version":"1.0.0"}}}'] }, + { label: 'If the plugin exits by itself with code 7', lines: ['{"error":{"code":"plugin","reason":"plugin_exit","message":"checkmate 1.0.0 exited with code 7","details":{"pluginId":"auth0/checkmate","version":"1.0.0","exitCode":7}}}', note('details.exitCode carries the code of the plugin. The CLI process itself exits 1. Other reasons: plugin_interrupted and plugin_start_failed.')] } + ] + } + ] + }, + // ===== Blocked paths ===== + { + id: 'tamper', group: 'Blocked paths', name: 'Blocked: tampered binary', + command: RUN_CMD, + summary: 'The binary on disk no longer matches the lockfile, so the run stops before any login.', + neverReached: ['Registry check', 'Call check', 'Approval check', 'Session / login', 'Proxy start', 'Spawn plugin'], + neverActors: ['c-registry', 'c-call', 'c-approval', 'c-session', 'c-proxy', 'c-spawn', 'cache', 'approvals', 'proxy', 'plugin', 'mgmt'], + protectedNote: 'Protected: no login, no token, no proxy. The modified file never ran.', + steps: [ + RUN_START, RUN_PRESCAN, + { + actors: ['c-verify', 'store'], from: 'cli', to: 'store', label: 'hash differs from the lockfile', status: 'fail', + title: 'Store.Verify fails: the binary changed', + text: 'Something replaced or edited the executable after the install. The hash no longer matches the lockfile, so the CLI refuses to run it, and it does so before any login. The same happens when plugin.json changes.', + data: [ + { + label: 'Human mode (stderr)', lines: [ + 'Checkmate no longer matches what was verified when it was installed (the hash of checkmate changed), so it will not run. Reinstall it with: auth0 plugin install checkmate.', + no('lockfile hash != hash of the file on disk')] + }, + { label: 'Agent mode (stderr)', lines: ['{"error":{"code":"validation","reason":"plugin_verification_failed","message":"checkmate no longer matches what was verified when it was installed (the hash of checkmate changed), so it will not run. Reinstall it with: auth0 plugin install checkmate"}}'] } + ] + } + ] + }, + { + id: 'revoked', group: 'Blocked paths', name: 'Blocked: revoked version', + command: RUN_CMD, + summary: 'The binary is intact, but the signed index has revoked this version, so it no longer runs.', + neverReached: ['Call check', 'Approval check', 'Session / login', 'Proxy start', 'Spawn plugin'], + neverActors: ['c-call', 'c-approval', 'c-session', 'c-proxy', 'c-spawn', 'approvals', 'proxy', 'plugin', 'mgmt'], + protectedNote: 'Protected: no login, no token, no proxy. A revoked version never starts.', + steps: [ + RUN_START, RUN_PRESCAN, RUN_VERIFY, + { + actors: ['c-registry', 'cache'], from: 'cli', to: 'cache', label: 'auth0/checkmate 1.0.0 is revoked', status: 'fail', + title: 'Index check fails: this version was revoked', + text: 'Revocation lives in the signed index, so it works as a fast kill switch even though the hash of the binary is still valid. Revocations are kept, so a machine cannot run a revoked version after it refreshes its index.', + data: [{ label: 'Message (stderr)', lines: ['Checkmate 1.0.0 was revoked: . Run: auth0 plugin update checkmate.', note(' is the text of the revocation entry in the index. The reason code is plugin_revoked.')] }] + } + ] + }, + { + id: 'unapproved', group: 'Blocked paths', name: 'Blocked: not approved on this tenant', + command: RUN_CMD, + summary: 'The binary and the index are fine, but no person approved these permissions on this tenant. No login happens.', + neverReached: ['Session / login', 'Proxy start', 'Spawn plugin'], + neverActors: ['c-session', 'c-proxy', 'c-spawn', 'proxy', 'plugin', 'mgmt'], + protectedNote: 'Protected: no login happened, no token was fetched, no proxy started, and the plugin never ran.', + steps: [ + RUN_START, RUN_PRESCAN, RUN_VERIFY, RUN_REGISTRY, RUN_CALLCHECK, + { + actors: ['c-approval', 'approvals'], from: 'cli', to: 'approvals', label: 'no record for ' + TENANT, status: 'fail', + title: 'Approval check fails: nobody approved this here', + text: 'Approval is per tenant. Here the plugin was installed in agent or no-input mode, so no approval was recorded. The CLI stops and prints the exact command a person must run. If the tenant had approved other permissions earlier, the message says the plugin "changed what it asks for since you approved it".', + data: [ + { label: 'Human mode (stderr)', lines: ['Checkmate is not approved for ' + TENANT + '. Run: ' + APPROVE + TENANT + '.', no('no token was requested')] }, + { label: 'Agent mode (stderr)', lines: ['{"error":{"code":"validation","reason":"plugin_not_approved","message":"checkmate is not approved for ' + TENANT + '. Run: ' + APPROVE + TENANT + '"}}'] } + ] + } + ] + }, + { + id: 'second-tenant', group: 'Blocked paths', name: 'Approve on a second tenant', + command: 'auth0 checkmate --tenant ' + OTHER, + summary: 'Approval on ' + TENANT + ' does not carry over. Only a person at a terminal can approve the other tenant. An agent cannot.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 checkmate --tenant ' + OTHER, + title: 'Run the plugin against another tenant', + text: 'The plugin is approved on ' + TENANT + ' only. The command targets ' + OTHER + ', which nobody has approved.', + data: [{ label: 'approvals.json before', lines: [ok(TENANT + ' / auth0/checkmate / sha256:

'), no(OTHER + ': no record')] }] + }, + { + actors: ['c-verify', 'c-registry'], from: 'cli', to: 'store', label: 'binary and index checks', + title: 'The binary and index checks pass', + text: 'The binary hash matches the lockfile and the version is not revoked. These checks do not depend on the tenant.', + data: [{ label: 'Checks', lines: [ok('hashes equal the lockfile'), ok('not revoked, index usable')] }] + }, + { + actors: ['c-approval', 'approvals'], from: 'cli', to: 'approvals', label: 'no record for ' + OTHER, status: 'fail', dim: ['c-session', 'c-proxy', 'c-spawn', 'proxy', 'plugin', 'mgmt'], + title: 'The approval check fails for this tenant', + text: 'Approvals are keyed to tenant and plugin. The run stops with the exact command, and no login or token is involved.', + data: [{ label: 'Message (stderr)', lines: ['Checkmate is not approved for ' + OTHER + '. Run: ' + APPROVE + OTHER + '.'] }] + }, + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'approve, in agent mode', status: 'fail', + title: 'An agent tries to approve and is refused', + text: 'Only a person can approve. The installed copy is checked first, then the refusal comes before the consent screen and nothing is written. There is no `--yes` flag that approves.', + data: [{ + label: 'Message (agent mode, stderr)', lines: [ + '{"error":{"code":"unknown","reason":"unclassified","message":"only a person can approve a plugin. Run this in a terminal: ' + APPROVE + OTHER + '"}}', + note('Without a terminal in human mode: "Only a person can approve a plugin. Run this in a terminal: ..."')] + }] + }, + { + actors: ['c-consent', 'user'], from: 'cli', to: 'user', label: 'Permissions requested for ' + OTHER, + title: 'A person runs approve in a terminal', + text: 'The person runs `auth0 plugin approve auth0/checkmate --tenant ' + OTHER + '` and sees the full consent screen for that tenant.', + data: [{ + label: 'Output', lines: ['auth0/checkmate 1.0.0 A plugin for the Auth0 CLI', ''].concat( + FIRST_SCREEN.slice(0, 8).map(function (l) { return l.replace(TENANT, OTHER); }), + ['Approve for ' + OTHER + '? [y/N/d] (d shows the exact scopes)']) + }] + }, + { + actors: ['user', 'c-consent'], from: 'user', to: 'cli', label: 'y', + title: 'The person approves', + text: 'A person answering at a terminal is the only way a new approval is created.', + data: [{ label: 'Prompt', lines: ['Approve for ' + OTHER + '? [y/N/d] (d shows the exact scopes) y'] }] + }, + { + actors: ['cli', 'approvals'], from: 'cli', to: 'approvals', label: 'add ' + OTHER, + title: 'The approval is recorded for the second tenant', + text: 'A second record is added next to the first. The record for ' + TENANT + ' is not touched.', + data: [{ label: 'approvals.json after', lines: [TENANT + ' / auth0/checkmate / sha256:

', add(OTHER + ' / auth0/checkmate / sha256:

'), 'Approved auth0/checkmate 1.0.0 for ' + OTHER + '.'] }] + }, + { + actors: ['c-approval', 'approvals'], from: 'cli', to: 'approvals', label: 'approved', + title: 'The next run passes the approval check', + text: 'The same command now clears the approval check. Session, proxy and spawn follow exactly as in the happy path.', + data: [{ label: 'Approval check', lines: [ok(OTHER + ' / auth0/checkmate / sha256:

is on record')] }] + } + ] + }, + // ===== Agent mode and CI ===== + { + id: 'agent-install', group: 'Agent mode and CI', name: 'Agent installs a plugin', + command: 'auth0 plugin install checkmate (run by an AI agent)', + summary: 'An agent can install a plugin, but it can never approve one. The install succeeds without an approval, and the JSON result says which command a person must run.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 plugin install checkmate', + title: 'An agent runs the install command', + text: 'The CLI turns agent mode on when it sees an agent in the environment (for example `AI_AGENT`, `CLAUDECODE` or `CURSOR_AGENT`), or when you pass `--agent-mode` or set `AUTH0_AGENT_MODE=true`. `AUTH0_AGENT_MODE=false` turns it off. Agent mode means JSON output, no prompts and no color.', + data: [{ label: 'Command', lines: ['$ auth0 plugin install checkmate', 'agent mode: on', note(AGENT_NOTE)] }] + }, + { + actors: ['c-registry', 'registry'], from: 'cli', to: 'registry', label: 'current, verified index', + title: 'Fetch and verify the index, as for a person', + text: 'Nothing is relaxed for an agent. The same signature, expiry and rollback checks run, and the index must be current.', + data: [{ label: 'Same checks as install', lines: [ok('signature, expiry, not older than the newest accepted'), ok('1.0.0 is not revoked')] }] + }, + { + actors: ['c-consent'], from: 'c-consent', to: 'c-consent', + title: 'No consent screen: an agent is never asked', + text: 'The consent screen is for a person. An agent is not asked even if it runs in a terminal, because only a person approves a plugin. The install goes on and records nothing.', + data: [{ label: 'Rule', lines: [no('no screen, no prompt, no approval recorded'), note('A new plugin installs without an approval. An update that needs new permissions does not, see the update flow.')] }] + }, + { + actors: ['cli', 'release', 'store'], from: 'cli', to: 'release', label: 'download, verify, move into the store', + title: 'Download, verify and store, as for a person', + text: 'The host allowlist, the size cap, the SHA-256 check, the manifest comparison and the atomic lockfile write all apply. The plugin is on disk and verified, and it cannot run yet.', + data: [{ label: 'Same checks as install', lines: [ok('allowed GitHub release host'), ok('sha256 equals the signed index'), ok('manifest equals the index record'), ok('lockfile written atomically')] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'JSON result, exit 0', + title: 'The JSON result says an approval is needed', + text: 'The command exits 0 and prints the result on stdout. `approvalRequired` is true and `approveCommand` is the exact command for a person.', + data: [{ + label: 'stdout', lines: [ + '{', + ' "id": "auth0/checkmate",', + ' "version": "1.0.0",', + ' "previousVersion": "",', + ' "action": "installed",', + ' "pinned": false,', + ' "approvalRequired": true,', + ' "approveCommand": "' + APPROVE + TENANT + '"', + '}'] + }] + }, + { + actors: ['c-approval', 'approvals'], from: 'cli', to: 'approvals', label: 'no record for ' + TENANT, status: 'fail', dim: ['c-session', 'c-proxy', 'c-spawn', 'proxy', 'plugin', 'mgmt'], + title: 'Running it is refused until a person approves', + text: 'The agent runs `auth0 checkmate`. The binary and index checks pass, and then the approval check fails, before any login.', + data: [{ label: 'stderr', lines: ['{"error":{"code":"validation","reason":"plugin_not_approved","message":"checkmate is not approved for ' + TENANT + '. Run: ' + APPROVE + TENANT + '"}}'] }] + }, + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'approve, in agent mode', status: 'fail', + title: 'The agent cannot approve it either', + text: 'The installed copy is checked first. Then the refusal comes before the consent screen, so nothing is written. A person approves in a terminal, or a pipeline pins the permissions with `AUTH0_CLI_PLUGIN_APPROVALS` (see the CI flow).', + data: [{ label: 'stderr', lines: ['{"error":{"code":"unknown","reason":"unclassified","message":"only a person can approve a plugin. Run this in a terminal: ' + APPROVE + TENANT + '"}}'] }] + } + ] + }, + { + id: 'agent-run', group: 'Agent mode and CI', name: 'Agent runs a plugin', + command: 'auth0 checkmate --validators a (run by an AI agent, already approved)', + summary: 'The checks are the same as for a person. The output is JSON, a failure is one JSON error, and the plugin gets agent mode in its environment.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 checkmate --validators a', + title: 'An agent runs an approved plugin', + text: 'The plugin was approved by a person on ' + TENANT + ' earlier. The agent passes no flags for it. Agent mode comes from the environment.', + data: [{ label: 'Command line', lines: ['$ auth0 checkmate --validators a', 'agent mode: on (JSON output, no prompts, no color)'] }] + }, + { + actors: ['c-prescan'], from: 'c-prescan', to: 'c-prescan', + title: 'Pre-scan passes agent mode on as variables', + text: 'The plugin cannot read the flags of the CLI, so the CLI turns them into variables. The plugin should print JSON and must not ask questions.', + data: [{ label: 'Environment of the plugin', lines: [ok('AUTH0_PLUGIN_AGENT_MODE=true'), ok('AUTH0_PLUGIN_NO_INPUT=true'), ok('AUTH0_PLUGIN_NO_COLOR=true'), ok('AUTH0_PLUGIN_JSON=true')] }] + }, + RUN_VERIFY, RUN_REGISTRY, + { + actors: ['c-call'], from: 'c-call', to: 'c-call', + title: 'The command must declare that it supports agent mode', + text: 'A command works in agent mode only if its manifest says `output.agentMode: true`, and it works with `--json` only if it says `output.json: true`. The plugin author promises that the output is JSON and that nothing waits for input.', + data: [{ + label: 'Checks', lines: [ + ok('checkmate declares output.agentMode: true'), + note('Without it: {"error":{"code":"usage","reason":"plugin_output_unsupported","message":"auth0 checkmate does not support agent mode (turn it off with --agent-mode=false), because its manifest does not declare output.agentMode"}}')] + }] + }, + RUN_APPROVAL, RUN_SESSION, RUN_PROXY, RUN_SPAWN, + { + actors: ['plugin', 'cli'], from: 'plugin', to: 'cli', label: 'JSON on stdout, exit 0', + title: 'The plugin prints JSON and exits 0', + text: 'The output goes straight to stdout, untouched. The CLI adds nothing to it, and it prints no summary line of denied calls in agent mode.', + data: [{ label: 'stdout (written by the plugin)', lines: ['{"validators":["a"],"findings":[]}'] }] + }, + { + actors: ['plugin', 'cli'], from: 'plugin', to: 'cli', label: 'exit 7', status: 'fail', + title: 'If the plugin fails, the CLI wraps it in one JSON error', + text: 'A plugin that exits non-zero, runs out of time, is interrupted or cannot start becomes one error with class "plugin" and a reason. The CLI exits 1, and `details.exitCode` has the code of the plugin. If the proxy denied calls, their summary is in `details.deniedCalls`.', + data: [{ + label: 'stderr', lines: [ + '{"error":{"code":"plugin","reason":"plugin_exit","message":"checkmate 1.0.0 exited with code 7","details":{"pluginId":"auth0/checkmate","version":"1.0.0","exitCode":7}}}'] + }] + } + ] + }, + { + id: 'agent-interactive', group: 'Agent mode and CI', name: 'Agent and an interactive command', + command: 'auth0 checkmate wizard (a command declared interactive, run by an agent)', + summary: 'A command that needs a person at the keyboard is refused in agent mode with a structured error, before any approval or login.', + neverReached: ['Approval check', 'Session / login', 'Proxy start', 'Spawn plugin'], + neverActors: ['c-approval', 'c-session', 'c-proxy', 'c-spawn', 'approvals', 'proxy', 'plugin', 'mgmt'], + protectedNote: 'Protected: nothing was approved, no login, no proxy, and the plugin never started.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 checkmate wizard', + title: 'An agent runs a command that is declared interactive', + text: '`wizard` is a made up example. Its manifest says `"interactive": true`. The caller is an AI agent, so agent mode is on.', + data: [{ label: 'Command line', lines: ['$ auth0 checkmate wizard', 'manifest: { "name": "wizard", "interactive": true }'] }] + }, + { + actors: ['c-prescan'], from: 'c-prescan', to: 'c-prescan', + title: 'Pre-scan finds the command and the CLI flags', + text: 'The CLI applies agent mode to itself. The plugin would get `AUTH0_PLUGIN_AGENT_MODE=true` and the name of the subcommand `wizard` as its first argument, if it ran.', + data: [{ label: 'Arguments', lines: ['plugin would get: wizard', ok('AUTH0_PLUGIN_AGENT_MODE=true')] }] + }, + RUN_VERIFY, RUN_REGISTRY, + { + actors: ['c-call'], from: 'c-call', to: 'user', label: 'error, exit 1', status: 'fail', + title: 'Refused: an interactive command cannot run in agent mode', + text: 'Commands declared interactive are refused in agent mode, with `--no-input`, and when input or output is not a terminal. The refusal is a usage error, and it comes before the approval check.', + data: [ + { label: 'Agent mode (stderr)', lines: ['{"error":{"code":"usage","reason":"plugin_interactive","message":"auth0 checkmate wizard is interactive, so it needs a terminal. It cannot run in agent mode, with --no-input, or when input or output is not a terminal"}}'] }, + { label: 'Human mode, no terminal (stderr)', lines: ['Auth0 checkmate wizard is interactive, so it needs a terminal. It cannot run in agent mode, with --no-input, or when input or output is not a terminal.'] } + ] + } + ] + }, + { + id: 'ci-pin', group: 'Agent mode and CI', name: 'CI with an approval pin', + command: 'AUTH0_CLI_PLUGIN_APPROVALS=auth0/checkmate@sha256: auth0 checkmate --agent-mode', + summary: 'A pipeline runs a plugin without a person, because a person pinned the exact permissions hash. Pins are never written to disk.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 checkmate --agent-mode --validators a', + title: 'A CI job runs the plugin with a pin', + text: 'The job signs in from the environment and sets a pin. A person created the pin earlier from the permissions hash of the plugin, which `auth0 plugin info` shows.', + data: [{ + label: 'CI environment', lines: [ + 'AUTH0_CLI_AUTH_MODE=env', + 'AUTH0_DOMAIN=' + TENANT + ' AUTH0_CLIENT_ID and AUTH0_CLIENT_SECRET set', + 'AUTH0_CLI_PLUGIN_APPROVALS=auth0/checkmate@sha256:<64 hex digits>', + '$ auth0 checkmate --agent-mode --validators a'] + }] + }, + { + actors: ['c-prescan'], from: 'c-prescan', to: 'c-prescan', + title: 'Pre-scan: agent mode and the tenant from the environment', + text: 'In env mode the tenant comes from `AUTH0_DOMAIN`, unless `--tenant` is given and matches. The CLI-owned `--agent-mode` is removed and passed on as `AUTH0_PLUGIN_AGENT_MODE=true`.', + data: [{ label: 'Arguments', lines: ['plugin gets: --validators a', ok('AUTH0_PLUGIN_AGENT_MODE=true'), ok('AUTH0_PLUGIN_TENANT=' + TENANT)] }] + }, + RUN_VERIFY, + RUN_REGISTRY, + RUN_CALLCHECK, + { + actors: ['c-approval', 'approvals'], from: 'cli', to: 'approvals', label: 'pin matches the permissions hash', + title: 'The pin approves the plugin, even in agent mode', + text: 'The pin is read strictly: a typo is an error, never "no pins". If its hash equals the permissions hash of the installed manifest, the plugin counts as approved on every tenant without touching approvals.json.', + data: [{ + label: 'Approval check', lines: [ + ok('pin auth0/checkmate@sha256: equals the installed permissions hash'), + no('nothing is written to approvals.json: pins are never saved'), + note('A bad entry in the variable is an error that names it.')] + }] + }, + { + actors: ['c-session'], from: 'c-session', to: 'c-session', + title: 'Session from the environment credentials', + text: 'The environment credentials are exchanged for a token only now, after the approval check. The client secret stays in the CLI. The plugin never receives it.', + data: [{ label: 'Token handling', lines: [ok('token held in CLI memory for the proxy'), no('AUTH0_CLIENT_SECRET is not in the plugin environment')] }] + }, + RUN_PROXY, + RUN_SPAWN, + { + actors: ['plugin', 'cli'], from: 'plugin', to: 'cli', label: 'exit 0', + title: 'The plugin finishes without a person', + text: 'The proxy applied the same scope table and secret stripping as in a run by a person. The job gets the output of the plugin on stdout and, on failure, the JSON error.', + data: [{ label: 'Result', lines: ['exit code 0'] }] + }, + { + actors: ['c-approval', 'approvals'], from: 'cli', to: 'approvals', label: 'pin no longer matches', status: 'fail', dim: ['c-session', 'c-proxy', 'c-spawn', 'proxy', 'plugin', 'mgmt'], + title: 'Later: a release changes the permissions, so the old pin stops matching', + text: 'A pin is bound to a permissions hash, not to a name. If a version with different permissions gets installed, the run is refused until a person pins the new hash.', + data: [{ label: 'stderr', lines: ['{"error":{"code":"validation","reason":"plugin_not_approved","message":"checkmate is not approved for ' + TENANT + '. Run: ' + APPROVE + TENANT + '"}}', note('In CI the fix is to update the pin, not to approve.')] }] + } + ] + }, + // ===== Build a plugin ===== + { + id: 'dev-create', group: 'Build a plugin', name: 'Create a plugin project', + command: DEV + 'auth0 plugin dev create checkmate --lang go --author "Platform team" --bugs https://github.com/acme/checkmate/issues', + summary: 'The author tools exist only in dev mode. Create writes a manifest and a small starter project in Go or TypeScript, and never overwrites anything.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'plugin dev create checkmate --lang go', + title: 'The author runs create', + text: 'The `plugin dev` commands are for plugin authors and exist only when `AUTH0_CLI_PLUGIN_DEV_MODE=true` is set. Missing values are asked for in a terminal and are an error otherwise. They need no login and no registry.', + data: [{ + label: 'Command', lines: [ + '$ ' + DEV + 'auth0 plugin dev create checkmate --lang go \\', + ' --author "Platform team" --bugs https://github.com/acme/checkmate/issues', + note('Without --author in a script: "--author is required: Team or person that owns the plugin. It becomes owner.team in plugin.json."')] + }] + }, + { + actors: ['c-scaffold'], from: 'c-scaffold', to: 'c-scaffold', + title: 'Check the name and the folder', + text: 'The name is the short name of the plugin: lowercase letters, digits and hyphens, starting with a letter. The plugin will run as `auth0 checkmate`. The folder may not exist or must be empty. An existing project is never overwritten.', + data: [{ label: 'Checks', lines: [ok('name checkmate is valid'), ok('./checkmate does not exist'), ok('id defaults to auth0/checkmate')] }] + }, + { + actors: ['cli', 'project'], from: 'cli', to: 'project', label: 'write plugin.json and the starter files', + title: 'Write the project', + text: 'The CLI writes the manifest and a starter that calls the Management API through the proxy. The Go starter has 27 files and the TypeScript starter has 31. With `--from` the starters come from a folder of your own.', + data: [{ + label: 'Go starter (top level)', lines: [ + 'plugin.json the manifest: id, version 0.1.0, platforms, commands, permissions', + 'main.go, commands/, plugin/, go.mod, .gitignore', + 'Makefile, .goreleaser.yaml, .golangci.yml, README.md', + '.github/workflows/ci.yml and release.yml, plus .github/actions, .github/scripts and dependabot.yml', + 'scripts/release/ the program behind make release'] + }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'Created auth0/checkmate', + title: 'Created, with the next steps', + text: 'The CLI prints what to do next. The starter asks for the scope `read:clients` only. You add scopes, hosts and files to `plugin.json` as the plugin needs them.', + data: [{ + label: 'Output', lines: [ + 'Created auth0/checkmate in /path/to/checkmate (27 files).', + 'Next:', + ' cd checkmate', + ' go mod tidy && go build -o checkmate .', + ' AUTH0_CLI_PLUGIN_DEV_MODE=true auth0 plugin dev link', + ' auth0 plugin approve checkmate', + ' auth0 checkmate', + 'To publish it, push the project to a new GitHub repository: the first push to main releases version 0.1.0. Release again with: make release BUMP=minor'] + }] + } + ] + }, + { + id: 'dev-validate', group: 'Build a plugin', name: 'Validate the manifest', + command: DEV + 'auth0 plugin dev validate ./checkmate', + summary: 'Validate checks plugin.json against the schema and the rules the CLI applies, without a login or the registry. It lists every problem with its path.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'plugin dev validate ./checkmate', + title: 'The author runs validate', + text: 'The folder is optional and defaults to the current one. Nothing is installed, linked or run.', + data: [{ label: 'Command', lines: ['$ ' + DEV + 'auth0 plugin dev validate ./checkmate'] }] + }, + { + actors: ['c-manifest', 'project'], from: 'cli', to: 'project', label: 'read plugin.json', + title: 'Read and check the manifest', + text: 'The CLI reads plugin.json and checks it against the plugin schema, then against the rules that apply at install, such as scope names, timeouts and platform names. It reports every problem, not only the first.', + data: [{ label: 'Checks', lines: ['schema: field names, types, patterns', 'scopes look like verb:resource', 'timeouts are durations such as 5m', 'platforms are known names'] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'The plugin manifest has 2 problems.', status: 'fail', + title: 'Problems are listed with their path', + text: 'Each row names the field and what is wrong with it. The command exits 1. With `--json` or in agent mode the same list is printed as JSON, with `valid: false`, and the error has the reason `plugin_manifest_invalid`.', + data: [{ + label: 'Output', lines: [ + ' PATH PROBLEM', + ' commands[0].permissions.scopes[1] \'nope\' does not match pattern \'^[a-z]+:[a-z0-9_]+$\'', + ' commands[0].timeout \'5 minutes\' does not match pattern \'^([0-9]+(\\\\.[0-9]+)?(ns|us|ms|s|m|h))+$\'', + '', + '=== error', + '', + 'The plugin manifest has 2 problems.'] + }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'The plugin manifest is valid.', + title: 'After the fix: valid', + text: 'The author edits the two fields and runs validate again. The command exits 0.', + data: [{ label: 'Output', lines: ['The plugin manifest is valid.'] }] + } + ] + }, + { + id: 'dev-link', group: 'Build a plugin', name: 'Link and run a local build', + command: DEV + 'auth0 plugin dev link', + summary: 'Link runs your local build as `auth0 ` through the same proxy, permissions and approval as an installed plugin. Only the checks that make no sense for an unpublished build are skipped.', + steps: [ + { + actors: ['user', 'project'], from: 'user', to: 'project', label: 'go build -o checkmate .', + title: 'The author builds the plugin', + text: 'The build is an ordinary executable in the project folder, named like the plugin. For another file, pass `--bin`.', + data: [{ label: 'Commands', lines: ['$ cd checkmate', '$ go mod tidy && go build -o checkmate .'] }] + }, + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'plugin dev link', + title: 'The author links the folder', + text: 'The folder is optional and defaults to the current one. Linking an already linked plugin replaces the link, and a linked plugin takes the place of an installed one with the same name.', + data: [{ label: 'Command', lines: ['$ ' + DEV + 'auth0 plugin dev link', note('With another file: auth0 plugin dev link ./checkmate --bin ./checkmate/dist/checkmate')] }] + }, + { + actors: ['c-link', 'project'], from: 'cli', to: 'project', label: 'read plugin.json, check the executable', + title: 'Read the manifest and check the executable', + text: 'The folder must hold a valid plugin.json, and the executable must be a regular file that can run. If the build is missing, the CLI says so and nothing is linked.', + data: [{ + label: 'Checks', lines: [ + ok('plugin.json is valid'), + ok('./checkmate/checkmate is a regular, executable file'), + note('Without a build: "The executable of the plugin cannot be used: stat ...: no such file or directory. Build it there, or give another file with --bin."')] + }] + }, + { + actors: ['cli', 'devlinks'], from: 'cli', to: 'devlinks', label: 'add the link', + title: 'Save the link', + text: 'The link is one entry in a small file next to the plugin store. It holds the folder and the executable, not a copy of either.', + data: [{ label: 'plugins/dev-links.json', lines: [add('auth0/checkmate: the folder and the executable of the build')] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'Linked auth0/checkmate 0.1.0', + title: 'Linked, and it is not approved yet', + text: 'A person must approve the permissions of the build, just as for an installed plugin. Dev mode does not change that.', + data: [{ + label: 'Output', lines: [ + 'Linked auth0/checkmate 0.1.0 from /path/to/checkmate. Run it with: auth0 checkmate', + 'Every run prints this line on stderr: Running the linked build of auth0/checkmate 0.1.0 from /path/to/checkmate. Its files are not checked against a release.', + 'auth0/checkmate is not approved on any tenant. Log in with auth0 login, then approve it in a terminal with: auth0 plugin approve auth0/checkmate --tenant '] + }] + }, + { + actors: ['c-consent', 'user'], from: 'user', to: 'cli', label: 'plugin approve checkmate --tenant ' + TENANT, + title: 'The author approves the build on a tenant', + text: 'The consent screen shows the permissions of the manifest as it is on disk now. Approval is stored like any other, keyed to the permissions hash.', + data: [{ label: 'Command', lines: ['$ auth0 plugin approve checkmate --tenant ' + TENANT, 'Approved auth0/checkmate 0.1.0 for ' + TENANT + '.'] }] + }, + { + actors: ['c-verify', 'project'], from: 'cli', to: 'project', label: 'read the manifest and the executable', + title: 'Run: the files are read as they are', + text: 'On `auth0 checkmate` the CLI prints the banner on stderr, then reads the manifest and the executable from the folder. It does not hash them and it does not use the lockfile, because a build changes all the time. A rebuild needs no new link.', + data: [{ + label: 'Skipped for a linked build', lines: [ + no('hashes of the files'), + no('the lockfile'), + no('the revocation list, and usage events'), + note('Everything else runs as for an installed plugin: the index check, the call check, the approval, the proxy and its scopes, secret stripping, the environment and the timeout.')] + }] + }, + { + actors: ['c-approval', 'approvals'], from: 'cli', to: 'approvals', label: 'permissions hash of the manifest on disk', status: 'fail', dim: ['c-session', 'c-proxy', 'c-spawn', 'proxy', 'plugin', 'mgmt'], + title: 'Editing the permissions asks for a new approval', + text: 'If the author adds a scope to plugin.json, the permissions hash changes and the old approval no longer matches. The run stops until the author approves again. The manifest is read a second time after the login, so a build edited in between cannot run with more than was approved.', + data: [{ label: 'Message (stderr)', lines: ['Checkmate changed what it asks for since you approved it for ' + TENANT + '. Run: ' + APPROVE + TENANT + '.'] }] + }, + { + actors: ['c-link'], from: 'c-link', to: 'c-link', + title: 'Unlink when you are done', + text: 'Unlink removes the entry from dev-links.json. An installed plugin with the same name, if any, runs again.', + data: [{ label: 'Command', lines: ['$ ' + DEV + 'auth0 plugin dev unlink auth0/checkmate', 'Unlinked auth0/checkmate.'] }] + } + ] + }, + // ===== Release a plugin ===== + { + id: 'release', group: 'Release a plugin', name: 'Release from the starter template', + command: 'make release BUMP=patch (TypeScript starter: bun run release)', + summary: 'One command opens a release pull request. Merging it builds the archives for every platform and publishes a GitHub release. Signing, attestation and publishing to the catalog are not part of it yet.', + steps: [ + { + actors: ['user', 'project'], from: 'user', to: 'project', label: 'make release BUMP=patch', + title: 'The author starts the release', + text: 'Nothing is edited by hand. The command needs `git`, `gh` (signed in with `gh auth login`) and a clean `main` that matches `origin`. Use `VERSION=1.2.3` for an exact version, or `DRY_RUN=1` to see the steps without changing anything.', + data: [{ label: 'Commands', lines: ['$ make release BUMP=patch', '$ make release VERSION=1.2.3', '$ make release BUMP=patch DRY_RUN=1'] }] + }, + { + actors: ['project', 'repo'], from: 'project', to: 'repo', label: 'branch release/v0.1.1, pull request', + title: 'It opens a release pull request', + text: 'The program creates the branch, raises `version` in plugin.json, commits with your own git settings, pushes and opens the pull request.', + data: [{ label: 'What it does', lines: ['branch release/v', 'commit chore: release v', 'file plugin.json, version raised', 'opens a pull request on GitHub'] }] + }, + { + actors: ['repo'], from: 'repo', to: 'repo', + title: 'Someone merges the pull request', + text: 'The release is decided by the merge. The Release workflow listens for a change to plugin.json on `main`. The first push of a new project releases the version the starter wrote, 0.1.0.', + data: [{ label: 'Trigger', lines: ['push to main that changes plugin.json', 'or: run the workflow by hand on main to finish a release that failed half way'] }] + }, + { + actors: ['repo', 'actions'], from: 'repo', to: 'actions', label: 'Release workflow starts', + title: 'The workflow reads the version', + text: 'The first job reads the version from plugin.json and works out the tag, `v`. It also checks whether that release already exists. If it does, the rest is skipped, so running the workflow again never changes a finished release.', + data: [{ label: 'Job: version', lines: ['tag = v from plugin.json', 'release exists? then skip everything else'] }] + }, + { + actors: ['actions'], from: 'actions', to: 'actions', + title: 'The tests run on the merged commit', + text: 'The workflow calls `ci.yml`: vet, test and build on Linux, macOS and Windows, and the linter. If anything fails, nothing is published.', + data: [{ label: 'Job: ci', lines: ['go vet, go test, go build on ubuntu, macos and windows', 'golangci-lint'] }] + }, + { + actors: ['actions'], from: 'actions', to: 'actions', + title: 'It builds one archive for each platform', + text: 'The Go starter uses GoReleaser. The TypeScript starter builds with `bun build --compile` once for each platform. Both give seven archives. Each archive holds exactly the executable and plugin.json at its root, because that is all the CLI accepts.', + data: [{ + label: 'Archives', lines: [ + 'checkmate_0.1.1_darwin-arm64.tar.gz, darwin-x64', + 'checkmate_0.1.1_linux-x64.tar.gz, linux-arm64', + 'checkmate_0.1.1_linux-x64-musl.tar.gz, linux-arm64-musl', + 'checkmate_0.1.1_windows-x64.zip', + 'checksums.txt (SHA-256 of every archive)'] + }] + }, + { + actors: ['actions', 'release'], from: 'actions', to: 'release', label: 'tag v0.1.1, upload archives', + title: 'It publishes a GitHub release', + text: 'The workflow tags the commit, creates the release with notes written by GitHub, and uploads the archives and `checksums.txt`. A second run replaces the assets of the first, which finishes a half made release.', + data: [{ label: 'Result', lines: [ok('GitHub release v0.1.1 with 7 archives and checksums.txt')] }] + }, + { + actors: ['release', 'registry'], from: 'release', to: 'registry', label: 'planned', status: 'info', + title: 'Here the template stops', + text: 'Signing the release, a build attestation, and listing the plugin in the registry catalog are planned. Until a plugin is listed, `auth0 plugin install` cannot find it. During development you use `plugin dev link`.', + data: [{ label: 'Planned', lines: ['signing of releases', 'build attestation', 'listing in the registry catalog'] }] + } + ] + }, + // ===== Housekeeping ===== + { + id: 'uninstall', group: 'Housekeeping', name: 'Uninstall', + command: 'auth0 plugin uninstall checkmate', + summary: 'A person confirms, the store removes every version, and the approvals on every tenant go with it. An agent needs --force.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 plugin uninstall checkmate', + title: 'Run the uninstall command', + text: 'Uninstall works without the registry, with plugins turned off, and with wrong approval pins, so a broken setup can always be cleaned up. `remove` is an alias.', + data: [{ label: 'Command', lines: ['$ auth0 plugin uninstall checkmate'] }] + }, + { + actors: ['cli', 'store'], from: 'cli', to: 'store', label: 'is it installed?', + title: 'The CLI checks the lockfile', + text: 'If the plugin is not in the lockfile, the command stops with an error that says so, and nothing changes.', + data: [{ label: 'Lockfile', lines: [ok('auth0/checkmate is installed'), note('Otherwise: "the plugin auth0/checkmate is not installed. Installed plugins: ". With none installed: "the plugin auth0/checkmate is not installed, and no plugin is. Find one with: auth0 plugin search".')] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'confirmation prompt', + title: 'Ask for confirmation', + text: 'In a terminal the person is asked first. `--force` skips the question.', + data: [{ label: 'Prompt', lines: ['Are you sure you want to uninstall auth0/checkmate? This removes every version and its approvals.'] }] + }, + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'yes', + title: 'The person confirms', + text: 'Answering no leaves everything as it was and prints that the plugin was not uninstalled.', + data: [{ label: 'Answer', lines: ['yes', note('If no: "auth0/checkmate was not uninstalled."')] }] + }, + { + actors: ['cli', 'store'], from: 'cli', to: 'store', label: 'remove versions and lockfile entry', + title: 'The store removes the plugin', + text: 'Every version folder and the lockfile entry are removed while the store is locked, and the freed size is counted.', + data: [{ label: 'Removed', lines: [del('plugins/store/auth0/checkmate/1.1.0/'), del('plugins/store/auth0/checkmate/1.0.0/'), del('plugins.lock.json entry for auth0/checkmate')] }] + }, + { + actors: ['cli', 'approvals'], from: 'cli', to: 'approvals', label: 'remove approvals for the plugin', + title: 'The approvals of that plugin are removed', + text: 'Approvals can only shrink here. The approvals of the plugin on every tenant go, and a damaged approvals file is deleted instead of blocking the uninstall.', + data: [{ label: 'approvals.json', lines: [del(TENANT + ' / auth0/checkmate'), del(OTHER + ' / auth0/checkmate')] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'Uninstalled auth0/checkmate and freed 8.5 MB.', + title: 'The space that was freed is printed', + text: 'Installing it again later starts from scratch, including a new consent screen. The CLI also sends one usage event for the uninstall, unless you opted out.', + data: [{ label: 'Output', lines: ['Uninstalled auth0/checkmate and freed 8.5 MB.'] }] + }, + { + actors: ['cli'], from: 'cli', to: 'user', label: 'refused without --force', status: 'fail', dim: ['store', 'approvals'], + title: 'What if an agent runs it without --force?', + text: 'In agent mode nobody can confirm, so the command refuses and removes nothing. With `--force` it goes ahead without a prompt. Outside agent mode, with `--no-input` or without a terminal, nothing can be asked, so the command removes the plugin without one.', + data: [{ label: 'stderr (agent mode, no --force)', lines: ['{"error":{"code":"unknown","reason":"unclassified","message":"this is a destructive command; re-run with --force to proceed without a confirmation prompt"}}'] }] + } + ] + }, + { + id: 'logout', group: 'Housekeeping', name: 'Logout removes approvals', + command: 'auth0 logout ' + TENANT, + summary: 'Consent never outlives the session it was given in. Logging out deletes the approvals of the tenant first.', + steps: [ + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 logout ' + TENANT, + title: 'Log out of a tenant', + text: 'The person logs out of ' + TENANT + '. The plugin stays installed, and the approvals on other tenants are not affected.', + data: [{ label: 'Command', lines: ['$ auth0 logout ' + TENANT] }] + }, + { + actors: ['cli', 'approvals'], from: 'cli', to: 'approvals', label: 'remove approvals of ' + TENANT, + title: 'The approvals of that tenant are deleted first', + text: 'They go before the tenant and its secrets. If something fails, the tenant is still logged in and logout can simply be tried again.', + data: [{ label: 'approvals.json', lines: [del(TENANT + ' / auth0/checkmate / sha256:

'), OTHER + ' / auth0/checkmate / sha256:

(kept)'] }] + }, + { + actors: ['cli'], from: 'cli', to: 'cli', + title: 'The tenant and its keyring secrets are removed', + text: 'Only then does the CLI remove the tenant from its config and delete the secrets of the tenant from the operating system keyring.', + data: [{ label: 'Removed', lines: [del('tenant ' + TENANT + ' from config'), del('keyring secrets for ' + TENANT)] }] + }, + { + actors: ['cli', 'user'], from: 'cli', to: 'user', label: 'Successfully logged out', + title: 'Logged out', + text: 'The CLI confirms the logout.', + data: [{ label: 'Output', lines: ['Successfully logged out from tenant: ' + TENANT] }] + }, + { + actors: ['user', 'cli'], from: 'user', to: 'cli', label: 'auth0 login, then auth0 checkmate', + title: 'Later: the person logs in again and runs the plugin', + text: 'The binary and the index are still fine, and the same plugin is still installed. The old approval is gone, though.', + data: [{ label: 'Commands', lines: ['$ auth0 login', '$ ' + RUN_CMD] }] + }, + { + actors: ['c-approval', 'approvals'], from: 'cli', to: 'approvals', label: 'no record for ' + TENANT, status: 'fail', dim: ['c-session', 'c-proxy', 'c-spawn', 'proxy', 'plugin', 'mgmt'], + title: 'Logging in again means approving again', + text: 'Consent was given in the old session, so it was deleted with it. The plugin will not run on this tenant until a person approves it again.', + data: [{ label: 'Message (stderr)', lines: ['Checkmate is not approved for ' + TENANT + '. Run: ' + APPROVE + TENANT + '.'] }] + } + ] + } + ]; + // --------------------------------------------------------------------------- + // Rendering + // --------------------------------------------------------------------------- + var W = 800; // viewBox width. The docs column is about 800 px, so 1 unit is about 1 px. + var ZT = 8; // top of the zone frames + var CT = 62; // top of the content area (room for the zone label and the trust badges) + var MARGIN = 8, PAD = 14, ZGAP = 14, SLOT = 36, BOX_H = 64, CHECK_H = 26, ROW_GAP = 64; + var BADGE = { trusted: 'trusted', untrusted: 'plugin code', neutral: 'outside' }; + var STATUS_TEXT = { ok: '✓ OK', fail: '✕ Blocked or failed', denied: '✕ Call denied', info: 'ℹ Planned' }; + var DRAW_MS = 400, DOT_MS = 900, STEP_MS = 1800; + var svg = document.getElementById('plugin-flow-diagram'); + var root = svg.closest('.plugin-flow'); + var NS = svg.namespaceURI; + var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; + var reduced = !!(mq && mq.matches); + var el = {}; + ['scenario', 'speed', 'prev', 'play', 'next', 'dots', 'summary', + 'p-count', 'p-status', 'p-title', 'p-text', 'p-protected', 'p-data', + 'g-zones', 'g-edges', 'g-nodes', 'g-wire', 'g-packet'].forEach(function (id) { el[id] = document.getElementById('plugin-flow-' + id); }); + function mk(tag, attrs, parent, text) { + var n = document.createElementNS(NS, tag); + if (attrs) Object.keys(attrs).forEach(function (k) { n.setAttribute(k, attrs[k]); }); + if (text !== undefined) n.textContent = text; + if (parent) parent.appendChild(n); + return n; + } + function h(tag, cls, text) { + var n = document.createElement(tag); + if (cls) n.className = cls; + if (text !== undefined) n.textContent = text; + return n; + } + // Text with `code` spans, built with DOM nodes only. + function richText(parent, str) { + str.split('`').forEach(function (part, i) { + if (i % 2 === 1) parent.appendChild(h('code', '', part)); + else parent.appendChild(document.createTextNode(part)); + }); + } + function resolve(id) { return NODES[id] && NODES[id].parent ? NODES[id].parent : id; } + // Rough text width in viewBox units for the system font stack. + function tw(str, px) { return str.length * px * 0.54; } + function wrap(str, max) { + var lines = [], cur = ''; + str.split(' ').forEach(function (word) { + if (cur && (cur + ' ' + word).length > max) { lines.push(cur); cur = word; } + else cur = cur ? cur + ' ' + word : word; + }); + if (cur) lines.push(cur); + return lines; + } + function labelBox(label) { + var lines = wrap(label, 28), w = 0; + lines.forEach(function (l) { w = Math.max(w, tw(l, 14)); }); + return { lines: lines, w: Math.ceil(w) + 20, h: lines.length * 18 + 10 }; + } + // The checks this scenario uses, in the order they first appear. + var checkCache = {}; + function checksOf(s) { + if (!checkCache[s.id]) { + var out = []; + s.steps.forEach(function (st) { + st.actors.forEach(function (a) { if (NODES[a].chip && out.indexOf(a) === -1) out.push(a); }); + }); + checkCache[s.id] = out; + } + return checkCache[s.id]; + } + function isFailing(st) { return st.status === 'fail' || st.status === 'denied'; } + // The boxes a step shows: its actors plus the two ends of the arrow, with checks folded into the CLI. + function visibleIds(st) { + var out = []; + function add(id) { var r = resolve(id); if (out.indexOf(r) === -1) out.push(r); } + st.actors.forEach(add); + add(st.from); + add(st.to); + return out; + } + function nodeSize(id, nChecks) { + var n = NODES[id]; + if (n.container) return nChecks ? { w: 214, h: 50 + nChecks * CHECK_H + 12 } : { w: 170, h: BOX_H }; + var w = Math.max(tw(n.name, 15) * 1.1, n.sub ? tw(n.sub, 14) : 0) + 28; + return { w: Math.ceil(Math.min(230, Math.max(150, w))), h: BOX_H }; + } + // Places the visible boxes. y values are relative to the top of the content area. + function layout(s, st) { + var ids = visibleIds(st), nChecks = checksOf(s).length; + var size = {}, colW = {}, rowSet = {}, zoneOfCol = {}, i; + ids.forEach(function (id) { + var n = NODES[id]; + size[id] = nodeSize(id, nChecks); + colW[n.col] = Math.max(colW[n.col] || 0, size[id].w); + zoneOfCol[n.col] = n.zone; + if (!n.container) rowSet[n.row] = true; + }); + var num = function (a, b) { return a - b; }; + var cols = Object.keys(colW).map(Number).sort(num); + var rows = Object.keys(rowSet).map(Number).sort(num); + var hasCli = ids.indexOf('cli') !== -1; + var span = rows.length ? (rows.length - 1) * (BOX_H + ROW_GAP) + BOX_H : BOX_H; + var hc = Math.max(span, hasCli ? size.cli.h : BOX_H); + // Gaps between neighbouring columns. The gap that holds the arrow label is widened. + var slots = []; + for (i = 1; i < cols.length; i++) slots.push(zoneOfCol[cols[i]] === zoneOfCol[cols[i - 1]] ? SLOT : 2 * PAD + ZGAP); + var ea = edgeEnds(st); + if (ea && st.label) { + var ia = cols.indexOf(NODES[ea[0]].col), ib = cols.indexOf(NODES[ea[1]].col); + if (Math.abs(ia - ib) === 1) { + var k = Math.min(ia, ib); + slots[k] = Math.max(slots[k], labelBox(st.label).w + 28); + } + } + var sumCols = 0, sumSlots = 0; + cols.forEach(function (c) { sumCols += colW[c]; }); + slots.forEach(function (g) { sumSlots += g; }); + var extra = W - (2 * MARGIN + 2 * PAD + sumCols + sumSlots), left = MARGIN + PAD; + if (slots.length && extra < 0) { + for (i = 0; i < slots.length; i++) slots[i] = Math.max(24, slots[i] + extra / slots.length); + } else if (extra > 0) { + if (slots.length) { + var add = Math.min(extra / slots.length, 110); + for (i = 0; i < slots.length; i++) slots[i] += add; + extra -= add * slots.length; + } + left += extra / 2; + } + var colX = {}, x = left; + cols.forEach(function (c, j) { colX[c] = x; x += colW[c] + (slots[j] || 0); }); + var zones = []; + ZONE_ORDER.forEach(function (key) { + var zc = cols.filter(function (c) { return zoneOfCol[c] === key; }); + if (!zc.length) return; + var a = colX[zc[0]] - PAD, b = colX[zc[zc.length - 1]] + colW[zc[zc.length - 1]] + PAD; + if (b - a < 240) { var grow = (240 - (b - a)) / 2; a -= grow; b += grow; } + zones.push({ key: key, x0: a, x1: b }); + }); + var pos = {}; + ids.forEach(function (id) { + var n = NODES[id], sz = size[id], px = colX[n.col] + (colW[n.col] - sz.w) / 2, cy; + if (n.container) { pos[id] = { x: px, y: 0, w: sz.w, h: hc, cli: true }; return; } + var ri = rows.indexOf(n.row); + cy = rows.length === 1 ? hc / 2 : BOX_H / 2 + ri * (hc - BOX_H) / (rows.length - 1); + pos[id] = { x: px, y: cy - sz.h / 2, w: sz.w, h: sz.h }; + }); + return { ids: ids, pos: pos, zones: zones, hc: hc, nChecks: nChecks }; + } + // The two boxes joined by the step arrow, or null for a step that stays inside one box. + function edgeEnds(st) { + var a = resolve(st.from), b = resolve(st.to); + return a === b ? null : [a, b]; + } + var sceneCache = {}; + function sceneHeight(s) { + if (!sceneCache[s.id]) { + var hc = 0; + s.steps.forEach(function (st) { hc = Math.max(hc, layout(s, st).hc); }); + sceneCache[s.id] = { hc: hc, h: CT + hc + 24 + ZT }; + } + return sceneCache[s.id]; + } + // Arrow points from box A to box B (absolute y values, oy is the content offset). + function route(A, B, oy) { + var acx = A.x + A.w / 2, bcx = B.x + B.w / 2, p, q, ay, by, xm; + if (Math.abs(acx - bcx) < 1) { + if (A.y < B.y) return [[acx, oy + A.y + A.h], [acx, oy + B.y]]; + return [[acx, oy + A.y], [acx, oy + B.y + B.h]]; + } + var acy = oy + A.y + A.h / 2, bcy = oy + B.y + B.h / 2; + ay = A.cli ? bcy : acy; + by = B.cli ? acy : bcy; + p = A.x < B.x ? A.x + A.w : A.x; + q = A.x < B.x ? B.x : B.x + B.w; + if (Math.abs(ay - by) < 1) return [[p, ay], [q, by]]; + xm = (p + q) / 2; + return [[p, ay], [xm, ay], [xm, by], [q, by]]; + } + function pathD(pts) { + return pts.map(function (p, i) { return (i ? 'L' : 'M') + p[0] + ' ' + p[1]; }).join(' '); + } + function polyLen(pts) { + var t = 0, i; + for (i = 1; i < pts.length; i++) t += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]); + return t; + } + function pointAt(pts, dist) { + var i, d; + for (i = 1; i < pts.length; i++) { + d = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]); + if (dist <= d || i === pts.length - 1) { + var t = d ? Math.min(1, dist / d) : 1; + return [pts[i - 1][0] + (pts[i][0] - pts[i - 1][0]) * t, pts[i - 1][1] + (pts[i][1] - pts[i - 1][1]) * t]; + } + dist -= d; + } + return pts[0]; + } + // Dot animation along a polyline, with ease-in-out and a delay so it starts after the arrow is drawn. + var raf = 0; + function stopPacket() { + if (raf) cancelAnimationFrame(raf); + raf = 0; + el['g-packet'].textContent = ''; + } + function runPacket(pts, bad) { + stopPacket(); + if (reduced || !pts) return; + var total = polyLen(pts), sp = speed(), delay = DRAW_MS / sp, dur = DOT_MS / sp, start = null; + var dot = mk('circle', { 'class': 'packet' + (bad ? ' bad' : ''), r: 7, cx: pts[0][0], cy: pts[0][1], opacity: 0 }, el['g-packet']); + function frame(ts) { + if (start === null) start = ts; + var t = Math.max(0, Math.min(1, (ts - start - delay) / dur)); + var e = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; + var p = pointAt(pts, e * total); + dot.setAttribute('cx', p[0]); + dot.setAttribute('cy', p[1]); + if (ts - start >= delay) dot.setAttribute('opacity', 1); + raf = t < 1 ? requestAnimationFrame(frame) : 0; + } + raf = requestAnimationFrame(frame); + } + // --------------------------------------------------------------------------- + // Drawing one step + // --------------------------------------------------------------------------- + function checkState(s, c, i) { + var cur = s.steps[i], k; + if (cur.actors.indexOf(c) !== -1) return isFailing(cur) ? 'fail' : 'current'; + for (k = 0; k < i; k++) if (s.steps[k].actors.indexOf(c) !== -1) return 'done'; + return 'todo'; + } + function drawNode(s, id, p, oy, ctx) { + var n = NODES[id], x = p.x, y = oy + p.y, cls = 'node ' + n.trust + (n.container ? ' container' : ''); + if (ctx.fail[id]) cls += ' fail'; + else if (ctx.active[id]) cls += ' active'; + if (ctx.target === id) cls += ' target'; + var g = mk('g', { 'class': cls, 'data-id': id }, el['g-nodes']); + mk('title', null, g, n.name + (n.sub ? ': ' + n.sub : '')); + mk('rect', { 'class': 'halo', x: x - 5, y: y - 5, width: p.w + 10, height: p.h + 10, rx: 8 }, g); + mk('rect', { 'class': 'box', x: x, y: y, width: p.w, height: p.h, rx: 8 }, g); + mk('text', { 'class': 'name', x: x + 14, y: y + 30 }, g, n.name); + if (n.sub) mk('text', { 'class': 'sub', x: x + 14, y: y + 50 }, g, n.sub); + if (n.container) { + checksOf(s).forEach(function (c, i) { + var state = checkState(s, c, ctx.step), ry = y + 44 + i * CHECK_H; + var row = mk('g', { 'class': 'check ' + state }, g); + if (state === 'current' || state === 'fail') mk('rect', { 'class': 'row', x: x + 8, y: ry, width: p.w - 16, height: CHECK_H - 2, rx: 8 }, row); + mk('text', { 'class': 'mark', x: x + 24, y: ry + 17, 'text-anchor': 'middle' }, row, { done: '✓', current: '●', fail: '✕', todo: '○' }[state]); + mk('text', { 'class': 'ctext', x: x + 40, y: ry + 17 }, row, NODES[c].name); + }); + } + var label = BADGE[n.trust], bw = Math.round(tw(label, 14) + 16); + var b = mk('g', { 'class': 'badge' }, g); + mk('rect', { x: x + p.w - bw - 10, y: y - 11, width: bw, height: 22, rx: 8 }, b); + mk('text', { x: x + p.w - 10 - bw / 2, y: y + 5, 'text-anchor': 'middle' }, b, label); + var cross = mk('g', { 'class': 'badge-x' }, g); + mk('circle', { cx: x, cy: y, r: 11 }, cross); + mk('text', { x: x, y: y + 5, 'text-anchor': 'middle' }, cross, '✕'); + } + function drawArrow(pts, bad) { + var n = pts.length, tip = pts[n - 1], prev = pts[n - 2], dx = tip[0] - prev[0], dy = tip[1] - prev[1]; + var len = Math.hypot(dx, dy) || 1, ux = dx / len, uy = dy / len, sp = speed(); + var line = pts.slice(0, n - 1).concat([[tip[0] - ux * 10, tip[1] - uy * 10]]); + var cls = 'edge-active' + (bad ? ' bad' : '') + (reduced ? '' : ' draw'); + var path = mk('path', { 'class': cls, d: pathD(line) }, el['g-edges']); + if (!reduced) { + path.style.setProperty('--len', String(Math.ceil(polyLen(line)) + 2)); + path.style.animationDuration = (DRAW_MS / sp) + 'ms'; + } + var head = mk('path', { + 'class': 'arrow-head' + (bad ? ' bad' : '') + (reduced ? '' : ' anim'), + d: 'M' + tip[0] + ' ' + tip[1] + ' L' + (tip[0] - ux * 13 - uy * 6.5) + ' ' + (tip[1] - uy * 13 + ux * 6.5) + + ' L' + (tip[0] - ux * 13 + uy * 6.5) + ' ' + (tip[1] - uy * 13 - ux * 6.5) + ' Z' + }, el['g-edges']); + if (!reduced) head.style.animationDelay = ((DRAW_MS * 0.75) / sp) + 'ms'; + } + function drawLabel(pts, label, vertical) { + var box = labelBox(label), total = polyLen(pts), mid = pointAt(pts, total / 2); + var cx = vertical ? mid[0] + 12 + box.w / 2 : mid[0], cy = mid[1]; + var g = mk('g', { 'class': 'wire' }, el['g-wire']); + mk('rect', { x: cx - box.w / 2, y: cy - box.h / 2, width: box.w, height: box.h, rx: 8 }, g); + box.lines.forEach(function (l, i) { + mk('text', { x: cx, y: cy - box.h / 2 + 5 + 14 + i * 18 - 2, 'text-anchor': 'middle' }, g, l); + }); + return { x: cx - box.w / 2, y: cy - box.h / 2, w: box.w, h: box.h }; + } + function drawScene(s, st, i) { + var L = layout(s, st), sc = sceneHeight(s), oy = CT + (sc.hc - L.hc) / 2, failing = isFailing(st); + ['g-zones', 'g-edges', 'g-nodes', 'g-wire'].forEach(function (k) { el[k].textContent = ''; }); + stopPacket(); + L.zones.forEach(function (z) { + var g = mk('g', { 'class': 'zone' }, el['g-zones']); + mk('rect', { x: z.x0, y: ZT, width: z.x1 - z.x0, height: sc.h - 2 * ZT, rx: 8 }, g); + mk('text', { x: z.x0 + 14, y: ZT + 26 }, g, ZONE_LABEL[z.key]); + }); + var ends = edgeEnds(st), ctx = { active: {}, fail: {}, target: ends ? ends[1] : resolve(st.actors[0]), step: i }; + st.actors.forEach(function (a) { (failing ? ctx.fail : ctx.active)[resolve(a)] = true; }); + EDGES.forEach(function (e) { + if (!L.pos[e[0]] || !L.pos[e[1]]) return; + if (ends && ((ends[0] === e[0] && ends[1] === e[1]) || (ends[0] === e[1] && ends[1] === e[0]))) return; + mk('path', { 'class': 'edge', d: pathD(route(L.pos[e[0]], L.pos[e[1]], oy)) }, el['g-edges']); + }); + L.ids.forEach(function (id) { drawNode(s, id, L.pos[id], oy, ctx); }); + var pts = null; + if (ends) { + pts = route(L.pos[ends[0]], L.pos[ends[1]], oy); + drawArrow(pts, failing); + if (st.label) drawLabel(pts, st.label, pts.length === 2 && pts[0][0] === pts[1][0]); + } + runPacket(pts, failing); + svg.setAttribute('aria-label', 'Step ' + (i + 1) + ': ' + L.ids.map(function (id) { return NODES[id].name; }).join(', ') + + '. The words for this step are in the text below the diagram.'); + } + // --------------------------------------------------------------------------- + // State and updates + // --------------------------------------------------------------------------- + var sIdx = 0, step = 0, playing = false, timer = 0; + function speed() { return parseFloat(el.speed.value) || 1; } + function scn() { return SCENARIOS[sIdx]; } + function firstFail(s) { + for (var i = 0; i < s.steps.length; i++) if (s.steps[i].status === 'fail') return i; + return -1; + } + // A scenario with neverReached stops at its first failing step. + function stopsAtFail(s) { return !!s.neverReached; } + function render() { + var s = scn(), st = s.steps[step], last = s.steps.length - 1; + var fi = stopsAtFail(s) ? firstFail(s) : -1; + var atFail = fi >= 0 && step >= fi; + drawScene(s, st, step); + // Caption + el['p-count'].textContent = 'Step ' + (step + 1) + ' of ' + s.steps.length; + var stKey = st.status || 'ok'; + el['p-status'].className = 'cap-status ' + stKey; + el['p-status'].textContent = STATUS_TEXT[stKey]; + el['p-title'].textContent = st.title; + el['p-text'].textContent = ''; + richText(el['p-text'], st.text); + el['p-protected'].textContent = ''; + if (atFail && s.protectedNote) { + var pr = h('div', 'protected'); + pr.appendChild(h('strong', '', '✕ What was protected. ')); + pr.appendChild(document.createTextNode(s.protectedNote.replace(/^Protected: /, ''))); + if (s.neverReached) pr.appendChild(h('div', 'never', 'Never reached: ' + s.neverReached.join(', ') + '.')); + el['p-protected'].appendChild(pr); + } + el['p-data'].textContent = ''; + (st.data || []).forEach(function (box) { + var d = h('div', 'databox'); + d.appendChild(h('div', 'dlabel', box.label)); + var body = h('div', 'dlines'); + box.lines.forEach(function (ln) { + var o = typeof ln === 'string' ? { k: '', t: ln } : ln; + var row = h('div', 'l ' + o.k); + var mark = { ok: '✓', no: '✗', del: '−', add: '+' }[o.k]; + if (mark) row.appendChild(h('span', 'm', mark)); + row.appendChild(document.createTextNode(o.t)); + body.appendChild(row); + }); + d.appendChild(body); + el['p-data'].appendChild(d); + }); + // Controls + el.prev.disabled = step === 0; + el.next.disabled = step === last; + updateDots(s); + } + function buildDots(s) { + el.dots.textContent = ''; + s.steps.forEach(function (st, i) { + var b = h('button', 'dot'); + b.type = 'button'; + b.setAttribute('aria-label', 'Step ' + (i + 1) + ': ' + st.title); + b.title = 'Step ' + (i + 1) + ': ' + st.title; + b.addEventListener('click', function () { setPlaying(false); go(i); }); + el.dots.appendChild(b); + }); + } + function updateDots(s) { + var dots = el.dots.childNodes; + s.steps.forEach(function (st, i) { + var b = dots[i], failed = isFailing(st) && i <= step; + b.className = 'dot ' + (i === step ? 'current' : (i < step ? 'done' : 'upcoming')) + (failed ? ' failed' : ''); + if (i === step) b.setAttribute('aria-current', 'step'); + else b.removeAttribute('aria-current'); + }); + } + function go(i) { + step = Math.max(0, Math.min(scn().steps.length - 1, i)); + render(); + schedule(); + } + function schedule() { + clearTimeout(timer); + if (!playing) return; + timer = setTimeout(function () { + if (step < scn().steps.length - 1) go(step + 1); + else setPlaying(false); + }, STEP_MS / speed()); + } + function setPlaying(on) { + playing = on; + el.play.textContent = on ? '❚❚ Pause' : '▶ Play'; + if (!on) clearTimeout(timer); + } + function togglePlay() { + if (playing) { setPlaying(false); return; } + setPlaying(true); + if (step >= scn().steps.length - 1) go(0); else schedule(); + } + function reset() { setPlaying(false); go(0); } + function selectScenario(i) { + sIdx = i; + setPlaying(false); + var s = scn(); + el.summary.textContent = s.summary; + svg.setAttribute('viewBox', '0 0 ' + W + ' ' + sceneHeight(s).h); + buildDots(s); + step = 0; + render(); + } + function buildSelect() { + var groups = {}; + SCENARIOS.forEach(function (s, i) { + if (!groups[s.group]) { + groups[s.group] = document.createElement('optgroup'); + groups[s.group].label = s.group; + el.scenario.appendChild(groups[s.group]); + } + var o = document.createElement('option'); + o.value = String(i); + o.textContent = s.name + ' (' + s.steps.length + ' steps)'; + groups[s.group].appendChild(o); + }); + } + function onMotionChange() { + reduced = !!(mq && mq.matches); + root.classList.toggle('still', reduced); + render(); + } + // --------------------------------------------------------------------------- + // Wiring + // --------------------------------------------------------------------------- + buildSelect(); + root.classList.toggle('still', reduced); + el.scenario.addEventListener('change', function () { selectScenario(parseInt(el.scenario.value, 10)); }); + el.speed.addEventListener('change', function () { schedule(); }); + el.prev.addEventListener('click', function () { setPlaying(false); go(step - 1); }); + el.next.addEventListener('click', function () { setPlaying(false); go(step + 1); }); + el.play.addEventListener('click', togglePlay); + if (mq) { + if (mq.addEventListener) mq.addEventListener('change', onMotionChange); + else if (mq.addListener) mq.addListener(onMotionChange); + } + document.addEventListener('keydown', function (ev) { + if (!root.contains(document.activeElement)) return; + if (ev.ctrlKey || ev.metaKey || ev.altKey) return; + var tag = ev.target && ev.target.tagName; + if (tag === 'SELECT' || tag === 'TEXTAREA' || tag === 'INPUT') return; + if (ev.key === 'ArrowRight') { ev.preventDefault(); setPlaying(false); go(step + 1); } + else if (ev.key === 'ArrowLeft') { ev.preventDefault(); setPlaying(false); go(step - 1); } + else if (ev.key === 'r' || ev.key === 'R') { ev.preventDefault(); reset(); } + else if ((ev.key === ' ' || ev.code === 'Space') && tag !== 'BUTTON') { ev.preventDefault(); togglePlay(); } + }); + setPlaying(false); + selectScenario(0); + })(); diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md new file mode 100644 index 000000000..8b90572a7 --- /dev/null +++ b/docs/plugins/architecture.md @@ -0,0 +1,98 @@ +--- +title: Architecture +parent: Plugins +nav_order: 5 +layout: default +--- + +## What this page covers + +This page explains the parts of the plugin system, what happens when you install and run a plugin, and which files the CLI writes on your machine. Use it when you want to know where a plugin's data lives or why a run stopped at a particular check. + +## The diagram + +In the diagram below, pick a scenario from the dropdown, then use Next, Previous or Play. Each step highlights the component that acts and explains what happens and why. The flows cover install and update with the consent screen, running a command with scope enforcement, creating, validating and linking a plugin, the release template, uninstalling, agent mode and CI. + +{% include plugin-flow.html %} + +## Components + +| Component | What it does | +| --- | --- | +| Auth0 CLI | Registers the plugin commands, runs the checks in order, asks for consent, and starts the plugin. | +| Registry client | Updates the signed registry metadata, checks it against the root built into the CLI, checks the index against it, and keeps the verified copy. | +| Store | Downloads archives from allowed hosts, checks their hashes, unpacks them, writes the lockfile, and checks the files before each run. | +| Approvals | Shows the consent screen, records approvals per tenant, and reads CI pins. | +| Runner | Builds the plugin environment, removes CLI flags from the arguments, and starts and stops the plugin process. | +| Loopback proxy | Serves HTTPS on `127.0.0.1`, an address that only this machine can reach, enforces scopes, strips secret fields, and forwards calls with the real token. | +| Dev links | Records local builds that an author runs with `auth0 plugin dev link`. | +| Scaffold | Writes the starter projects for `auth0 plugin dev create`. | +| Analytics | Sends the plugin usage events, unless analytics are turned off. | +| Plugin | Any language, built by its author. A native executable with a `plugin.json` next to it. It calls the Management API through the proxy. | +| Management API | Receives the calls that the proxy forwards with the real access token. | + +The CLI is the only part that talks to the registry, the release hosts and your tenant. A plugin's Management API calls go only through the proxy, but the CLI does not block other network traffic from the plugin. See [Security and trust model](security.md) for that limit. + +## Install and update + +1. You run `auth0 plugin install `. Install needs no login. It needs a tenant only to record the approval, and it uses `--tenant` or your default tenant. +2. The registry client updates the signed metadata from the registry URL, which is the base URL of the metadata directory. It follows any newer roots from the root built into the CLI, checks the signatures and expiry dates of the timestamp, snapshot and targets files, and downloads `index.json`. It then checks the index against the length and hash in the targets file, its own expiry date, and the newest index it accepted before. +3. The CLI picks the newest version that is not revoked, has an archive for your operating system, CPU and C library, and accepts this CLI version. +4. The consent screen shows the permissions of that version for your tenant, from the signed index. You can answer no, and nothing is downloaded. +5. The store downloads the archive from an allowed host into `tmp/`, stopping if it grows past the size cap. Then it checks the SHA-256 hash against the index. +6. The store unpacks the archive and compares the manifest inside with the index record. Only then are the files moved into `store/`. +7. The lockfile is rewritten through a temporary file and a rename. The approval is recorded last, and only after the install succeeded. + +An update follows the same steps. If the permissions hash is the same as the one your tenant approved, the update asks nothing. If it changed, the screen shows only the difference, and the old version stays active until you approve. The previous version stays on disk, so a rollback can reuse it. + +## Run + +1. You run `auth0 `. The CLI takes out the flags it owns, such as `--tenant`, and passes the rest to the plugin untouched. +2. The CLI checks that plugins are not turned off, and that a `--tenant` you passed matches `AUTH0_DOMAIN` when the environment fixes the tenant. +3. The CLI checks the installed files against the lockfile. This happens before any login. +4. The CLI reads the index, checks that the version is not revoked, and checks that the command can run the way it was called. +5. The CLI checks that the tenant approved these permissions. A missing approval stops the run here, before any token is requested. +6. The CLI opens the session for the approved tenant and checks the files a second time. +7. The CLI starts the loopback proxy with the scopes of this command, and a new certificate and secret for the run. +8. The CLI starts the plugin with a short environment that holds the proxy address and the secret. The plugin's output goes straight to your terminal. +9. Each call from the plugin goes to the proxy, which checks the path, the method and the scope, forwards the call with the real token, and removes secret fields from the response. +10. When the plugin exits, the CLI stops the proxy and prints one line about any calls that were denied. That line is not printed in agent mode. The CLI then exits with the result of the run. + +For the rules behind each check, see [Security and trust model](security.md). + +## Files on disk + +Plugin state lives in a `plugins` folder next to `config.json`, in your CLI config directory. The CLI stores no tokens or client secrets there. Those stay in the operating system keyring. + +```plaintext +~/.config/auth0/ +├── config.json +└── plugins/ + ├── plugins.lock.json installed plugins, hashes, active and pinned versions (0600) + ├── approvals.json approvals per tenant, keyed by permissions hash (0600) + ├── dev-links.json local builds from plugin dev link + ├── hint.json when the update hint was last shown + ├── registries/auth0/ + │ ├── metadata/ latest root, timestamp, snapshot and targets files + │ ├── roots/ roots after the first one, as 2.root.json, 3.root.json + │ ├── index.json last verified index + │ ├── state.json newest index version accepted, last refresh + │ └── .lock held while the registry is refreshed + ├── store/auth0/checkmate/ + │ └── 1.0.0/ + │ ├── checkmate the executable (0755), checkmate.exe on Windows + │ └── plugin.json the manifest from the archive (0600) + └── tmp/ downloads in progress +``` + +The path above is the one the CLI uses for `config.json`, which is `~/.config/auth0` on macOS and Linux. The lockfile and the approvals file are written to a temporary file and then renamed into place, so an interrupted write does not leave a half-written file behind. The folders are created with mode `0700`. + +The first root is built into the CLI, so it is not stored. The `roots` folder holds the roots that the registry published after it, and it only has files once the registry has rotated a key. A refresh writes to these files only after everything verified, so a failed refresh leaves them as they were. When the CLI uses the saved index, it checks the roots, the targets file and the index again without the network. + +Deleting the `plugins` folder removes every installed plugin and every approval. It does not remove your login, because the login stays in the keyring and in `config.json`. + +## Where to go next + +- [Security and trust model](security.md) explains the checks, the limits and how to report a problem. +- [Building a plugin](building.md) walks through `plugin dev create`, `dev link` and `dev validate`. +- [Reference](reference.md) lists the environment variables, reason codes and paths. diff --git a/docs/plugins/automation.md b/docs/plugins/automation.md new file mode 100644 index 000000000..f3357e032 --- /dev/null +++ b/docs/plugins/automation.md @@ -0,0 +1,167 @@ +--- +layout: default +title: Agent mode and automation +parent: Plugins +nav_order: 7 +--- + +This page explains how plugins behave when no person is at the keyboard, in agent mode, with `--no-input`, and in CI. You will see what an agent or a script can do, what it is refused, and how to approve a plugin in CI without a prompt. + +## The rule + +Only a person approves a plugin. Everything else can be automated. An agent or a script can search, inspect, install, update and run plugins, but it cannot record an approval. Two ways to skip the prompt in CI: approve in a terminal first, or pin the hash in an environment variable. The pin is described below. + +## Agent mode + +Agent mode turns on JSON output, turns off prompts and turns off colors. It is on when you pass `--agent-mode`, when `AUTH0_AGENT_MODE=true` is set, or when the CLI detects an AI agent. Pass `--agent-mode=false` or set `AUTH0_AGENT_MODE=false` to turn it off. + +Errors in agent mode, and with `--json`, are one JSON object on stderr. This is the error when `plugin approve` runs without a tenant to approve for: + +```json +{"error":{"code":"validation","reason":"plugin_no_tenant","message":"there is no tenant to approve the plugin for. Pass --tenant , or log in with: auth0 login"}} +``` + +`code` is the class of the failure, and `reason` is the specific cause. Scripts should branch on `reason`, because the message can change between releases. `details` is added when there is more to say. The reason codes for plugins are listed in [Troubleshooting](troubleshooting.md) and [Reference](reference.md). + +### Install and update + +In agent mode, with `--no-input`, and when there is no terminal, `auth0 plugin install` installs the plugin without an approval and exits with code 0. The output says what is still missing: + +```bash +auth0 plugin install checkmate --tenant acme.us.auth0.com --agent-mode +``` + +```json +{ + "id": "auth0/checkmate", + "version": "1.0.0", + "previousVersion": "", + "action": "installed", + "pinned": false, + "approvalRequired": true, + "approveCommand": "auth0 plugin approve auth0/checkmate --tenant acme.us.auth0.com" +} +``` + +An agent reads `approvalRequired` and passes `approveCommand` to a person. No flag approves a plugin during install. Running the plugin stays blocked until a person approves it or a pin covers it. + +`auth0 plugin update` is stricter, because new permissions need an approval before they apply. If the new version asks for different permissions and nobody can answer, the update fails. The installed version stays active, and the message says why: + +```text +checkmate 1.1.0 asks for permissions that are not approved, and an agent cannot approve plugins. checkmate was not updated, and 1.0.0 stays active. Run this in a terminal to review them: auth0 plugin update checkmate +``` + +`update --all` reports each plugin as an entry of a JSON array, and it fails if any plugin could not be updated. Pinned plugins and plugins that kept their permissions do not count as failures. + +### Approve and uninstall + +`auth0 plugin approve` needs a terminal. In agent mode, with `--no-input` and in a script, it exits with code 1 and returns the command that a person has to run as a JSON error: + +```json +{"error":{"code":"unknown","reason":"unclassified","message":"only a person can approve a plugin. Run this in a terminal: auth0 plugin approve auth0/checkmate --tenant acme.us.auth0.com"}} +``` + +`auth0 plugin uninstall` in agent mode requires `--force`, like other destructive commands. Without it, the command fails and removes nothing. + +### Running a plugin + +A plugin command can run only in the ways that its manifest declares. The CLI refuses any other call before the plugin starts: + +| You asked for | Refused when | Reason | +| --- | --- | --- | +| Agent mode | The command does not declare `output.agentMode` | `plugin_output_unsupported` | +| `--json` or `--json-compact` | The command does not declare `output.json` | `plugin_output_unsupported` | +| `--csv` | The command does not declare `output.csv` | `plugin_output_unsupported` | +| Agent mode, `--no-input`, or no terminal | The command is interactive, so it needs a terminal | `plugin_interactive` | + +For example, a command that does not declare agent mode fails like this: + +```json +{"error":{"code":"usage","reason":"plugin_output_unsupported","message":"auth0 checkmate env does not support agent mode (turn it off with --agent-mode=false), because its manifest does not declare output.agentMode"}} +``` + +When a plugin starts and then fails, the error has the code `plugin`, and `details` names the plugin and its version. If the plugin exited by itself, `details.exitCode` holds its exit code, and the reason is `plugin_exit`. A plugin that runs past its timeout is stopped along with its child processes, and it fails with `plugin_timeout`: + +```json +{"error":{"code":"plugin","reason":"plugin_timeout","message":"checkmate 1.0.0 did not finish within 5s and was stopped","details":{"pluginId":"auth0/checkmate","version":"1.0.0"}}} +``` + +In agent mode the CLI does not print the line about denied API calls. A failed run carries that line in `details.deniedCalls` instead. + +If you type the name of a plugin that is in the registry but not installed, the CLI returns the install command as data: + +```json +{"error":{"code":"not_found","reason":"plugin_not_installed","message":"Plugin \"checkmate\" is available. Install it with: auth0 plugin install checkmate","details":{"pluginId":"auth0/checkmate","installCommand":"auth0 plugin install checkmate"}}} +``` + +### Finding plugin commands + +`auth0 commands --json` lists plugin commands next to the core ones. A plugin command has `"source": "plugin"`, its `pluginId` and `pluginVersion`, the `requiredScopes` of the command, and an `output` object. The `output` object says which of `json`, `csv` and `agentMode` the command supports. An agent can pick a command and check which flags it may use without parsing the help text. + +## Exit codes + +The exit code is `0` on success and `1` on any failure, so scripts that only test for zero keep working. The one exception is an interrupt. When you stop the CLI with Ctrl-C, it exits with `130`. The kind of failure is in the error object, not in the exit code. When a plugin fails, its own exit code is in `details.exitCode`, and the CLI still exits with `1`. + +## Approve a plugin in CI + +Set `AUTH0_CLI_PLUGIN_APPROVALS` to a comma-separated list of `registry/name@sha256:` entries. A plugin whose permissions hash matches one of the pins counts as approved on every tenant, so pin only a version you have reviewed. Installing a plugin never approves it. Approval needs a person at a terminal or a pin, as in this example: + +```bash +export AUTH0_CLI_PLUGIN_APPROVALS="auth0/checkmate@sha256:9b991b1b3d0c523b4d7aa0f05b5e970f3621bae92d157d563a0458ba28a5b9a7" +``` + +Take the hash from the `PERMISSIONS HASH` row of `auth0 plugin info checkmate`, or from the `permissionsHash` key of `auth0 plugin info checkmate --json`. Review the permissions once in a terminal before you pin the hash, because a pin says you reviewed exactly those permissions. + +A few rules apply: + +- A pin is checked by `install`, `update` and every run. With a matching pin, `install` and `update` do not ask, and the result has `"approvalRequired": false`. +- A pin matches the hash and nothing else. If a new version of the plugin asks for different permissions, the hash changes, and the pin no longer matches. The run then fails with `plugin_not_approved` until you review the new permissions and update the pin. +- You can list more than one pin, for example to cover a rollout: `auth0/checkmate@sha256:,auth0/checkmate@sha256:`. Each entry needs the full ID with the registry name, as in `auth0/checkmate`. +- A pin is never written to `approvals.json`. +- An invalid value is an error, not an empty list, so a typo cannot look like having no pins: + +```text +The value of AUTH0_CLI_PLUGIN_APPROVALS is not valid. (the entry "checkmate@sha256:abc" is not in the form registry/name@sha256:<64 lowercase hex digits>) +``` + +A pin works with `AUTH0_CLI_AUTH_MODE=env`, where the CLI reads the tenant and the credentials from the environment: + +```bash +export AUTH0_CLI_AUTH_MODE=env +export AUTH0_DOMAIN=acme.us.auth0.com +export AUTH0_CLIENT_ID= +export AUTH0_CLIENT_SECRET= +auth0 plugin install checkmate@1.0.0 +auth0 checkmate --json +``` + +In this mode the CLI does not save the tenant or the credentials in `config.json` or in the keychain. Plugin files still go to the plugins folder described in [Managing plugins](managing.md). + +In this mode the tenant is fixed by `AUTH0_DOMAIN`. Passing a different `--tenant` fails instead of running on another tenant: + +```text +--tenant "other.us.auth0.com" conflicts with AUTH0_DOMAIN="acme.us.auth0.com"; in AUTH0_CLI_AUTH_MODE=env mode the tenant is fixed by AUTH0_DOMAIN. Drop --tenant, or set AUTH0_DOMAIN to the tenant you want to target. +``` + +An exact version install pins the plugin. So `auth0 plugin install checkmate@1.0.0` also stops `update --all` from moving it in a long-lived environment. + +## Turn plugins off + +`AUTH0_CLI_PLUGINS_DISABLED=true` blocks install, update, approve and running plugins. Search, list, info and uninstall keep working. Use it on managed machines and in images where plugins are not wanted: + +```text + ▸ Plugins are turned off on this machine by AUTH0_CLI_PLUGINS_DISABLED=true, so auth0 plugin install is not available. Searching, listing and uninstalling still work. +``` + +## Other switches + +| Variable | Effect | +| --- | --- | +| `AUTH0_CLI_PLUGIN_UPDATE_HINT=false` | Turns off the hint about a newer plugin version, which shows at most once a day. The hint never shows in agent mode, with `--no-input`, with `--json`, `--json-compact` or `--csv`, or when the output is not a terminal. | +| `AUTH0_CLI_ANALYTICS=false` | Turns off usage events, including the plugin events. See [Security and trust model](security.md). | + +## Next + +- [Troubleshooting](troubleshooting.md): reason codes and fixes. +- [Reference](reference.md): environment variables and reason codes in one place. +- [Managing plugins](managing.md): the commands behind these flows. diff --git a/docs/plugins/building.md b/docs/plugins/building.md new file mode 100644 index 000000000..f6209439d --- /dev/null +++ b/docs/plugins/building.md @@ -0,0 +1,518 @@ +--- +layout: default +title: Building a plugin +parent: Plugins +nav_order: 3 +--- + +In this page you create a plugin project, link a build of it to the Auth0 CLI, and run it as `auth0 ` while you work on it. The project comes from one of the starter projects that ship inside the CLI, in Go or in TypeScript. + +## Before you start + +A plugin is an executable plus a `plugin.json` file. The CLI runs the plugin only when you start it through the CLI, as `auth0 `. If you run the executable by hand, it stops and tells you to use the CLI, because the variables that connect it to the CLI are not set: + +```text +error: AUTH0_PLUGIN_DOMAIN is not set. This plugin has to be started by the Auth0 CLI: run `auth0 checkmate` instead of running the file directly +``` + +The commands for plugin authors, the `auth0 plugin dev` group, are available only in dev mode. Turn dev mode on in the shell you build in: + +```sh +export AUTH0_CLI_PLUGIN_DEV_MODE=true +``` + +Only the exact value `true` turns dev mode on. Dev mode also makes the CLI read the links that `auth0 plugin dev link` creates. Without dev mode a linked build is ignored: `auth0 plugin list` shows nothing, and `auth0 checkmate` is an unknown command. Turn dev mode off on machines where you only use installed plugins, so a local build cannot run there. + +Without dev mode, every `plugin dev` command stops with this message: + +```text +=== error + + ▸ The commands for plugin authors are available only in dev mode. Turn it on with: AUTH0_CLI_PLUGIN_DEV_MODE=true. +``` + +You also need the toolchain of the language you pick: Go 1.26 or later, or Bun, a JavaScript runtime that can also compile a program into one executable. The CI of the starter projects uses Bun 1.4.4, and the commands in this page ran with Bun 1.4.4 and Go 1.26. + +## Create a project + +`auth0 plugin dev create` writes a new project into a directory. The directory is created when it does not exist, but its parent directory must exist. An empty directory is used as it is. A directory that holds files is refused, because an existing project is never overwritten. + +```sh +auth0 plugin dev create checkmate --lang go --author "Platform team" --bugs https://github.com/acme/checkmate/issues +``` + +```text + ▸ Created auth0/checkmate in /home/you/checkmate (27 files). + ▸ Next: + cd /home/you/checkmate + go mod tidy && go build -o checkmate . + AUTH0_CLI_PLUGIN_DEV_MODE=true auth0 plugin dev link + auth0 plugin approve checkmate + auth0 checkmate + ▸ To publish it, push the project to a new GitHub repository: the first push to main releases version 0.1.0. Release again with: make release BUMP=minor +``` + +The last line is the CLI's own output. A GitHub release alone does not make the plugin installable, because the registry catalog also has to list it, as a separate step. See [Releasing a plugin](releasing.md). + +The path in this output is shortened. For a TypeScript project, pass `--lang ts`. The next steps then start with `bun install && bun run build`, and the release command is `bun run release minor`. + +The name is the short name of the plugin. It starts with a lowercase letter and holds only lowercase letters, digits and hyphens. The plugin runs as `auth0 `, so the name cannot be the name of a core command such as `apps`. The CLI refuses that name: + +```text +=== error + + ▸ Auth0 already has a command named "apps". Plugin names cannot shadow core commands. +``` + +`--lang`, `--author` and `--bugs` are required. In a terminal, `create` asks for any value you leave out. Everything else takes a default. Without a terminal, a missing value is an error: + +```text +=== error + + ▸ --lang is required: Language of the starter project: go or ts. +``` + +### Flags of `plugin dev create` + +| Flag | Default | Meaning | +| --- | --- | --- | +| `--lang` | none, required | `go` or `ts`. | +| `--author` | none, required | The team or person that owns the plugin. It becomes `owner.team` in `plugin.json`. | +| `--bugs` | none, required | An HTTPS URL where people report problems. It becomes `owner.bugs`. | +| `--id` | `auth0/` | The plugin ID. The CLI knows one registry, `auth0`, so the ID must be `auth0/` followed by the name. Another value is refused. | +| `--version` | `0.1.0` | The first version, as `MAJOR.MINOR.PATCH`. | +| `--description` | `A plugin for the Auth0 CLI` | One line, up to 120 characters. | +| `--command` | the name of the plugin | The name of the first command. When it equals the plugin name, it runs as `auth0 `. Otherwise it runs as `auth0 `. | +| `--command-description` | the plugin description | What the first command does. | +| `--dir` | `./` | Where to write the project. The directory must not exist or must be empty, and its parent must exist. | +| `--license` | `UNLICENSED` | Written into the files of the project. No `LICENSE` file is created. | +| `--scopes` | `read:clients` | Comma-separated Management API scopes for the first command. Pass `--scopes ""` for none. | +| `--from` | the starter projects in the CLI | A directory of your own starter projects, laid out like the built-in ones. See below. | +| `--json`, `--json-compact` | off | Print the directory, the files, the ID, the language and any warnings as JSON. | + +With no scopes, `create` still writes the project and warns you that the sample call will be denied: + +```text + ▸ The first command declares no scopes, so the Management API call of the sample is denied. Add the scopes it needs under permissions.scopes in plugin.json. +``` + +When the CLI that runs `create` is a release build, the manifest also gets `engines.auth0-cli`, set to `>=` followed by the version of that CLI. A development build writes no `engines` block. + +### Your own starter projects + +`--from ` replaces the built-in projects with your own. The directory holds a `go` folder, a `ts` folder, or both, and each one is a complete project. A file named `X.tmpl` is written as `X`. The text `__PLUGIN_NAME__`, `__COMMAND__` and `__LICENSE__` is replaced in every file. A tree may not hold symbolic links, more than 64 files, or more than 256 KB, and it may not hold a `plugin.json`, because `create` always writes one. `create` checks all of this before it writes anything. + +## The project layout + +The Go project has 27 files, and the TypeScript project has 31. These are the parts you will change. + +Go: + +```text +checkmate/ + main.go starts the command + commands/ + registry.go finds the command from the arguments + clients.go the sample command + plugin/ + client.go the Management API client, pointed at the CLI proxy + output.go JSON, table and message helpers + errors.go error text and exit codes + paginate.go walks every page of a list call + plugin.go the name, the version and the exit codes + scripts/release/ the program behind make release + plugin.json the manifest + Makefile build, test, lint, release + .goreleaser.yaml builds and packs the release archives + .github/ CI and release workflows +``` + +TypeScript: + +```text +checkmate/ + src/ + index.ts the entry point + run.ts finds the command and turns errors into exit codes + commands/ + index.ts the list of commands + clients.ts the sample command + lib/ environment, SDK client, output, errors, flags, pagination + scripts/release.ts the program behind bun run release + scripts/version.ts the version rules of the release program + plugin.json the manifest + package.json build, test, typecheck and release scripts + .github/ CI and release workflows +``` + +Both projects include tests. The Go project calls the Management API with [go-auth0](https://github.com/auth0/go-auth0). The TypeScript project uses the [auth0](https://www.npmjs.com/package/auth0) Node package and compiles to one executable with Bun. + +### The manifest + +`create` writes `plugin.json` from the values you gave, and it validates the file before it writes anything. This is the manifest of the Go project above: + +```json +{ + "$schema": "https://raw.githubusercontent.com/auth0/auth0-cli-plugins/main/schema/plugin.v1.schema.json", + "id": "auth0/checkmate", + "version": "0.1.0", + "apiVersion": 1, + "description": "A plugin for the Auth0 CLI", + "owner": { + "team": "Platform team", + "bugs": "https://github.com/acme/checkmate/issues" + }, + "platforms": [ + "darwin-arm64", + "darwin-x64", + "linux-x64", + "linux-arm64", + "linux-x64-musl", + "linux-arm64-musl", + "windows-x64" + ], + "commands": [ + { + "name": "checkmate", + "description": "A plugin for the Auth0 CLI", + "interactive": false, + "timeout": "5m", + "output": { + "json": true, + "csv": false, + "agentMode": true + }, + "permissions": { + "scopes": [ + "read:clients" + ], + "hosts": [], + "files": [], + "env": [] + } + } + ] +} +``` + +Every field is described in the [reference](reference.md#pluginjson). + +## Build, link and run + +Build the plugin so that the executable sits next to `plugin.json`, named like the plugin. + +For Go: + +```sh +cd checkmate +go mod tidy +go build -o checkmate . +``` + +`go mod tidy` also writes `go.sum`. For TypeScript: + +```sh +cd checkmate +bun install +bun run build +``` + +The TypeScript build writes `./checkmate` for the machine you are on. + +Then link the directory to the CLI: + +```sh +auth0 plugin dev link +``` + +```text + ▸ Linked auth0/checkmate 0.1.0 from /home/you/checkmate. Run it with: auth0 checkmate + ▸ Every run prints this line on stderr: Running the linked build of auth0/checkmate 0.1.0 from /home/you/checkmate. Its files are not checked against a release. + ▸ auth0/checkmate is not approved on any tenant. Log in with auth0 login, then approve it in a terminal with: auth0 plugin approve auth0/checkmate --tenant +``` + +`link` takes a directory as its argument and defaults to the current one. The directory must hold a valid `plugin.json`, and the executable is the file named like the plugin in it, with `.exe` added on Windows. If your build lands somewhere else, pass its path with `--bin`: + +```sh +auth0 plugin dev link ./checkmate --bin ./checkmate/dist/checkmate +``` + +`link` runs the same manifest checks as `plugin dev validate`, and it refuses a manifest with a problem with the reason `plugin_link_invalid`. It also refuses a missing executable. + +The link is stored in `dev-links.json` in the plugins directory, which is listed in the [paths](reference.md#paths). The CLI reads `plugin.json` and the executable again on every run, so after a change you only rebuild. You link once. Linking the same plugin again replaces the link, and `auth0 plugin dev unlink checkmate` removes it. The build itself is never touched. + +A linked plugin takes the place of an installed plugin with the same name. `auth0 plugin list` marks it: + +```text + ID VERSION PINNED PREVIOUS SIZE LINKED + auth0/checkmate 0.1.0 no 0 B yes +``` + +A linked plugin is held to the same rules as an installed one: the same proxy, the same scope checks, and the same approval by a person. The checks that make no sense for an unpublished build are skipped. Those are the hashes of its files, the lockfile and the revocation list. + +### Approve and run + +A plugin runs on a tenant only after a person approves its permissions there. Approval needs a terminal and a tenant: + +```sh +auth0 plugin approve checkmate --tenant example.us.auth0.com +``` + +The screen shows what the plugin asks for, and your answer is saved for that tenant: + +```text + ▸ auth0/checkmate 0.1.0 A plugin for the Auth0 CLI + +This plugin asks to use your Auth0 tenant example.us.auth0.com: + + Can read clients + Interactive no + +Each command runs with only the permissions it needs. Hosts and files are declared by the publisher and are not enforced by the CLI. + +Approve for example.us.auth0.com? [y/N/d] (d shows the exact scopes) +``` + +`approve` refuses to run without a terminal, in agent mode and with `--no-input`. It reports that only a person can approve a plugin, and it prints the command to run in a terminal: + +```text +=== example.us.auth0.com error + + ▸ Only a person can approve a plugin. Run this in a terminal: auth0 plugin approve auth0/checkmate --tenant example.us.auth0.com. +``` + +Approvals are kept per tenant, so the command also needs a tenant. Pass `--tenant`, or use the default tenant. + +Before the CLI starts any plugin, linked or installed, it needs a registry index that it can verify. The index carries the table that says which Management API calls each scope allows, and the list of secret fields that the proxy removes from responses. A linked build is not in the index, but it is checked against the same table. If the CLI cannot verify an index, the plugin does not start: + +The output below is from the current build, which does not embed a trusted registry key yet. + +```text + ▸ Running the linked build of auth0/checkmate 0.1.0 from /home/you/checkmate. Its files are not checked against a release. + +=== example.us.auth0.com error + + ▸ Checkmate 0.1.0 was not run, because the registry index cannot be trusted: no trusted registry keys are embedded in this build. Reconnect and run: auth0 plugin search. +``` + +With `--json`, the same failure has the reason `plugin_registry_unavailable`. The CLI needs a trusted registry key to check the registry, so builds without one stop here. Creating, building, validating, linking and approving a plugin do not need the index. See [Status](index.md#status). + +Once a build can verify the index, `auth0 checkmate` runs the sample command. It lists the clients of the tenant and needs the `read:clients` scope, which the manifest declares. + +### The development loop + +1. Change the code and rebuild. +2. Run `auth0 ` again. There is nothing to reinstall or relink. +3. If you change `permissions`, whether a command is `interactive`, or the name of a command, the CLI asks for approval again. The approval is keyed on a hash of those fields only. Descriptions, flags, timeouts, output settings and the version do not change the hash. + +You can see the hash that the CLI computes from your manifest with `auth0 plugin info checkmate`. This is the output after the approval above: + +```text + ID auth0/checkmate + VERSION 0.1.0 + DESCRIPTION A plugin for the Auth0 CLI + OWNER Platform team https://github.com/acme/checkmate/issues + REQUIRES none + PLATFORMS darwin-arm64, darwin-x64, linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, windows-x64 + PERMISSIONS 1 scope, 0 hosts, 0 file paths, 0 environment variables + PERMISSIONS HASH sha256:daccec04e517c367feb51cd7a87fcf4933f05f0d1cdd29a2fe4091b52d7a31e9 + VERSIONS + INSTALLED no + APPROVED TENANTS example.us.auth0.com + LINKED yes, a local build runs in place of any installed copy +``` + +After you add `read:users` to the scopes, the same command shows `2 scopes` and a different hash. That is why a changed scope needs a new `plugin approve`. Changing only the description or the timeout leaves the hash as it was. + +## How the CLI starts your plugin + +The sections below describe what the CLI does between your command line and your `main`, so you can tell where a problem comes from. + +### Arguments + +The CLI removes its own flags from the arguments wherever they appear: `--tenant`, `--debug`, `--no-input`, `--no-color`, `--agent-mode`, `--json`, `--json-compact`, `--csv` and `--help`. Everything else reaches your plugin exactly as typed, in the original order, and the CLI does not check it. Everything after a bare `--` goes to your plugin untouched, including a later `--tenant`. + +A manifest cannot declare a flag with one of those names, and it cannot use `-h` as a shorthand. `plugin dev validate` reports both. + +The flags you list under `flags` in `plugin.json` appear in `auth0 --help` and in shell completion. The CLI does not parse or check them. Your code does that. + +`auth0 --help` and `-h` are answered by the CLI from the manifest, and they never reach your plugin. For the sample manifest the help starts like this: + +```text +A plugin for the Auth0 CLI + +Management API scopes: read:clients +Timeout: 5m +Output: --json, --json-compact, agent mode + +... +``` + +A command that is not the root command runs as `auth0 `. The CLI passes the command name to your plugin as its first argument. Type the command name directly after the plugin name. If a flag comes first, the CLI may not find the command. A call of the root command that has the name of one of its subcommands among its arguments, before a bare `--`, is refused with the reason `plugin_ambiguous_subcommand`, because it is not clear which command was meant. Write `--flag=value`, or put the argument after `--`. + +### Environment + +The plugin gets a small environment, and nothing else is inherited. These variables are set for every run: + +| Variable | Meaning | +| --- | --- | +| `AUTH0_PLUGIN_DOMAIN` | Address of the CLI proxy, as `host:port`. It is not the tenant domain. | +| `AUTH0_PLUGIN_MANAGEMENT_TOKEN` | A secret for this run only, which lets the plugin use the proxy. It is not an Auth0 token. | +| `AUTH0_PLUGIN_CA_CERT` | A PEM file with the certificate of the proxy. | +| `SSL_CERT_FILE`, `NODE_EXTRA_CA_CERTS` | The same certificate file, for runtimes that read these variables. | +| `AUTH0_PLUGIN_TENANT` | The tenant the run is for. | +| `AUTH0_PLUGIN_JSON`, `AUTH0_PLUGIN_JSON_COMPACT`, `AUTH0_PLUGIN_CSV` | `true` or `false`, from the output flags. Agent mode turns `AUTH0_PLUGIN_JSON` on. | +| `AUTH0_PLUGIN_AGENT_MODE`, `AUTH0_PLUGIN_NO_INPUT`, `AUTH0_PLUGIN_NO_COLOR`, `AUTH0_PLUGIN_DEBUG` | `true` or `false`, from the global flags. | + +Besides these, the plugin gets a short list of variables from your own environment, such as `PATH`, `HOME` and `TERM`, and the variables that its command declares under `permissions.env`. Names that start with `AUTH0_` are never passed through, because they belong to the CLI and its credentials. The [reference](reference.md#environment-variables-your-plugin-receives) has the complete list. + +### Input, output and exit codes + +Your plugin writes results to stdout and everything else to stderr. Both go straight through to the terminal. Standard input is passed on when the command is `interactive` or when standard input is not a terminal, for example when something is piped in. Otherwise the plugin gets the null device. The plugin starts in the directory where you ran `auth0`. + +The CLI does not pass the exit code of your plugin on. A non-zero code makes `auth0` exit with 1 and report the reason `plugin_exit`. The JSON error carries the code your plugin used, as `exitCode` in its details. The starter projects use 0 for success, 1 when the command fails and 2 when the arguments are wrong. + +### Timeouts and stopping + +A command has a `timeout` in `plugin.json`, which is a Go duration such as `30s` or `10m`. The starter projects set `5m`. A command with no timeout gets the default of the registry index, which is 10 minutes unless the index says otherwise. When the time is up, or when the user presses Ctrl-C, the CLI stops the plugin and everything it started: + +- On macOS and Linux, a plugin that is not interactive runs in its own process group. The CLI sends `SIGTERM` to the group, and `SIGKILL` if anything is still running after 5 seconds. +- On Windows, a plugin that is not interactive runs in a job object, which is a group of processes that Windows can end together. The CLI sends a Ctrl-Break event, which is best effort, and ends the job after 5 seconds. +- An interactive plugin stays in the process group of the CLI, so that the terminal keeps working. On macOS and Linux, a timeout then stops only the plugin process itself. + +Both starter projects cancel their requests on Ctrl-C and `SIGTERM`. A run that goes over its timeout ends with the reason `plugin_timeout`. A run that was interrupted ends with `plugin_interrupted`. + +## Call the Management API + +Your plugin never receives a real access token. It calls the CLI proxy instead, and the proxy replaces the per-run secret with the real token. + +The client is set up in one file of each starter project: `plugin/client.go` in Go, and `src/lib/env.ts` with `src/lib/client.ts` in TypeScript. The client uses the address in `AUTH0_PLUGIN_DOMAIN` and the secret in `AUTH0_PLUGIN_MANAGEMENT_TOKEN`, and it trusts the certificate in `AUTH0_PLUGIN_CA_CERT`. The Go client loads that certificate itself, because Go ignores `SSL_CERT_FILE` on macOS and Windows. Bun reads `NODE_EXTRA_CA_CERTS`, so the TypeScript project needs no extra code for it. + +The proxy applies these rules to every call: + +- It forwards only calls under `/api/v2/`, the Management API. +- It forwards a call only when a scope that the invoked command declared allows it. A command that declares no scopes cannot call the API. +- It uses the scopes of the command that was run, not the union of the scopes of all commands in the manifest. +- It removes secret fields, such as client secrets, from the responses. +- It refuses a call that is not in its scope table, and a path that is not in canonical form. + +When a call needs a scope that your command did not declare, the proxy answers with HTTP 403 and an `errorCode` of `insufficient_scope`. The message names the scope: + +```text +Plugin auth0/checkmate did not declare scope "read:clients" required for GET /api/v2/clients. Add it to permissions.scopes in plugin.json. +``` + +The starter projects print `error: the CLI blocked this call.`, then that message, then the command that approves the new permissions, `auth0 plugin approve `. After the run, the CLI also prints one summary line on stderr. It counts the calls that were denied and says which permissions to review. The [reference](reference.md#proxy-error-codes) lists every error code that the proxy returns. + +If the session is a user login, the proxy cannot renew an expired token without the user. Your plugin then gets the error code `session_expired`. A session made with client credentials is renewed for you, up to three times in one run. + +## Scopes and permissions + +Each command declares what it needs under `permissions` in `plugin.json`: + +| Field | Enforced by the CLI | What it does | +| --- | --- | --- | +| `scopes` | Yes | The Management API scopes the command may use. The proxy allows a call only if one of these scopes allows it. | +| `env` | Yes | Names of environment variables from the user's environment that the command may read. Names that start with `AUTH0_` are refused. | +| `hosts` | No | Hosts the plugin says it contacts. Each one needs a `reason`. | +| `files` | No | Paths the plugin says it reads or writes. Each one needs `access` (`read` or `write`) and a `reason`. | + +Hosts and files are declarations by the publisher. The consent screen says so. A plugin runs as a normal process with the permissions of the user, so it can reach hosts and files that it did not declare. Write honest reasons, because the person who approves the plugin reads them. + +The consent screen shows the union of the permissions of all commands. At run time, the proxy enforces only the scopes of the command that runs. + +## Add a command + +For Go: + +1. Copy `commands/clients.go` to `commands/.go`. Change the name, the help text and the code in the run function, and keep the call to `Register` in `init`. +2. Add the command to `commands` in `plugin.json`, with its description and the scopes it needs. +3. Rebuild. + +For TypeScript: + +1. Copy `src/commands/clients.ts` to `src/commands/.ts`. Change the name, the help text and the code in `run`. +2. Import it in `src/commands/index.ts` and add it to the list. +3. Add the command to `commands` in `plugin.json`, then rebuild. + +A command whose name is not the name of the plugin runs as `auth0 `. Declare its flags in the code and list them under `flags` in `plugin.json`. Both starter projects read `--json` and the other output settings from the environment, so the command only chooses between printing JSON and a table. + +### Output modes + +A command states which output modes it supports, under `output`: + +- `output.json: true` allows `--json` and `--json-compact`. +- `output.csv: true` allows `--csv`. +- `output.agentMode: true` allows the command to run in agent mode. + +When a user asks for a mode that the command did not declare, the CLI refuses before it starts the plugin, with the reason `plugin_output_unsupported`. A manifest with no `output` block declares nothing, so `--json` and agent mode are both refused. The starter projects declare `json` and `agentMode`. + +### Interactive commands + +A command with `interactive: true` needs a terminal. The CLI refuses it with the reason `plugin_interactive` when `--no-input` is set, in agent mode, and when standard input or output is not a terminal. + +## Check the manifest + +`auth0 plugin dev validate` checks `plugin.json` the way the registry checks a plugin when it is published. It works offline, takes a directory or a file (the default is the current directory), and exits with 1 when it finds a problem. + +```sh +auth0 plugin dev validate +``` + +```text + ▸ The plugin manifest is valid. +``` + +It prints every problem, with the path of the field, not only the first one. Problems against the JSON schema come first. The checks that the schema cannot express run only when the schema check passes, so fix the fields that are listed and run the command again. + +```text + PATH PROBLEM + commands[0].flags[0].name flag "json" collides with a flag owned by the CLI + commands[0].permissions.env[0].name "AUTH0_DOMAIN" uses a prefix reserved by the CLI (AUTH0_) + +=== error + + ▸ The plugin manifest has 2 problems. +``` + +A schema problem reads like this. The long patterns are shortened here with "...": + +```text + PATH PROBLEM + commands[0].timeout 'soon' does not match pattern '^([0-9]+(\\.[0-9]+)?(ns|us|ms|s|m|h))+$' + owner.bugs 'http://example.com/issues' does not match pattern '^https://...' + version 'v1.0' does not match pattern '^(0|[1-9][0-9]*)\\....' +``` + +The CLI also refuses a plugin ID whose name is the name of a core command: + +```text + PATH PROBLEM + id auth0 already has a command named "apps". Plugin names cannot shadow core commands +``` + +With `--bin `, the command also checks that the executable exists and can run. With `--json`, the result is printed on stdout. A valid manifest and an invalid one have the same shape: + +```json +{ + "valid": false, + "problems": [ + { + "path": "--bin", + "message": "stat ./checkmate: no such file or directory" + } + ] +} +``` + +On failure the process exits with 1, and the error carries the reason `plugin_manifest_invalid`. With `--json`, that error is printed on stderr, and its `details.problems` list holds the same problems as the `problems` list on stdout. + +## Run the checks of the starter project + +The starter projects have tests and a lint or typecheck step. For Go, run `make test` and `make lint`. For TypeScript, run `bun test` and `bun run typecheck`. On a new project, all of them pass. The CI of the starter runs the tests and the build on Linux, macOS and Windows for every pull request, and again before every release. + +## Next + +- [Releasing a plugin](releasing.md) explains the release command and the release workflow of the starter project. +- [Reference](reference.md) lists every field of `plugin.json`, every variable, every path and every reason code. +- [Security and trust model](security.md) explains what the CLI protects and what it cannot. diff --git a/docs/plugins/getting-started.md b/docs/plugins/getting-started.md new file mode 100644 index 000000000..c18e92b45 --- /dev/null +++ b/docs/plugins/getting-started.md @@ -0,0 +1,228 @@ +--- +layout: default +title: Getting started +parent: Plugins +nav_order: 1 +--- + +This page shows you how to find a plugin, look at what it asks for, install it, approve it for your tenant, and run it. The examples use a plugin called `checkmate`. Plugin commands are Experimental, so their output and behavior can change between releases. + +The steps on this page show the flow that plugins follow once the registry is open. The search and info output below is sample output. For what works today, see [Status](index.md#status). + +## What a plugin is + +A plugin is a program that the Auth0 CLI downloads, checks and starts for you. You run it as `auth0 `, and the CLI is the only supported way to start it. The CLI checks your approval and gives the plugin a proxy session, so running the plugin binary directly skips those checks and gives it no Auth0 access. + +Searching, inspecting and installing plugins does not need a login. Running a plugin does, because the plugin calls the Management API (the API that reads and changes your tenant) through your session. Log in first with `auth0 login`. For CI and agents, [Agent mode and automation](automation.md) describes an environment variable mode that needs no saved login. + +## Find a plugin + +List every plugin in the registry: + +```bash +auth0 plugin search +``` + +```text + ID VERSION INSTALLED DESCRIPTION + auth0/checkmate 1.0.0 no Run a tenant security audit +``` + +Add a word to filter the list. The word is matched against the plugin ID and the description, and the case does not matter: + +```bash +auth0 plugin search security +``` + +Before you install anything, look at what the plugin asks for: + +```bash +auth0 plugin info checkmate +``` + +```text + ID auth0/checkmate + VERSION 1.0.0 + DESCRIPTION Run a tenant security audit + OWNER CheckMate team https://example.com/checkmate/bugs + REQUIRES none + PLATFORMS darwin-arm64, darwin-x64, linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, windows-x64 + PERMISSIONS 5 scopes, 1 host, 1 file path, 1 environment variable + PERMISSIONS HASH sha256:9b991b1b3d0c523b4d7aa0f05b5e970f3621bae92d157d563a0458ba28a5b9a7 + VERSIONS 1.0.0 (latest) + INSTALLED no + APPROVED TENANTS none +``` + +`OWNER` is the team to contact about the plugin. `PERMISSIONS` counts what the plugin declares, and the install shows you the full list. `PERMISSIONS HASH` is a fingerprint of those permissions, and it changes when they change. You can give a plugin as `checkmate`, as `auth0/checkmate`, or with a version as `checkmate@1.0.0`. The registry is called `auth0`, and it is the only one for now. + +## Install a plugin + +```bash +auth0 plugin install checkmate +``` + +The CLI checks the registry, shows you what the plugin asks for on the consent screen (the list of permissions that you approve or decline), and downloads the plugin only after you agree: + +```text + ▸ auth0/checkmate 1.0.0 Run a tenant security audit + ▸ Download: 71.2 MB (darwin-arm64) + +This plugin asks to use your Auth0 tenant acme.us.auth0.com: + + Can read clients, connections, tenant settings + Can change clients (update), connections (update) + Hosts api.github.com Check SDK versions + Files $CWD (write) Save the report + Environment GITHUB_TOKEN Call the GitHub API + Interactive no + +Each command runs with only the permissions it needs. Hosts and files are declared by the publisher and are not enforced by the CLI. + +Approve for acme.us.auth0.com? [y/N/d] (d shows the exact scopes) +``` + +The last line of the consent screen names the tenant that you pass with `--tenant`, or your default tenant. Type `y` to approve, `d` to see the exact scope names, and anything else to decline. After `d`, the CLI lists the scopes and asks again with `[y/N]`. + +If you decline, nothing is downloaded or installed. The command exits with code 0, because declining is not a failure: + +```text + ▸ auth0/checkmate was not approved, so nothing was installed. To review the permissions again, run: auth0 plugin install checkmate +``` + +If the CLI has no tenant to ask about, because you passed no `--tenant` and have no default tenant, it installs the plugin without asking. It then tells you to log in and approve the plugin later. + +Each part of the screen means something different: + +| Row | What it means | +| --- | --- | +| Can read, Can change | The Management API scopes the plugin may use. The proxy that the CLI starts for the plugin refuses calls outside the scopes of the command that you ran. A scope that is neither a read nor a change is listed under `Other`. A plugin with no scopes shows `Auth0 API none`. | +| Hosts | Other hosts the plugin says it contacts, with the reason. | +| Files | Paths the plugin says it reads or writes, with the reason. | +| Environment | Environment variables the plugin asks to receive, with the reason. | +| Interactive | `yes` when at least one command needs a terminal. | + +Hosts and files come from the plugin's publisher. The CLI does not enforce them. Scopes and environment variables are enforced. Read the reasons before you approve, and see [Security and trust model](security.md) for how each part is checked. + +After you answer `y`, the CLI downloads the file, checks it against the hash in the signed registry index (the list of plugins and versions that the CLI trusts), and installs it. If your login does not have the scopes the plugin asks for, the CLI warns you. The warning gives the `auth0 login` command that adds them. If you logged in with client credentials, the warning says the application needs a grant for those scopes on the Auth0 Management API instead. The CLI never starts a login on its own. + +An install that you approve ends with a line like this one. The approval is saved for this tenant only: + +```text + ▸ Installed auth0/checkmate 1.0.0. +``` + +## Run the plugin + +The plugin is now a command of the CLI, and it shows up under its own heading in `auth0 --help`: + +```text +Plugin Commands: + checkmate Run a tenant security audit +``` + +Read its help first. The CLI builds the help from the plugin's manifest, so it shows the scopes and the timeout of each command: + +```bash +auth0 checkmate --help +``` + +Then run it: + +```bash +auth0 checkmate +``` + +Before the plugin starts, the CLI runs these checks in order. It stops at the first one that fails: + +1. Plugins are not turned off on this machine. +2. The tenant that you asked for matches `AUTH0_DOMAIN`, if that variable fixes the tenant. +3. The installed files match the hashes that were checked at install time. +4. The registry index is recent enough, and the version has not been revoked. +5. The command can run the way you called it. For example, it has a terminal if it needs one, and the output flags you passed are declared in its manifest. +6. You approved the plugin for the tenant. +7. Your session for that tenant is open, and it belongs to the approved tenant. + +The plugin never sees your token. The CLI starts a local proxy for the run and gives the plugin its address and a temporary token. When the plugin calls the Management API through the proxy, the proxy swaps in your real token and allows only the scopes that the command declared. + +When the plugin finishes, the CLI prints one line if the proxy denied calls. This tells you when a plugin could not get data it asked for, even if the plugin hid the error: + +```text + ▸ Plugin auth0/checkmate: 2 API calls were denied (read:users for GET /api/v2/users, GET /oauth/token). Review permissions.scopes in plugin.json. +``` + +The second call is outside `/api/v2`, so the proxy refuses it without checking scopes. The last sentence is written for the plugin's author. If you see this line, tell the owner listed as `OWNER` in `auth0 plugin info`. + +### Flags and arguments + +The CLI reads its own flags wherever you put them: `--tenant`, `--debug`, `--no-input`, `--no-color`, `--agent-mode`, `--json`, `--json-compact`, `--csv` and `--help`. It passes every other argument to the plugin as you typed it, without checking it. Everything after a bare `--` goes to the plugin untouched. + +Type the name of a subcommand directly after the plugin name, as in `auth0 checkmate scan --path .`. The CLI refuses a call of the plugin's root command that has a subcommand name among its arguments, before a bare `--`, with the reason `plugin_ambiguous_subcommand`. A flag placed before the subcommand is the usual cause, because the CLI cannot tell the subcommand from the value of that flag. + +The output flags work only when the plugin declares them. Asking for `--csv` from a command that does not declare it fails with the reason `plugin_output_unsupported`. + +## Approve on another tenant + +Approval belongs to a tenant. If you run the plugin on a tenant that you have not approved it for, the run stops before anything else happens: + +```text +=== other.us.auth0.com error + + ▸ checkmate is not approved for other.us.auth0.com. Run: auth0 plugin approve auth0/checkmate --tenant other.us.auth0.com +``` + +Run the command from the message in a terminal. It shows the same consent screen as the install and records your answer: + +```bash +auth0 plugin approve checkmate --tenant other.us.auth0.com +``` + +An approval holds for later versions of the plugin that ask for the same permissions. If a new version asks for anything else, the CLI asks again, as the next section shows. + +## Update a plugin + +```bash +auth0 plugin update checkmate +``` + +```text + ▸ Updated auth0/checkmate from 1.0.0 to 1.1.0. +``` + +When the new version asks for different permissions, you approve them before the update happens. If you already approved the installed version on this tenant, you see only what changed. If you did not, you see the full consent screen. When the new version asks for the same permissions, the CLI asks nothing. This is an example of the consent screen for a changed version: + +```text +auth0/checkmate 1.1.0 asks for different permissions on tenant acme.us.auth0.com + Added can read connections + Removed can read logs + Added host new.example.com New + Removed host old.example.com Old + Changed file $CWD (write) Save the report + Changed variable GITHUB_TOKEN A new reason + Added command fresh (interactive) + Removed command gone + Changed command toggle is now interactive +Each command runs with only the permissions it needs. Hosts and files are declared by the publisher and are not enforced by the CLI. +``` + +If you decline, the old version stays active and the command says so: + +```text + ▸ auth0/checkmate was not updated, because the new permissions were not approved. 1.0.0 stays active. +``` + +If there is no terminal to ask in, or you use `--no-input` or agent mode, the old version also stays active. The command then fails and says why. See [Agent mode and automation](automation.md). + +Once a day, after a plugin runs, the CLI can print a hint when a newer version is available or when the installed version is deprecated: + +```text + ▸ checkmate 1.1.0 is available. Run: auth0 plugin update checkmate +``` + +The hint shows only in a terminal, never with `--json`, `--no-input` or agent mode. Set `AUTH0_CLI_PLUGIN_UPDATE_HINT=false` to turn it off. + +## Next + +- [Managing plugins](managing.md): list, pin, roll back, approve and uninstall. +- [Agent mode and automation](automation.md): use plugins in CI and from agents. +- [Troubleshooting](troubleshooting.md): what the errors mean and how to fix them. diff --git a/docs/plugins/index.md b/docs/plugins/index.md new file mode 100644 index 000000000..74a64966b --- /dev/null +++ b/docs/plugins/index.md @@ -0,0 +1,54 @@ +--- +layout: default +title: Plugins +has_children: true +nav_order: 8 +--- + +Plugins add commands to the Auth0 CLI. You can install a plugin from the Auth0 registry and run it as `auth0 `, or build one and release it through the same registry. The plugin commands are Experimental, so their details can change between releases. + +Plugins are Experimental, and a few parts of the registry and release path are still being built. The current state is on this page under [Status](#status). + +## What a plugin is + +A plugin is a program that the CLI downloads, checks and starts. The CLI is the only supported way to start it. The CLI checks your approval and gives the plugin a proxy session, so running the plugin binary directly skips those checks and gives it no Auth0 access. + +Each plugin has a `plugin.json` manifest. The manifest lists the plugin's commands, and for each command, the scopes of the Auth0 Management API (the API that reads and changes your tenant) that it uses. It also lists the other hosts the plugin contacts, the files it reads or writes, and the environment variables it receives. The CLI shows you this list before you approve the plugin. + +## Who it is for + +If you use the CLI, you can add a command that someone else built, such as a security audit, and decide what it may do on each tenant. If you build plugins, you can ship a command that uses the CLI's login, so you do not write your own token handling. If you run the CLI in CI or with an AI agent (a program that runs CLI commands for you), you can install and run plugins without a person at the keyboard, as long as a person approves the permissions first. + +## The short version + +```bash +auth0 plugin install checkmate +auth0 checkmate +``` + +The first command shows you what the plugin asks for, and it installs the plugin after you approve it for your tenant. The second command runs the plugin. Before every run, the CLI checks the installed files, the signed registry index and your approval. It then starts the plugin with a local proxy that holds your login. The plugin gets only a temporary address and token, and the proxy allows only the scopes that the command declares. + +## Status + +Plugins are Experimental, so commands, output and file formats can change between releases. The rest of the design is in place, and these parts are still being built: + +- **Registry root.** The CLI checks the signed registry metadata against a root of trust built into the binary. That root is not in the CLI yet, so no build can install or run a plugin from the registry. A linked build stops at the same check. Plugins open to the public once the root is in place. +- **Signed releases.** The release workflows do not sign archives or record build attestations yet. The CLI will verify both once they are added. +- **Catalog.** A plugin must be listed in the registry catalog before `auth0 plugin install` can find it. A GitHub release alone does not list it. +- **Per-run tokens.** Plugins currently use the same kind of session as the CLI, through the local proxy, and the proxy limits them to the scopes each command declares. Tokens scoped to a single run are planned. + +## Pages in this section + +- [Getting started](getting-started.md): find a plugin, install it, approve it and run it. +- [Managing plugins](managing.md): list, inspect, pin, update, roll back, approve tenants, uninstall, and find the files on disk. +- [Building a plugin](building.md): create a project, add commands, link a local build and validate the manifest. +- [Releasing a plugin](releasing.md): set the version, check the release archive, and what the release workflow does. +- [Architecture](architecture.md): the components, the install and run flows, and the files on disk, with an interactive diagram. +- [Security and trust model](security.md): how the registry, the hashes, the approvals and the proxy protect you, and what a plugin cannot do. +- [Agent mode and automation](automation.md): what agents and CI can do, what they are refused, and how to pin an approval. +- [Troubleshooting](troubleshooting.md): the errors you can see, their reason codes and the fixes. +- [Reference](reference.md): the `plugin.json` fields, environment variables, paths, platform names and reason codes. + +## Next + +Start with [Getting started](getting-started.md) if you use plugins, or with [Building a plugin](building.md) if you write one. diff --git a/docs/plugins/managing.md b/docs/plugins/managing.md new file mode 100644 index 000000000..ba982fe73 --- /dev/null +++ b/docs/plugins/managing.md @@ -0,0 +1,225 @@ +--- +layout: default +title: Managing plugins +parent: Plugins +nav_order: 2 +--- + +This page covers what you do with a plugin after the first install. You can list and inspect the plugins you have, pin a version or go back to an older one, update, approve a tenant, uninstall, and find the files on disk. + +## List what is installed + +```bash +auth0 plugin list +``` + +```text + ID VERSION PINNED PREVIOUS SIZE + auth0/checkmate 1.1.0 no 1.0.0 71.2 MB +``` + +| Column | Meaning | +| --- | --- | +| `VERSION` | The version that runs. | +| `PINNED` | `yes` when you installed an exact version, so `update --all` leaves the plugin alone. | +| `PREVIOUS` | The version that was active before. It stays on disk, so you can go back without a download. | +| `SIZE` | The space the plugin uses on disk. | + +If you linked a plugin from a local build in dev mode (the mode that turns on the author commands, see [Building a plugin](building.md)), a `LINKED` column appears. See [Building a plugin](building.md). + +The command also takes `--json` and `--csv`. The JSON entries have the keys `id`, `name`, `version`, `pinned`, `previous`, `sizeBytes` and `linked`. + +## Look at a plugin + +`auth0 plugin info` shows one version of a plugin, whether it is installed, and the tenants where you approved it: + +```bash +auth0 plugin info checkmate +auth0 plugin info checkmate@1.0.0 +``` + +```text + ID auth0/checkmate + VERSION 1.1.0 + DESCRIPTION Run a tenant security audit + OWNER CheckMate team https://example.com/checkmate/bugs + REQUIRES none + PLATFORMS darwin-arm64, darwin-x64, linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, windows-x64 + PERMISSIONS 5 scopes, 1 host, 1 file path, 1 environment variable + PERMISSIONS HASH sha256:9b991b1b3d0c523b4d7aa0f05b5e970f3621bae92d157d563a0458ba28a5b9a7 + VERSIONS 1.1.0 (latest), 1.0.0 + INSTALLED 1.1.0 + APPROVED TENANTS acme.us.auth0.com +``` + +A few values need explaining: + +- `REQUIRES` is the range of CLI versions that the plugin works with. It reads `auth0-cli >=1.38.0` when the plugin sets one. +- `VERSIONS` marks `(latest)` and `(revoked)`. A revoked version cannot be installed or run. +- `INSTALLED` adds `(failed verification)` when the files on disk no longer match what was checked at install time. The message that comes with it tells you to reinstall. +- `APPROVED TENANTS` adds `(different permissions)` to a tenant where you approved an older set of permissions than the ones shown. +- A `HOMEPAGE` row appears when the plugin sets one. +- A `DEPRECATED` row appears when the registry marks the version as deprecated. The row shows the message, and `(use instead)` when there is a replacement. A deprecated plugin keeps working. +- For a plugin that you linked from a local build in dev mode, `info` describes the manifest of that build and adds a `LINKED` row. + +The `PERMISSIONS HASH` is the value that you pin in CI. See [Agent mode and automation](automation.md). + +`auth0 plugin info checkmate --json` returns the same fields for scripts, including `permissionsHash`. + +## Install an exact version, or go back + +Install an exact version with `name@version`: + +```bash +auth0 plugin install checkmate@1.0.0 +``` + +An exact version is pinned. `auth0 plugin update --all` skips pinned plugins and tells you which newer version it left out. The install also tells you how to move on: + +```text + ▸ auth0/checkmate 1.0.0 is already installed. + ▸ auth0/checkmate is pinned to 1.0.0, so `auth0 plugin update --all` leaves it alone. Move it to the latest version with: auth0 plugin update checkmate +``` + +The same command is how you go back. The CLI keeps the active version and the one before it on disk. Installing the previous version again only switches to it, without a download: + +```text + ▸ Switched auth0/checkmate from 1.1.0 to 1.0.0. + ▸ auth0/checkmate is pinned to 1.0.0, so `auth0 plugin update --all` leaves it alone. Move it to the latest version with: auth0 plugin update checkmate +``` + +A version older than the previous one is downloaded again. If the version that you switch to asks for different permissions than the active one, a terminal asks you to approve them first, as for an update. A version that the registry has revoked is refused, and the message gives the reason. + +`name@version` takes `MAJOR.MINOR.PATCH` only. A pre-release version such as `1.0.0-beta.1` is not accepted. + +## Update + +```bash +auth0 plugin update checkmate +auth0 plugin update --all +``` + +`update checkmate` moves the plugin to the latest version and clears its pin. `update --all` updates every plugin that is not pinned, and prints one line for each plugin: + +```text + ▸ auth0/checkmate is pinned to 1.0.0 and was skipped. Version 1.1.0 is available: auth0 plugin update checkmate + ▸ auth0/other 2.0.0 is up to date. +``` + +The new version is downloaded and verified before anything changes, so an update either completes or leaves the old version active. When the new version asks for different permissions, the CLI asks you to approve them for the current tenant. It shows only what changed when you approved the installed version on that tenant. The old version stays active if you decline, if there is no terminal, in `--no-input` mode, or in agent mode. + +Declining at a terminal is not a failure, so the command exits with code 0. With no terminal, in `--no-input` mode or in agent mode, a named update fails. `--all` fails when some plugin could not be updated. Pinned plugins and plugins you declined do not count as failures. + +Approval is per tenant, so other tenants are asked at their next run. When the tenant you are on has not approved the result, install and update print a line like this one, with the command that approves it: + +```text + ▸ auth0/checkmate is not approved for acme.us.auth0.com yet. Approve it in a terminal with: auth0 plugin approve auth0/checkmate --tenant acme.us.auth0.com +``` + +With `--json`, `update` prints one entry for each plugin, with the keys `id`, `version`, `previousVersion`, `action`, `pinned` and `approvalRequired`. When they apply, it also prints `approveCommand`, `available` and `error`. The `action` is one of `installed`, `switched`, `unchanged`, `skipped`, `declined` or `failed`. A new version that was downloaded is reported as `installed`. + +Installing and updating need a current copy of the registry index. They fail when the registry cannot be reached and the saved copy is more than a day old. Searching and running a plugin fall back to the saved copy, and a plugin keeps running from it for up to 30 days. See [Troubleshooting](troubleshooting.md). + +## Approve a tenant + +A plugin runs on a tenant only after you approved its permissions there. Install asks about the tenant you are on. For another tenant, use `approve`: + +```bash +auth0 plugin approve checkmate --tenant acme.us.auth0.com +``` + +This needs a terminal. In agent mode, with `--no-input`, or in a script, it fails and prints the command to run, because only a person approves a plugin. `approve` always shows the full consent screen, even when you approved an older version. It also works for a plugin that you linked from a local build in dev mode. Without `--tenant` and without a default tenant, it fails with the reason `plugin_no_tenant`. + +```text + ▸ Approved auth0/checkmate 1.0.0 for acme.us.auth0.com. +``` + +Approval is keyed to the permissions, not the version. A new version that asks for the same permissions needs no new approval. A version that asks for anything else stops with this message until you approve again: + +```text + ▸ checkmate changed what it asks for since you approved it for acme.us.auth0.com. Run: auth0 plugin approve auth0/checkmate --tenant acme.us.auth0.com +``` + +A tenant keeps one approval for each plugin, so approving new permissions replaces the earlier ones. Logging out of a tenant with `auth0 logout` deletes that tenant's approvals, so an approval never outlives the login it was given in. Logging in again means approving again. + +## Uninstall + +```bash +auth0 plugin uninstall checkmate +``` + +In a terminal, the CLI asks first: + +```text +Are you sure you want to uninstall auth0/checkmate? This removes every version and its approvals. (y/N) +``` + +If you confirm, the CLI removes every version, the lockfile entry and the approvals on every tenant. It then tells you how much space it freed: + +```text + ▸ Uninstalled auth0/checkmate and freed 71.2 MB. +``` + +Answer anything but `y` and nothing is removed. Use `--force` to skip the question. Without a terminal, or with `--no-input`, uninstall does not ask. In agent mode it refuses to run without `--force`, and it says `this is a destructive command; re-run with --force to proceed without a confirmation prompt`. `auth0 plugin remove` is an alias. Uninstall works without the registry and when plugins are turned off. It also removes an `approvals.json` file that cannot be read. + +## Where the files are + +All plugin state is in a `plugins` folder next to the CLI's `config.json`. The CLI builds that path from the `HOME` environment variable on every operating system. On macOS and Linux that is `~/.config/auth0/plugins`. On Windows it is the same relative path, under the folder that `HOME` names. `XDG_CONFIG_HOME` is not used. + +```text +plugins/ + plugins.lock.json installed plugins: versions, hashes, active and previous version + .lock the lock for the plugins and the lockfile + .approvals.lock the lock for approvals.json + .dev-links.lock the lock for dev-links.json + approvals.json approvals for each tenant, keyed by the permissions hash + hint.json when the update hint was last shown + dev-links.json plugins linked in dev mode, only if you link one + registries/auth0/metadata/ the latest signed root, timestamp, snapshot and targets files + registries/auth0/roots/ the roots after the first one, such as 2.root.json, only after a key rotation + registries/auth0/index.json the last verified registry index + registries/auth0/state.json the newest index version accepted, and when the index was last refreshed + registries/auth0/.lock the lock for refreshing the index + store/auth0/checkmate/1.1.0/ the plugin: the executable checkmate (checkmate.exe on Windows) and plugin.json + store/auth0/checkmate/1.0.0/ the previous version + tmp/ downloads in progress +``` + +The folders, the lockfile, the approvals and the manifests are readable only by your user. The executable is the one file marked as runnable. The active version is a field of the lockfile and not a symbolic link, so switching versions is one file write. Do not edit these files. Before every run, the CLI checks the executable and the manifest against the hashes in the lockfile. A changed file stops the run with the reason `plugin_verification_failed`. + +## Turn plugins off + +Set `AUTH0_CLI_PLUGINS_DISABLED=true` on machines where you do not want plugins. Install, update, approve and running a plugin then fail with a message that names the variable. Search, list, info and uninstall keep working. Any value that is not `true` or `false` also turns plugins off, so a typo cannot leave them on by accident. + +## Registry mirror and proxy + +The CLI downloads through the proxy named in `HTTPS_PROXY`. If you cannot reach the registry address, you can point the `auth0` registry at a mirror in `config.json`. The `url` is the base URL of the directory that holds the registry files, not the URL of `index.json`. The CLI adds a slash at the end when it is missing, and it sends a query string on the URL with every request. The metadata and the index are still checked against the root inside the CLI, and plugin files are still downloaded only from GitHub release hosts: + +```json +{ + "registries": [ + { "name": "auth0", "url": "https://mirror.example.com/registry/" } + ] +} +``` + +A mirror must serve the same files as the registry, under that base URL: + +```text +timestamp.json +.root.json +.snapshot.json +.targets.json +targets/.index.json +plugins/.json +``` + +`` is a version number, and `` is the hash of the index. The default URL is `https://raw.githubusercontent.com/auth0/auth0-cli-plugins/registry/`. The URL must use `https`. In `AUTH0_CLI_AUTH_MODE=env` the CLI does not read `config.json`, so the mirror does not apply there. + +The `auth0` registry is the only one. Every install comes from the signed index, so the CLI does not install from a local file. + +## Next + +- [Agent mode and automation](automation.md): approvals and updates without a person at the keyboard. +- [Troubleshooting](troubleshooting.md): errors and their reason codes. +- [Security and trust model](security.md): what the checks protect against. diff --git a/docs/plugins/reference.md b/docs/plugins/reference.md new file mode 100644 index 000000000..c4e72eb1d --- /dev/null +++ b/docs/plugins/reference.md @@ -0,0 +1,222 @@ +--- +layout: default +title: Reference +parent: Plugins +nav_order: 9 +--- + +This page lists every field of `plugin.json`, the environment variables that the CLI and your plugin read, the files the CLI keeps on disk, the platform names, the reason codes and the error codes of the proxy. Use it when you need an exact name or value. + +## plugin.json + +The manifest is a JSON object for `apiVersion` 1. The commands `plugin dev create`, `plugin dev validate` and `plugin dev link` check it strictly: a field that the schema does not list is an error. The CLI reports a value that breaks a rule with the path of the field. The fields are listed below. + +### Top level + +| Field | Required | Rules | +| --- | --- | --- | +| `$schema` | No | A string. The URL of the schema that the manifest was written against. | +| `id` | Yes | `registry/name`, in lowercase. Each part holds lowercase letters, digits and hyphens, and starts with a letter or a digit. The CLI knows one registry, `auth0`, so the ID is `auth0/`. The name cannot be the name of a core command such as `apps`. | +| `version` | Yes | A semantic version without a leading `v`, such as `1.2.0`. The schema also accepts a prerelease or build suffix. The CLI installs only `MAJOR.MINOR.PATCH`. | +| `apiVersion` | Yes | The integer `1`. | +| `description` | Yes | One line of 1 to 120 characters, with no line breaks. | +| `homepage` | No | An HTTPS URL of the home page of the plugin. | +| `owner` | Yes | An object with `team` and `bugs`. No other key is allowed. | +| `owner.team` | Yes | The team or person that owns the plugin. At least one character. | +| `owner.bugs` | Yes | An HTTPS URL where people report problems. | +| `engines` | No | An object with one key, `auth0-cli`. | +| `engines.auth0-cli` | Yes, inside `engines` | One or more comparators separated by spaces, such as `>=1.39.0 <2.0.0`. Each comparator is `>=`, `<=`, `>`, `<` or `=` followed by `MAJOR.MINOR.PATCH`. All of them must match. The CLI compares the three numbers and ignores a prerelease suffix. | +| `platforms` | Yes | A list of at least one platform name, with no repeats. See [Platforms](#platforms). | +| `commands` | Yes | A list of at least one command. | + +### Command + +| Field | Required | Rules | +| --- | --- | --- | +| `name` | Yes | Lowercase letters, digits and hyphens, starting with a letter. A command with the name of the plugin is the root command, and it runs as `auth0 `. Any other command runs as `auth0 `. Names are unique within the manifest. | +| `description` | Yes | At least one character. Shown in the help of the command. | +| `flags` | No | A list of flags. See [Flag](#flag). | +| `interactive` | No | `true` when the command needs a terminal. The default is `false`. | +| `timeout` | No | A Go duration greater than zero, such as `30s` or `10m`. When it is missing, the default of the registry index applies, which is `10m` unless the index sets another value. | +| `output` | No | An object with the booleans `json`, `csv` and `agentMode`. Only the modes that are `true` are allowed. A missing `output` allows no mode. | +| `permissions` | No | An object with `scopes`, `hosts`, `files` and `env`. See [Permissions](#permissions). | + +### Flag + +| Field | Required | Rules | +| --- | --- | --- | +| `name` | Yes | The same form as a command name. It cannot be a flag that the CLI owns: `tenant`, `json`, `json-compact`, `csv`, `agent-mode`, `no-input`, `no-color`, `debug` or `help`. Names are unique within the command. | +| `shorthand` | No | One ASCII letter, used as `-x`. It cannot be `h`, and it is unique within the command. | +| `type` | Yes | One of `string`, `bool`, `int`, `float` or `stringArray`. | +| `description` | Yes | At least one character. | + +The CLI does not parse the flags of a plugin. It only shows them in the help and in shell completion. Your code reads them. + +### Permissions + +| Field | Required | Rules | +| --- | --- | --- | +| `scopes` | No | A list of Management API scopes, with no repeats. Each one is lowercase letters, a colon, then lowercase letters, digits or underscores, such as `read:clients`. The proxy uses them. | +| `hosts` | No | A list of objects with `host` and `reason`. `host` is a lowercase host name with no scheme, port or path. A single `*.` at the start is allowed, and it must be followed by at least two labels, such as `*.amazonaws.com`. | +| `files` | No | A list of objects with `path`, `access` and `reason`. `access` is `read` or `write`. | +| `env` | No | A list of objects with `name` and `reason`. `name` is an uppercase environment variable name. A name that starts with `AUTH0_`, in any case, is refused. | + +Every `reason` and every `path` is free text. It must have at least one character, and it cannot hold control characters or the Unicode line and paragraph separators. The consent screen shows the reasons, so write them for the person who approves the plugin. + +Hosts and files are declared, not enforced. Scopes and environment variables are enforced: the proxy checks scopes at run time, and only the declared environment variables reach the plugin. + +### What the approval covers + +The approval of a plugin is keyed on a hash of the permissions of all commands, including their reasons, and of each command's name and `interactive` value. `auth0 plugin info` shows this hash as `PERMISSIONS HASH`. Changing any of these values asks for a new approval. Changing `description`, `flags`, `timeout`, `output` or `version` does not. + +## Platforms + +The platform names in `platforms` are these seven. The CLI picks the platform for the machine that runs it. + +| Name | Operating system | Architecture | C library | +| --- | --- | --- | --- | +| `darwin-arm64` | macOS | arm64 | Not applicable | +| `darwin-x64` | macOS | x64 | Not applicable | +| `linux-x64` | Linux | x64 | glibc | +| `linux-arm64` | Linux | arm64 | glibc | +| `linux-x64-musl` | Linux | x64 | musl | +| `linux-arm64-musl` | Linux | arm64 | musl | +| `windows-x64` | Windows | x64 | Not applicable | + +On Linux, the CLI checks which C library the system uses, and it picks the `musl` names on a musl system such as Alpine. The CLI installs `.tar.gz` and `.zip` archives. The starter projects pack Windows archives as `.zip` and the others as `.tar.gz`. + +## Paths + +The CLI keeps its settings and plugins in the directory `~/.config/auth0` on macOS and Linux. It finds the home directory from the `HOME` environment variable on every system. The paths below are relative to that directory. + +| Path | What it holds | +| --- | --- | +| `config.json` | The tenants and the default tenant. | +| `plugins/` | The plugin store. The directory is created with owner-only permissions. | +| `plugins/plugins.lock.json` | The installed plugins: the active version, the previous version, the pinned version and the hashes of the binary, the manifest and the archive. | +| `plugins/approvals.json` | The approvals of each plugin, per tenant. The file is readable and writable by the owner only. | +| `plugins/dev-links.json` | The local builds that `plugin dev link` linked. | +| `plugins/registries/auth0/` | The cache of the registry: `metadata/` with the latest `root.json`, `timestamp.json`, `snapshot.json` and `targets.json`, `roots/` with the roots after the first one (`.root.json`, from 2 up), `index.json`, `state.json` with the newest index version accepted and the time of the last refresh, and the lock file `.lock` that guards them. The first root is built into the CLI and is not stored. | +| `plugins/store////` | The files of an installed version: the executable and `plugin.json`. For example, `plugins/store/auth0/checkmate/1.0.0/`. | +| `plugins/tmp/` | The staging area for downloads and installs. | +| `plugins/hint.json` | The time of the last update hint. | +| `plugins/.lock`, `.approvals.lock`, `.dev-links.lock` | Lock files that stop two CLI processes from changing the same file at the same time. | + +On Windows, the CLI builds the same paths from the `HOME` environment variable. If `HOME` is not set, the paths are relative to the current directory. Set `HOME` before you use the plugin commands on Windows. + +## Environment variables your plugin receives + +The CLI builds the environment of a plugin from three parts. Nothing else from your environment reaches the plugin. + +**Set by the CLI for every run:** + +| Variable | Value | +| --- | --- | +| `AUTH0_PLUGIN_DOMAIN` | The address of the proxy, as `host:port`. It is not the tenant domain. | +| `AUTH0_PLUGIN_MANAGEMENT_TOKEN` | A secret for this run only. The proxy accepts it instead of a token. | +| `AUTH0_PLUGIN_CA_CERT` | The path of a PEM file with the certificate of the proxy. | +| `SSL_CERT_FILE` | The same file as `AUTH0_PLUGIN_CA_CERT`. | +| `NODE_EXTRA_CA_CERTS` | The same file as `AUTH0_PLUGIN_CA_CERT`. | +| `NO_PROXY`, `no_proxy` | The value from your environment, with the loopback addresses added, so that calls to the proxy do not go through a proxy of your own. | +| `AUTH0_PLUGIN_TENANT` | The tenant that the run is for. | +| `AUTH0_PLUGIN_JSON`, `AUTH0_PLUGIN_JSON_COMPACT`, `AUTH0_PLUGIN_CSV` | `true` or `false`, from the output flags. In agent mode, `AUTH0_PLUGIN_JSON` is `true`. | +| `AUTH0_PLUGIN_AGENT_MODE`, `AUTH0_PLUGIN_NO_INPUT`, `AUTH0_PLUGIN_NO_COLOR`, `AUTH0_PLUGIN_DEBUG` | `true` or `false`, from the global flags. | + +**Copied from your environment, when set and not empty:** + +| Variable | Note | +| --- | --- | +| `PATH`, `HOME`, `LANG`, `LC_ALL`, `LC_CTYPE`, `TERM`, `TMPDIR` | The basic settings of a process. | +| `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, `http_proxy`, `https_proxy`, `no_proxy` | Proxy settings of your network. | +| `SystemRoot`, `TEMP`, `TMP`, `USERPROFILE`, `APPDATA`, `LOCALAPPDATA`, `PATHEXT`, `ComSpec` | Windows only. | + +**Declared by the command:** every name in `permissions.env` is copied from your environment when it is set and not empty. A name that starts with `AUTH0_` is never copied, even when the manifest asks for it. + +When a name is set both by the CLI and by your environment, the CLI's value wins. + +## Environment variables the CLI reads + +| Variable | Effect | +| --- | --- | +| `AUTH0_CLI_PLUGIN_DEV_MODE` | Turns on the `auth0 plugin dev` commands and makes the CLI read the links of `plugin dev link`. Only the exact value `true` turns it on. | +| `AUTH0_CLI_PLUGINS_DISABLED` | A value of `true`, or any value that is not a boolean, turns the plugin commands off for install, update, approve and running a plugin. Search, list, info and uninstall still work. | +| `AUTH0_CLI_PLUGIN_APPROVALS` | Pins the approved permissions of plugins for CI. The value is a comma-separated list of `registry/name@sha256:<64 lowercase hex digits>`, where the hash is the permissions hash that `auth0 plugin info` shows. A pin is used in agent mode too. The CLI never writes a pin to disk. A malformed entry is an error. | +| `AUTH0_CLI_PLUGIN_UPDATE_HINT` | `false`, or any value that is not a boolean, turns off the one-line hint about a newer version. The hint is shown at most once a day, on stderr, after a plugin ran. | +| `AUTH0_AGENT_MODE` | `true` or `false`. It overrides the detection of agent mode. The `--agent-mode` flag wins over it. | +| `AUTH0_CLI_ANALYTICS` | `false` turns off usage tracking, including the events of the plugin commands. | +| `AUTH0_CLI_AUTH_MODE`, `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_API_TOKEN` | Environment login. With `AUTH0_CLI_AUTH_MODE=env`, the plugin commands use `AUTH0_DOMAIN` as the tenant when you pass no `--tenant`. | + +## Exit codes and errors + +A failed plugin command exits with 1, including a run in which the plugin itself failed. An interrupt, such as Ctrl-C, exits with 130 instead. When the plugin exits with a non-zero code, the CLI does not pass that code on. It is in the error details as `exitCode`. + +With `--json`, a failure prints one object. The example below is from the current build, which does not embed a trusted registry root yet: + +```json +{"error":{"code":"validation","reason":"plugin_registry_unavailable","message":"checkmate 0.1.0 was not run, because the registry index cannot be trusted: no trusted registry keys are embedded in this build. Reconnect and run: auth0 plugin search"}} +``` + +The `code` is a class of failure: `usage`, `validation`, `auth`, `not_found`, `plugin` or `unknown`. The `reason` says which failure it was. A plugin run that fails also carries `details` with `pluginId`, `version`, `exitCode` (left out when the plugin did not exit by itself) and `deniedCalls` (left out when the proxy denied nothing). + +## Reason codes + +| Reason | Class | Meaning | +| --- | --- | --- | +| `plugin_dev_mode_required` | validation | A `plugin dev` command ran without `AUTH0_CLI_PLUGIN_DEV_MODE=true`. | +| `plugin_create_invalid` | validation | `plugin dev create` refused the input: a missing value, a name or URL that is not valid, an ID that is not `auth0/`, a directory that is not empty, a parent directory that does not exist, or a `--from` tree that is not allowed. | +| `plugin_manifest_invalid` | validation | `plugin dev validate` found problems in the manifest, or the `--bin` file cannot run. | +| `plugin_link_invalid` | validation | `plugin dev link` refused the directory: the manifest is not valid, the executable is missing, or the ID is not `auth0/`. | +| `plugin_not_linked` | validation | `plugin dev unlink` was given a plugin that is not linked. | +| `plugin_name_reserved` | validation | The name of the plugin is the name of a core command. | +| `plugin_reference` | validation | The plugin reference is not valid, or it has a version where only a name is allowed. | +| `plugin_update_target` | validation | `plugin update` got neither a plugin name nor `--all`, or it got both. | +| `plugin_not_installed` | validation, or `not_found` | The plugin named in the command is not installed. When a command name matches a plugin of the saved registry index that is not installed, the class is `not_found`, and the details name the install command. | +| `plugin_no_tenant` | validation | The command has no tenant. Pass `--tenant`, or log in. | +| `plugin_tenant_mismatch` | validation | The session belongs to another tenant than the one the plugin is approved for. | +| `plugin_command_missing` | validation | The command is not in the manifest of the installed or linked plugin. The manifest changed since the run started. | +| `plugin_changed` | validation | The plugin changed while the session was being opened. Run it again. | +| `plugin_verification_failed` | validation | The installed files do not match what was installed. Reinstall the plugin. | +| `plugin_registry_unavailable` | validation | The registry index cannot be verified or cannot be used, so the plugin did not start. | +| `plugin_revoked` | validation | The installed version was revoked in the registry. Update the plugin. | +| `plugin_interactive` | validation | An interactive command ran with `--no-input`, in agent mode, or without a terminal. | +| `plugin_output_unsupported` | validation | The command was asked for an output mode that its manifest does not declare. | +| `plugin_not_approved` | validation | The plugin is not approved for the tenant, or its permissions changed since the last approval. | +| `plugin_approvals_damaged` | validation | The approvals file cannot be read. | +| `plugin_approval_check` | validation | The approvals could not be checked. | +| `plugin_timeout_invalid` | validation | The timeout of the command cannot be used. | +| `local_validation` | validation | A value that the CLI reads from the environment or the config cannot be used. For example, an entry of `AUTH0_CLI_PLUGIN_APPROVALS` is malformed. The message names the value. | +| `plugin_ambiguous_subcommand` | usage | The root command was called with the name of one of its subcommands among its arguments, before a bare `--`. Put the command name first, or write the value as `--flag=value`. | +| `plugin_session_unavailable` | auth | The run has no login session. Run `auth0 login`. | +| `plugin_exit` | plugin | The plugin exited with a non-zero code. The code is in `details.exitCode`. | +| `plugin_timeout` | plugin | The plugin ran past its timeout, and the CLI stopped it. | +| `plugin_interrupted` | plugin | The run was stopped by Ctrl-C, `SIGTERM` or `SIGHUP`. | +| `plugin_start_failed` | plugin | The CLI could not start the plugin process. | + +Some errors have no reason of their own. They carry the reason `unclassified` with the code `unknown`, and the message names the cause. The table for running a plugin in [Troubleshooting](troubleshooting.md#running-a-plugin) lists the refusal caused by `AUTH0_CLI_PLUGINS_DISABLED`. The other messages without a reason are listed in [Install and update](troubleshooting.md#install-and-update) and in [Other messages](troubleshooting.md#other-messages). + +## Proxy error codes + +The proxy answers a call that it does not forward with the HTTP status below. The body has `statusCode`, `error`, `message` and `errorCode`. Read `errorCode` to find the reason. + +| `errorCode` | HTTP status | When | +| --- | --- | --- | +| `invalid_proxy_token` | 401 | The per-run secret is missing or wrong. | +| `bad_request` | 400 | The request is malformed, its path is not canonical or ambiguous, or its query string has characters that are not allowed. | +| `method_not_allowed` | 405 | The HTTP method is not allowed for this path. | +| `uri_too_long` | 414 | The request target is too long. | +| `forbidden_path` | 403 | The path is not allowed through the proxy. | +| `no_scope_rule` | 403 | The endpoint is not in the scope table of the registry index. | +| `insufficient_scope` | 403 | The command did not declare a scope that allows the call. The message names the scope to add. | +| `payload_too_large` | 413 | The request body is too large. | +| `session_expired` | 401 | The session expired or was rejected, and the proxy cannot renew it. For a user login, run `auth0 login`. | +| `unsupported_response` | 502 | The response from Auth0 is not JSON, or it is too large for the proxy to check. | +| `upstream_redirect` | 502 | Auth0 answered with a redirect, which the proxy does not follow. | +| `upstream_failed` | 502 | The proxy could not reach Auth0, or could not read its response. | + +The proxy renews a token of a client-credentials session when Auth0 answers 401. It does this up to three times in one run. + +## Next + +- [Building a plugin](building.md) walks through a project from the first command to the approval. +- [Releasing a plugin](releasing.md) explains how a version becomes a release. +- [Security and trust model](security.md) explains how the CLI checks what it installs and runs. diff --git a/docs/plugins/releasing.md b/docs/plugins/releasing.md new file mode 100644 index 000000000..c949098ed --- /dev/null +++ b/docs/plugins/releasing.md @@ -0,0 +1,221 @@ +--- +layout: default +title: Releasing a plugin +parent: Plugins +nav_order: 4 +--- + +In this page you publish a new version of a plugin as a GitHub release, with one archive per platform and a `checksums.txt` file. The release command and the release workflow of the starter project do the steps for you, so you never edit a version or a tag by hand. + +## What the starter project gives you + +Both starter projects contain a release command and two GitHub Actions workflows: + +| Part | Go project | TypeScript project | +| --- | --- | --- | +| Release command | `make release` | `bun run release` | +| Program behind it | `scripts/release/` | `scripts/release.ts` and `scripts/version.ts` | +| Tests on every pull request | `.github/workflows/ci.yml` | `.github/workflows/ci.yml` | +| Release on merge | `.github/workflows/release.yml` | `.github/workflows/release.yml` | +| Builds the archives | GoReleaser, a tool that builds and packs Go release archives, configured in `.goreleaser.yaml` | `bun build --compile` in `.github/actions/build-archive/` | +| Reads the version and checks what exists | `.github/actions/read-version/` | `.github/actions/read-version/` | + +The flow has two halves. A command on your machine opens a pull request that raises the version. When you merge that pull request, the workflow tests the commit, builds the archives and publishes the release. + +## Create the first release + +`plugin dev create` ends with this line: + +```text + ▸ To publish it, push the project to a new GitHub repository: the first push to main releases version 0.1.0. Release again with: make release BUMP=minor +``` + +The release workflow runs when a change to `plugin.json` reaches `main`, and a first push contains `plugin.json`. So the first push releases the version that the project was created with, once the tests pass. + +A GitHub release does not list the plugin in the registry. Other people can install it with `auth0 plugin install` once it is listed in the registry catalog, which is a separate step. See [Status](index.md#status). + +The workflows listen to `main`. If your default branch has another name, change the `branches` line in both `release.yml` and `ci.yml`. + +## The version + +The version lives in the `version` field of `plugin.json`. The tag is `v` followed by the version, and the archives carry the version in their names. + +- Use `MAJOR.MINOR.PATCH`, without a leading `v`. This is the only form the CLI installs. If a registry lists a version such as `1.0.0-rc.1`, `auth0 plugin install` refuses it, because the version is not in the form `MAJOR.MINOR.PATCH`. +- The release command and the workflow accept a prerelease such as `1.0.0-rc.1` when you give the exact version, so you can publish one to GitHub for your own testing. They refuse build metadata such as `1.0.0+build.5`, because the tag and the archive names carry the version. The manifest schema accepts both forms. +- In the TypeScript project, `package.json` may have a `version` too. If it does, it must match `plugin.json`, and the release command keeps the two in step. + +## Start a release + +Run the command on a clean checkout of `main` that matches `origin`. It needs `git` and `gh`, and `gh` must be logged in (`gh auth login`). + +For Go: + +```sh +make release BUMP=patch # or minor, or major +make release VERSION=1.2.3 # an exact version +make release BUMP=patch DRY_RUN=1 # show the steps, change nothing +``` + +For TypeScript: + +```sh +bun run release patch # or minor, or major +bun run release 1.2.3 # an exact version +bun run release patch --dry-run # show the steps, change nothing +``` + +A dry run prints the plan and changes nothing. This is the output of the Go project, and the TypeScript project prints the same three lines: + +```text +Releasing 0.1.1 (now 0.1.0) from main: + create branch release/v0.1.1, set the version in plugin.json, + commit "chore: release v0.1.1", push and open the pull request "Release v0.1.1" +Dry run: nothing was changed. +``` + +Without the dry run flag, the command does what the plan says. It creates the branch `release/v`, raises the version in `plugin.json`, commits `chore: release v` with your own git settings, pushes the branch, and opens a pull request titled `Release v`. It then switches back to the default branch and prints the address of the pull request. The pull request is opened with your own login, so the repository does not need to allow Actions to create pull requests. + +If a step fails before the push, the command restores the repository. It switches back to the default branch and deletes the release branch. If the branch is pushed but the pull request cannot be opened, the message tells you the command to open it yourself. + +The command refuses to start in these cases. Both starter projects print the same messages: + +```text +release: there are uncommitted changes. Commit or stash them first: +M README.md +``` + +```text +release: "bogus" is not patch, minor, major or a semantic version such as 1.2.3 (no build metadata). +``` + +```text +release: "v1.0.0" is not patch, minor, major or a semantic version such as 1.2.3 (no build metadata). Write 1.0.0 without the v. +``` + +It also refuses these cases: + +- You are not on the default branch, or your checkout is not level with `origin`. +- A branch `release/v` or a tag `v` already exists, here or on `origin`. +- An exact version that is already the version in `plugin.json`. +- `patch`, `minor` or `major` while the current version is a prerelease. Then give the exact version. +- `gh` is not logged in. The message is `release: gh is not logged in. Run: gh auth login`. + +## What the workflow does + +Merging the pull request changes `plugin.json` on `main`, which starts the Release workflow. You can also start it by hand from the Actions tab, on `main`, to finish a release that stopped half way. + +1. **Read the version.** The workflow reads `version` from `plugin.json`, derives the tag, and checks whether the tag and the release already exist. A release counts as complete when it holds `checksums.txt` and an archive for each platform in `plugin.json`. A manual run on a branch other than the default branch is skipped. +2. **Run the tests.** The workflow calls `ci.yml` on the merged commit. It skips this step when the release is already complete. +3. **Build the archives** for the seven platforms (see below). +4. **Write `checksums.txt`** with the SHA-256 hash of every archive. +5. **Create the release** `v` at the merged commit, with the archives, `checksums.txt` and notes that GitHub generates. GitHub creates the tag at that commit. + +The Go project does steps 3 to 5 with GoReleaser in one job. The TypeScript project builds each platform in its own job, with `bun build --compile --target=bun-`. Each job uploads its archive as a workflow artifact. A last job collects them, writes `checksums.txt` and publishes the release. In the TypeScript project, `checksums.txt` goes up last, because its presence is what marks a release as complete. + +The workflow is safe to run again. If a run stops half way, run it again. It completes whatever is missing, and it does nothing once the release is complete. Two things protect you from publishing the wrong code: + +- If the tag already exists at another commit and the release is not complete, the run stops with this message: `The tag v is at , but this run is for . Raise the version in plugin.json, or re-run the workflow of the commit the tag points to.` +- Only one run per tag can publish at a time. + +The workflow token has read access to the repository. Only the job that publishes also gets write access to the contents, so it can create the tag, the release and the assets. The actions in the workflows are pinned to full commit hashes. + +## What an archive must contain + +For each platform in `plugin.json`, the release has one archive named `__.tar.gz`, or `.zip` for Windows. Each archive holds exactly two files at its root: + +- the executable, named ``, or `.exe` on Windows, +- `plugin.json`. + +Nothing else is allowed. When someone installs the plugin, the CLI refuses an archive that has directories, links, other file names, duplicate entries, or an executable whose name is not the one the registry lists. That is why the starter projects leave out the README and the license. + +This is what the Go project produced with `make release-snapshot`, which builds every archive into `dist/` without publishing anything. The middle part of each name is the snapshot version, which depends on the git state of the project: + +```text +dist/checkmate_0.0.0-SNAPSHOT-none_darwin-arm64.tar.gz +dist/checkmate_0.0.0-SNAPSHOT-none_darwin-x64.tar.gz +dist/checkmate_0.0.0-SNAPSHOT-none_linux-arm64-musl.tar.gz +dist/checkmate_0.0.0-SNAPSHOT-none_linux-arm64.tar.gz +dist/checkmate_0.0.0-SNAPSHOT-none_linux-x64-musl.tar.gz +dist/checkmate_0.0.0-SNAPSHOT-none_linux-x64.tar.gz +dist/checkmate_0.0.0-SNAPSHOT-none_windows-x64.zip +dist/checksums.txt +``` + +The contents of two of them: + +```text +$ tar -tzf checkmate_0.0.0-SNAPSHOT-none_linux-x64.tar.gz +plugin.json +checkmate + +$ unzip -l checkmate_0.0.0-SNAPSHOT-none_windows-x64.zip + Length Date Time Name +--------- ---------- ----- ---- + 894 10-30-2026 08:49 plugin.json + 8444416 10-30-2026 08:51 checkmate.exe +--------- ------- + 8445310 2 files +``` + +For the TypeScript project, the script that the workflow runs for one platform also works on your machine. It needs `bash`, `jq`, Bun, and `zip` for Windows archives: + +```sh +bash .github/scripts/build-archive.sh linux-x64-musl +``` + +```text +out/checkmate_0.1.0_linux-x64-musl.tar.gz +``` + +That archive holds `checkmate` and `plugin.json`. + +### Platforms + +`plugin.json` lists the platforms you ship. The starter projects ship all seven. A release must have an archive for every platform in the manifest. + +| Platform | Go build | Bun target | +| --- | --- | --- | +| `darwin-arm64` | `darwin/arm64` | `bun-darwin-arm64` | +| `darwin-x64` | `darwin/amd64` | `bun-darwin-x64` | +| `linux-x64` | `linux/amd64` | `bun-linux-x64` | +| `linux-arm64` | `linux/arm64` | `bun-linux-arm64` | +| `linux-x64-musl` | `linux/amd64`, built again as its own archive | `bun-linux-x64-musl` | +| `linux-arm64-musl` | `linux/arm64`, built again as its own archive | `bun-linux-arm64-musl` | +| `windows-x64` | `windows/amd64` | `bun-windows-x64` | + +The Go project builds a static binary with `CGO_ENABLED=0`, so the musl archives contain the same program as the glibc ones. In the snapshot, the `linux-x64` and `linux-x64-musl` archives have the same hash for that reason. On Linux, the CLI checks which C library the machine uses and picks the matching platform. + +### `checksums.txt` + +`checksums.txt` has one line per archive, in the format of `sha256sum`: the hash, two spaces and the file name. Two lines from the same snapshot: + +```text +36e4c1e39e3285938cdd7aaeceefb5b5ef97bbcc52b0893ed38b55fda182a10b checkmate_0.0.0-SNAPSHOT-none_darwin-arm64.tar.gz +cd7564598778ea839e5a32bca14912bc046471c9e7655997e14fc82c4b1be931 checkmate_0.0.0-SNAPSHOT-none_darwin-x64.tar.gz +``` + +The CLI does not read this file. The hash that the CLI checks when a person installs a plugin comes from the signed registry index. Use `checksums.txt` to compare an archive that you downloaded with the one that the workflow built. + +## What is automated, and what is not + +The starter project automates these steps: + +- Opening the release pull request from your machine, with the version raised and a standard commit message. +- Running the tests on the merged commit. +- Creating the tag `v` at that commit. +- Building the archives for seven platforms, with the files that the CLI accepts. +- Writing `checksums.txt`. +- Creating the GitHub release with generated notes. +- Finishing a release that stopped half way, and doing nothing once it is complete. + +## What the starter does not do + +- Sign the archives or record a build attestation. The workflows contain no signing step and do not ask for an attestation. +- Turn on immutable releases. This is a repository setting on GitHub. The starter project neither turns it on nor checks it. +- List the plugin in the registry catalog. No command or workflow in the starter project does this. + +## Next + +- [Reference](reference.md) lists the fields of `plugin.json`, the platform names, the paths and the reason codes. +- [Security and trust model](security.md) explains how the CLI checks what it installs. +- [Building a plugin](building.md) covers the work before a release. diff --git a/docs/plugins/security.md b/docs/plugins/security.md new file mode 100644 index 000000000..bbe653afa --- /dev/null +++ b/docs/plugins/security.md @@ -0,0 +1,181 @@ +--- +title: Security and trust model +parent: Plugins +nav_order: 6 +layout: default +--- + +## What this page covers + +This page explains what a plugin can reach, what the CLI checks before every run, and what it does not protect against. Read it before you install a plugin on a machine that holds production credentials, and before you publish one. + +The Auth0 CLI is the only supported way to start a plugin. It checks your approval and gives the plugin a proxy session. Running the plugin binary directly skips those checks and gives it no Auth0 access. + +## The short version + +- A plugin is a native program that runs as your user on your machine. The CLI controls which Auth0 calls it can make, but it does not run the plugin in a sandbox. +- The CLI never gives a plugin your access token or your client secret. The plugin calls a local proxy, and the proxy adds the token. +- Each command gets only the scopes that the command declares, and the proxy refuses every other call. +- A plugin runs only on a tenant that a person approved, with the permissions that person saw. +- Plugins can still read the files you can read and send data to any host. The CLI does not stop that, so install only plugins you trust. + +## What the CLI checks before a run + +Every run of `auth0 ` goes through the same checks, in this order. A check that fails stops the run before the next one starts, and nothing from the plugin runs. + +1. Plugins are not turned off on this machine (`AUTH0_CLI_PLUGINS_DISABLED`). +2. When the environment fixes the tenant (`AUTH0_CLI_AUTH_MODE=env`), a `--tenant` that you pass matches `AUTH0_DOMAIN`. +3. The installed executable and `plugin.json` still match the hashes in the lockfile. +4. The registry index is current enough to run from, and it does not revoke this version. +5. The command can run the way it was called. An interactive command needs a terminal, and the output you asked for must be declared. +6. The tenant approved the permissions of this plugin. +7. Only now does the CLI open a session. The session must be for the approved tenant. + +The CLI checks the executable and manifest a second time after the session is open, because a login can take a while. If either changed in between, the run stops. + +## The registry, signatures and keys + +The registry is a signed index that lists each plugin, its versions, the platforms it builds for and the permissions each version asks for. The registry is a TUF repository (The Update Framework), and the index is its one signed file. + +- The registry uses four roles: root, targets, snapshot and timestamp. There are no delegations, and the repository uses consistent snapshots, which means every signed file except `timestamp.json` has a version number or a hash in its name. +- The production root has 3 root keys, and 2 of them must sign a new root. The targets, snapshot and timestamp roles each have 1 key, and 1 signature is enough. +- The signed files expire. The root is valid for 365 days, the targets and snapshot files for 90 days, and the timestamp for 7 days. +- One `root.json` is built into the binary. It is the starting point of trust, and it must be signed by its own root keys. No environment variable or config file can add or replace it. A registry mirror serves files, but the CLI still checks them against this root. +- The targets file lists `index.json` with its length and SHA-256 hash. The CLI downloads the index and refuses it unless it has that length and hash. +- Metadata or an index that fails a signature, threshold, hash or length check is refused, and the CLI never falls back to an unverified copy. +- The index has its own expiry date and version. An expired index is refused. +- The CLI remembers the newest index version it accepted, and it refuses an older one. It also refuses metadata that is older than what it has already seen, so a copy of an old index cannot roll your machine back to a version that was since revoked. +- Revoked versions are listed in the index. A revoked version does not run, and the CLI stops it at the next index refresh. + +A build of the CLI that has no root built in cannot verify any index. It stops with the message `no trusted registry keys are embedded in this build`. + +### Root rotation + +The registry can replace the keys of any role without a new CLI release. To do that, it publishes a new version of the root. The new root must be signed by enough keys of the previous root and by enough keys of its own, so that nobody can install keys without the old keys agreeing. The registry keeps every version of the root. On its next refresh, the CLI walks from the root it has to the newest one, checks each step, and stores the roots it accepted. This works for the targets, snapshot and timestamp keys and for the root keys. + +### Offline use and the saved copy + +The CLI keeps the last verified index on disk, and installed plugins can keep running from it when the registry cannot be refreshed. + +- The CLI does not trust the saved copy just because it is there. Each time it reads the saved copy, and without the network, it checks the chain of roots from the root built into the CLI, the signatures of the targets file, and the length and hash of the index. It then checks the schema, the expiry of the root, the targets file and the index, and that the index is not older than the newest one it accepted. Only the expiry of the timestamp and of the snapshot file is not checked at that point, because those files are short lived and there is no network to renew them. Signatures are always checked. +- When a refresh fails because the registry cannot be reached, because it serves older metadata or an older index, because another auth0 process holds the lock of the cache, or because the timestamp it serves has expired, the CLI keeps using the last verified index. It shows a warning with the age of the index. +- The time of the last refresh does not move in those cases. After 30 days without a good refresh, installed plugins stop until the index is refreshed. +- A signature, threshold, hash or length failure never falls back to the saved copy. An expired root, snapshot or targets file does not either, and neither does a cancelled command. + +## Hashes and the lockfile + +Nothing is unpacked or made executable until its SHA-256 hash matches the signed index. + +- Downloads come only from `github.com`, `objects.githubusercontent.com` and `release-assets.githubusercontent.com`, over HTTPS on port 443. The CLI checks the host again on every redirect. +- The download size is capped. The default cap is 256 MiB, and the index can set a different one. +- An archive must hold exactly two files: the executable, named after the plugin (`checkmate`, or `checkmate.exe` on Windows), and `plugin.json`. Directories, links, duplicate names and names with path separators or `..` are refused. +- `plugins.lock.json` records the hash of the executable, the hash of `plugin.json`, the hash of the archive, the permissions hash, the active version and the pin. It is written with mode `0600`. +- Before every run the CLI hashes the executable and `plugin.json`. It also refuses a file that is a symlink, a file with more than one hard link, or a directory on the path that anyone else can write to. On Windows the CLI skips the mode and link checks, because Windows uses access control lists instead. + +If you see a message that the installed copy "no longer matches what was verified", the file changed after the install. Reinstall the plugin with `auth0 plugin install `. + +## Consent for each tenant + +Before a plugin first runs on a tenant, a person has to approve its permissions for that tenant. The approval is recorded in `approvals.json`, with mode `0600`. + +- An approval is keyed to the tenant, the plugin and a hash of the permissions: the scopes, hosts, files, environment variables, and each command's name and whether it is interactive. The version is not part of the key. A new release with the same permissions does not ask again. +- A release with different permissions asks again. The update screen shows only what changed, and the old version stays active until the person approves. +- Only a person at a terminal can approve. Agent mode, `--no-input` and a session without a terminal all refuse to approve. `auth0 plugin approve` refuses in agent mode too. +- CI can pin the permissions hash with `AUTH0_CLI_PLUGIN_APPROVALS`, for example `auth0/checkmate@sha256:`. A pin is read strictly and held in memory for the run. The CLI never writes a pin to disk. +- Logging out of a tenant deletes that tenant's approvals before it removes the tenant. Uninstalling a plugin deletes its approvals on every tenant. + +## Scope enforcement through the loopback proxy + +Loopback means this machine only. For each run the CLI starts a proxy on `127.0.0.1`, an address that only programs on your machine can reach, at a random port. The proxy has a new self-signed certificate, valid for `127.0.0.1` only, and a new random secret. The plugin gets the proxy address as `AUTH0_PLUGIN_DOMAIN` and the secret as `AUTH0_PLUGIN_MANAGEMENT_TOKEN`. The plugin's SDK sends the secret as its bearer token, and the proxy swaps in the real token before it forwards the call. + +The proxy denies every call unless all of these hold: + +- The path is already in canonical form, which means one plain spelling of the path, and it starts with `/api/v2`. The proxy never repairs a path. A path with dot segments (`.` or `..` between slashes), encoded slashes (`%2F`), encoded dots (`%2E`) or other escapes is refused with `bad_request`. A canonical path outside `/api/v2` returns `forbidden_path`. +- The method is `GET`, `POST`, `PUT`, `PATCH` or `DELETE`. Any other method returns `method_not_allowed`. +- The endpoint is in the scope table in the signed index. An endpoint that is not in the table returns `no_scope_rule`. +- One of the scopes that allow the endpoint is among the scopes of the command you ran. If none is, the proxy returns a 403 with `insufficient_scope` and names the scope to add. + +The scopes come from the command that runs, not from the whole plugin. The consent screen shows the union of all commands, but a call to one command cannot use the scopes of another. + +Other protections on the proxy: + +- Only `Accept`, `Content-Type`, `User-Agent` and `Auth0-Client` are forwarded. The plugin's `Authorization` header and cookies are dropped. +- A response must be JSON for the proxy to inspect it. Other responses return `unsupported_response`. A response larger than 32 MiB is refused. +- The proxy does not follow redirects. A redirect from the tenant returns `upstream_redirect`. +- The proxy refreshes an expired token at most three times in one run. +- A user login cannot be renewed silently. The proxy returns `session_expired` and tells the plugin, and you, to run `auth0 login`. + +## Secret fields are removed + +Before a response reaches the plugin, the proxy removes every field whose name is in the secret list from the signed index. The match ignores case, and it works at any depth in the JSON. A client secret, for example, is removed even when the plugin asks for it by name. The list lives in the index, so it can grow without a CLI release. + +## What the plugin environment contains + +A plugin does not inherit your environment. It gets a short list of variables, and nothing else. + +- The base list: `PATH`, `HOME`, `LANG`, `LC_ALL`, `LC_CTYPE`, `TERM`, `TMPDIR`, `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, and the lowercase forms of the proxy variables. On Windows the list also has `SystemRoot`, `TEMP`, `TMP`, `USERPROFILE`, `APPDATA`, `LOCALAPPDATA`, `PATHEXT` and `ComSpec`. +- The proxy and certificate variables: `AUTH0_PLUGIN_DOMAIN`, `AUTH0_PLUGIN_MANAGEMENT_TOKEN`, `AUTH0_PLUGIN_CA_CERT`, `SSL_CERT_FILE` and `NODE_EXTRA_CA_CERTS`. The last two point to the run's CA file, so the usual SDKs trust the proxy. +- The CLI flags, as `AUTH0_PLUGIN_TENANT` and one `AUTH0_PLUGIN_` for each CLI flag, such as `AUTH0_PLUGIN_JSON` or `AUTH0_PLUGIN_AGENT_MODE`. +- The variables that the command declares in `permissions.env`. The manifest rejects any name that starts with `AUTH0_`, and the runner drops those names too. + +The plugin never receives `AUTH0_CLIENT_SECRET`, `AUTH0_API_TOKEN`, the real access token, or any other `AUTH0_` variable from your shell. The loopback addresses are added to `NO_PROXY`, so the plugin reaches the proxy directly, even when you have a corporate proxy set. + +## Timeouts and the process tree + +Each command has a timeout. The default is 10 minutes, and the manifest can set its own. When the timeout passes, or when you press Ctrl-C or the CLI gets SIGTERM: + +- On macOS and Linux, a plugin that is not interactive runs in its own process group. The CLI sends SIGTERM to the whole group, waits 5 seconds, and then sends SIGKILL. Helper processes that the plugin started go down with it. +- On Windows, the plugin and everything it starts belong to a job object. The CLI ends the job object. Stopping on Windows is a best effort attempt. +- An interactive command stays in the CLI's process group, so the terminal keeps working. On macOS and Linux a timeout then stops only the plugin process itself. +- The proxy closes after every run, including a failed one, and its secret and certificate stop working at that point. + +A plugin that times out or is interrupted ends with the reason `plugin_timeout` or `plugin_interrupted`, and the CLI exits 1, or 130 when you pressed Ctrl-C. + +## What a plugin cannot do + +- Read your access token or client secret from the CLI, from its environment, from its arguments or from a file the CLI writes. +- Call an endpoint that is not in the scope table, or that the scopes of the running command do not allow. +- Call a path outside `/api/v2`, such as the token endpoint. +- Read a secret field from a response, such as a client secret. +- Run on a tenant that nobody approved, or run on a session for a different tenant than the one approved. +- Run with a binary or manifest that changed after the install, or run a version that the index revoked. +- Keep calling the tenant after the run ends, because the proxy and its secret are gone. + +## What a plugin can still do + +A plugin runs as you, with your permissions. These limits are real, and the CLI does not stop them today: + +- It can read and write any file that you can read and write. The `files` entries in the manifest are declarations that the consent screen shows, and the CLI does not enforce them. +- It can send any data it can read to any host. The `hosts` entries are declarations too, and the CLI does not enforce them. +- Because it runs as you, it could read the credentials that the CLI stores in the operating system keyring, or run `auth0` commands with your saved login. A plugin that does this would bypass the proxy. Review of the plugin before it is published is the main control against this. +- Anything it prints goes straight to your terminal. The CLI does not filter the output of a plugin. + +Tokens scoped to a single run, with only the scopes that run needs, are planned. With them, a plugin would no longer use the same kind of session as the CLI. Until then, the proxy and the declared scopes are the limit. + +## Telemetry + +The CLI sends a usage event for plugin activity, unless you turn analytics off with `AUTH0_CLI_ANALYTICS=false`. The events are `CLI - Plugin - Run`, `Install`, `Update` and `Uninstall`. + +The events carry: + +- The plugin name, without the registry, and the plugin version. +- The CLI version, the operating system and the CPU architecture. +- For a run, whether it succeeded. For a failed run, the reason code of the CLI, or `other` when the code is not one of the known plugin codes. +- The install ID that the CLI sends with its other events. + +The events never carry the arguments, the flags, the tenant name, the output of the plugin, file paths, or any token or secret. + +The CLI sends nothing for a plugin that comes from a registry other than the built-in one, for a linked build from `plugin dev link`, or for a run that stops before the installed files are verified. A debug build with no version does not send events either. + +## Reporting a problem + +- For a security problem in the Auth0 CLI, do not open a public GitHub issue. Report it through the [Auth0 Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy), as the [README](https://github.com/auth0/auth0-cli#vulnerability-reporting) describes. +- For a problem with a plugin, use the bug link in that plugin's `owner.bugs` field. The author of the plugin owns the fix. +- For a bug in the CLI that is not a security problem, open an issue on the [auth0-cli repository](https://github.com/auth0/auth0-cli/issues). + +## Next + +- [Status](index.md#status): which parts of the registry and release path are in place. +- [Architecture](architecture.md) shows how the parts fit together and which files the CLI writes. +- [Reference](reference.md) lists the environment variables, reason codes and paths. +- [Agent mode and automation](automation.md) covers approval pins and non-interactive runs. diff --git a/docs/plugins/troubleshooting.md b/docs/plugins/troubleshooting.md new file mode 100644 index 000000000..143c5816c --- /dev/null +++ b/docs/plugins/troubleshooting.md @@ -0,0 +1,200 @@ +--- +layout: default +title: Troubleshooting +parent: Plugins +nav_order: 8 +--- + +This page lists the errors you can get from the plugin commands, what each one means, and what to do about it. Find the message you see, or the `reason` from the JSON error, and follow the fix. + +## Read an error + +In a terminal, the CLI prints the message under a heading. With `--json`, `--json-compact` or in agent mode, the error is one JSON object on stderr. The `reason` is the stable part, and the message can change between releases: + +```json +{"error":{"code":"validation","reason":"plugin_registry_unavailable","message":"..."}} +``` + +The exit code is `1` for every failure, except an interrupt, which exits with `130`. See [Agent mode and automation](automation.md) for the error object and its `details`. + +Some errors have no reason. They show `"code":"unknown"` and `"reason":"unclassified"`, and the message names the cause. Those errors are listed under [Install and update](#install-and-update) and [Other messages](#other-messages). + +Each one names the file, plugin, version or tenant involved, and the next command to run. + +## Running a plugin + +When you run `auth0 `, the CLI checks things in a fixed order and stops at the first failure. The order is described in [Getting started](getting-started.md). This table follows it. + +| Reason | What it means | What to do | +| --- | --- | --- | +| No reason. The message names `AUTH0_CLI_PLUGINS_DISABLED`. | Plugins are turned off on this machine. | Unset the variable, or set it to `false`. Any value other than `true` or `false` also turns plugins off. | +| `plugin_verification_failed` | A file of the installed plugin no longer matches what was checked at install time, so the plugin does not run. | Reinstall it with `auth0 plugin install `. | +| `plugin_registry_unavailable` | The registry index is missing, expired, or older than the 30 days a plugin may run on a saved copy. | Reconnect, then run `auth0 plugin search` to refresh the index. | +| `plugin_revoked` | The registry revoked this version. | Run `auth0 plugin update `. | +| `plugin_interactive` | The command needs a terminal, and you used agent mode, `--no-input`, or no terminal. | Run it in a terminal. | +| `plugin_output_unsupported` | You asked for `--json`, `--json-compact`, `--csv` or agent mode, and the command does not declare it. | Remove the flag. For agent mode, use `--agent-mode=false`. | +| `plugin_not_approved` | You have not approved the plugin for this tenant, or it asks for something different from what you approved. | Run the `auth0 plugin approve` command in the message. | +| `plugin_approvals_damaged` | `approvals.json` cannot be read. | Remove the file named in the message, then approve again. The CLI does not delete it on its own. | +| `plugin_approval_check` | The approval check failed for another reason. The message says why. | Read the message and fix the value it names. | +| `plugin_no_tenant` | The CLI does not know which tenant to use. | Pass `--tenant `, or log in with `auth0 login`. | +| `plugin_session_unavailable` | There is no usable session for the tenant. | Run `auth0 login`. | +| `plugin_tenant_mismatch` | The session is for a different tenant than the one the plugin is approved for. | Pass `--tenant` for the approved tenant, or approve the plugin for the tenant you are logged in to. | +| `plugin_timeout_invalid` | The timeout in the plugin's manifest is not a usable duration. | Tell the plugin's owner. `auth0 plugin info` shows who that is. | +| `plugin_command_missing` | The plugin has no command with that name any more. | Run it again. If it persists, reinstall the plugin. | +| `plugin_changed` | The plugin changed on disk while the CLI was opening the session. | Run the command again. | +| `plugin_ambiguous_subcommand` | A call of the root command has a subcommand name among its arguments, before a bare `--`, so the CLI cannot tell which command you meant. | Type the subcommand first, write the flag as `--flag=value`, or put the argument after `--`. | + +### Not approved + +```text +=== other.us.auth0.com error + + ▸ checkmate is not approved for other.us.auth0.com. Run: auth0 plugin approve auth0/checkmate --tenant other.us.auth0.com +``` + +Approval belongs to a tenant and to a set of permissions. A new version that asks for something different gives the same error with different text: + +```text + ▸ checkmate changed what it asks for since you approved it for acme.us.auth0.com. Run: auth0 plugin approve auth0/checkmate --tenant acme.us.auth0.com +``` + +### A plugin that does not verify + +If the executable was changed after the install, the run stops: + +```text + ▸ checkmate no longer matches what was verified when it was installed (the hash of checkmate changed), so it will not run. Reinstall it with: auth0 plugin install checkmate +``` + +If `plugin.json` was changed, the plugin is not offered as a command, and running `auth0 checkmate` does not run it. When the saved registry index lists the plugin, the CLI says that it is available to install: + +```text + ▸ Plugin "checkmate" is available. Install it with: auth0 plugin install checkmate +``` + +If the saved index does not list it, the CLI reports the command as unknown. In both cases, `auth0 plugin info checkmate` shows `INSTALLED 1.0.0 (failed verification)` and says what changed. The fix is the install command from the message. + +### The registry index is old or cannot be reached + +Searching and running a plugin use the saved copy of the registry index when the registry cannot be reached. The CLI tells you how old the copy is: + +```text + ▸ The registry could not be reached at : (using the cached index from 4 days ago) +``` + +The CLI uses the saved copy in the same way when the registry serves older metadata or an older index than the CLI has already accepted, and when another auth0 process is refreshing the registry. When the registry has stopped re-signing its metadata, so that the timestamp it serves has expired, the warning reads: + +```text + ▸ The registry has not been re-signed since 2026-10-01T00:00:00Z (using the cached index from 9 days ago) +``` + +In all of these cases the time of the last refresh does not change, so the saved copy keeps its age. A plugin keeps running on the saved copy for up to 30 days after the last good refresh. After that, it stops with `plugin_registry_unavailable`: + +```text + ▸ checkmate 1.0.0 was not run, because the registry index cannot be trusted: The registry index was last refreshed 44 days ago, so installed plugins cannot run until it is refreshed. Reconnect and run: auth0 plugin search. +``` + +If you are behind a proxy, set `HTTPS_PROXY`. If the registry address is blocked, point the `auth0` registry at a mirror, as described in [Managing plugins](managing.md). + +## Install and update + +Installing and updating need an index that was refreshed within the last day. When the refresh fails, the message looks like this, with the command you ran in place of the placeholder: + +```text + ▸ installing a plugin needs a current registry index, but the registry at could not be refreshed: . Reconnect and run: auth0 plugin install +``` + +These errors have a reason: + +| Reason | What it means | What to do | +| --- | --- | --- | +| `plugin_reference` | The plugin name or version is not valid. | Names use lowercase letters, digits and hyphens, and start with a letter or digit. A version has the form `MAJOR.MINOR.PATCH`. To install an exact version, use `auth0 plugin install name@version`. | +| `plugin_name_reserved` | The plugin has the name of a core command. | The plugin cannot be installed under that name. The message says `auth0 already has a command named "". Plugin names cannot shadow core commands`. | +| `plugin_not_installed` | `update`, `uninstall` or `approve` named a plugin that is not installed. | Install it first, or check the spelling. The message lists the installed plugins, or says that none are installed. | +| `plugin_update_target` | `plugin update` was run with no name and without `--all`. | Give one name, or use `--all`. | +| `plugin_no_tenant` | `approve` has no tenant to approve the plugin for. | Pass `--tenant `, or log in with `auth0 login`. | + +Install works without a tenant. It installs the plugin and tells you to approve it later. An update that asks for new permissions and has no tenant to ask about keeps the installed version, and it says so. + +Other errors from these commands have no reason, so the JSON shows `unclassified`. The message names the cause. The table below lists the messages you are most likely to see. The plugin name is the short name, such as `checkmate`, or the full ID, such as `auth0/checkmate`, depending on the message. + +| Message starts with or contains | What it means | What to do | +| --- | --- | --- | +| ` is not listed in the auth0 registry.` | There is no such plugin in the registry. | Check the spelling with `auth0 plugin search`. | +| ` is not available in the auth0 registry.` | The version does not exist. The message lists the versions that do. | Pick one of them. | +| ` has no build for .` | The version has no build for your operating system and architecture. The message lists the platforms it supports. | Install one of those versions, or use another machine. | +| ` requires auth0-cli . You have .` | The plugin needs another version of the CLI. | Update the CLI. | +| ` targets plugin API version , which needs a newer auth0 CLI.` | The plugin was built for a newer plugin API. | Update the CLI. | +| ` was revoked: .` | The registry revoked this version. | Install a newer one with `auth0 plugin update `. | +| `Downloaded file does not match the registry hash. The file was discarded.` | The download is not the file the signed registry lists. Nothing was installed. | Try again. If it repeats, do not install the plugin, and report it as described in [Security and trust model](security.md). | +| `Downloaded file does not have the size the registry lists.` or `The download is larger than allowed.` | The file is not the one the registry lists, for the same reason. | Try again. If it repeats, report it. | +| `The plugin could not be downloaded. The registry lists it, but the file is unavailable.` | The release file is missing, or the server refused it. | Try again later. If it persists, tell the plugin's owner. | +| `Plugins can only be downloaded over https from GitHub release hosts.` | The registry points at a host that plugins cannot come from. | Report it. This is a registry problem. | +| `The plugin archive is not a valid plugin archive.` or `The artifact does not match the registry record.` | The file is not what the registry describes. Nothing was installed. | Report it to the plugin's owner. | +| `The auth0 registry index failed signature verification.` | The signed registry metadata does not have enough valid signatures from the keys the CLI trusts, or the index does not have the length and hash that the signed metadata gives. The same message appears when a saved root does not verify. The CLI never falls back to an unverified index, and it does not use the saved copy after this error. | Check for a proxy or mirror that changes the files. If there is none, report it. If the message names a saved file and a folder to remove, remove that folder to start over. | +| `The registry index expired` | The root, the snapshot or targets file, or the index itself is past its expiry date. The message names the file. An expired timestamp alone does not cause this error when a saved copy is usable, because the CLI shows the "has not been re-signed" warning instead. | Reconnect and refresh with `auth0 plugin search`. If it persists, the registry has not been re-signed, so report it. | +| `The registry index is older than one that was already accepted.` | The registry served an older index, or older metadata, than one the CLI already saw, or its files are out of step with each other. When a saved copy is usable, the CLI uses it and shows this as a warning. | Try again later. If it persists, report it. | +| `The registry index uses a newer format. Update the auth0 CLI to use it.` | The CLI is too old for the registry. | Update the CLI. | +| `Another auth0 process is changing the installed plugins. Try again in a moment.` | Two CLI processes tried to change the plugins at once. | Run the command again. | +| `Another auth0 process is refreshing the registry. Try again in a moment.` | Two CLI processes tried to refresh the registry index at once. | Run the command again. | +| `Another auth0 process changed this plugin while the change was being approved. Run the command again.` | The plugin changed after you answered the consent screen. Nothing was changed. | Run the command again. | + +### Other messages + +Some messages come from the approvals and the lockfile. They have no reason, so the JSON shows `unclassified`: + +| Message starts with or contains | What it means | What to do | +| --- | --- | --- | +| `The plugin approvals file is damaged.` | `approvals.json` cannot be read, so the CLI does not treat it as empty. The message names the file. | Remove the file that the message names, then approve the plugin again. | +| `The plugin lockfile is damaged.` | `plugins.lock.json` cannot be read. The CLI never replaces it on its own. | Remove the `plugins` folder described in [Managing plugins](managing.md), then install again. | +| `only a person can approve a plugin.` | `approve` ran where nobody can answer, such as agent mode or `--no-input`. | Run the command shown in the message in a terminal. | +| `this is a destructive command; re-run with --force` | `uninstall` ran in agent mode without `--force`. | Add `--force`, or run the command in a terminal. | + +An invalid `AUTH0_CLI_PLUGIN_APPROVALS` value is also an error. Its reason is `local_validation`. The message is `The value of AUTH0_CLI_PLUGIN_APPROVALS is not valid.` followed by the entry that is wrong, and the form it must have. Each entry is `registry/name@sha256:`, separated by commas. See [Agent mode and automation](automation.md). + +If the pin is valid, but the run still says the plugin is not approved, the plugin asks for different permissions than the ones you pinned. Read the current hash with `auth0 plugin info `, and compare it with the pin. + +## The plugin ran and failed + +When the plugin starts and then fails, the error has the code `plugin`. The reason says how it ended: + +| Reason | What it means | What to do | +| --- | --- | --- | +| `plugin_exit` | The plugin exited with a code other than zero. The message shows the code, and agents get it as `details.exitCode`. | Read what the plugin printed. The plugin's owner explains its exit codes. | +| `plugin_timeout` | The plugin ran longer than its timeout, and the CLI stopped it along with its child processes. | Run it again. If the task is long, ask the plugin's owner about the timeout. | +| `plugin_interrupted` | You interrupted the run, for example with Ctrl-C. | None. | +| `plugin_start_failed` | The CLI could not start the plugin. The message has the cause. | Reinstall the plugin. If it persists, report it with the full message. | + +If the CLI printed that the proxy denied API calls, the plugin asked for something that its manifest does not declare: + +```text + ▸ Plugin auth0/checkmate: 2 API calls were denied (read:users for GET /api/v2/users, GET /oauth/token). Review permissions.scopes in plugin.json. +``` + +The plugin may still have finished. The line is written for the plugin's author, so pass it to the owner listed as `OWNER` in `auth0 plugin info`. The line can also say that Auth0 refused calls because your session lacks a scope. In that case, run the `auth0 login` command that the install warning gave you, with the scopes it lists. + +## Author commands + +These reasons come from the commands under `auth0 plugin dev`. All of them need `AUTH0_CLI_PLUGIN_DEV_MODE=true`. See [Building a plugin](building.md) and [Reference](reference.md). + +| Reason | What it means | +| --- | --- | +| `plugin_dev_mode_required` | The author commands are off. Set `AUTH0_CLI_PLUGIN_DEV_MODE=true`. | +| `plugin_create_invalid` | `plugin dev create` was given a value it cannot use. The message names it. | +| `plugin_link_invalid` | A link cannot be made, or the linked build cannot be used. The message says why, and names the `validate` and `unlink` commands. | +| `plugin_not_linked` | `plugin dev unlink` named a plugin that is not linked. The message lists the linked plugins, or says that none are linked. | +| `plugin_manifest_invalid` | `plugin dev validate` found problems in `plugin.json`. The `details` object has a `problems` list, and each entry has the `path` of the field and a `message`. With `--json`, the command also prints the same list on stdout, next to `valid`. | + +## Start over + +If the plugin state looks wrong and nothing above fits, `auth0 plugin uninstall ` removes one plugin with its versions and approvals. To clear everything, remove the `plugins` folder described in [Managing plugins](managing.md). The next install downloads the registry index again and asks for approval again. Nothing else in the CLI's configuration lives in that folder. + +## Report a problem + +If you think a plugin or the registry is unsafe, or a check failed in a way you cannot explain, follow the reporting steps in [Security and trust model](security.md). For a bug in the plugin commands, open an issue on the CLI repository. Include the full message, the reason code, the plugin and its version, and the version of the CLI. Remove any tokens and client secrets before you paste output. + +## Next + +- [Reference](reference.md): reason codes, environment variables and paths in one place. +- [Agent mode and automation](automation.md): the error object and exit codes. +- [Managing plugins](managing.md): where the files are and how to remove them.