Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fb44323
chore: merge production into dev after 5.8.0 (#236)
dcd-release-please[bot] Oct 19, 2026
aef26dd
chore: bump eslint-plugin-unicorn from 77.0.0 to 78.0.0 (#239)
dependabot[bot] Oct 20, 2026
5fffd7a
deps: bump the minor-and-patch group across 1 directory with 5 update…
dependabot[bot] Oct 20, 2026
5f710d5
feat(login): reuse the last org and add --org (#243)
finalerock44 Oct 21, 2026
8cedb4e
feat(auth): fetch the Supabase key from the API instead of compiling …
riglar Oct 22, 2026
766770a
ci: skip promotion-pin on release-please PRs (#249)
riglar Oct 22, 2026
55a0609
feat(encrypt): list --encrypt in --help now encryption is on by defau…
finalerock44 Oct 22, 2026
452324b
feat: fetch the KEK public key from the API instead of pinning it (#251)
finalerock44 Oct 22, 2026
92a8812
chore: merge production into dev after 5.9.0 (#248)
dcd-release-please[bot] Oct 22, 2026
43cdf5f
deps: patch ip-address advisory (#256)
riglar Oct 23, 2026
93bef01
ci: authenticate the Claude action by workload identity federation (#…
riglar Oct 23, 2026
d6aea84
chore: merge production into dev after 5.10.0 (#254)
dcd-release-please[bot] Oct 23, 2026
69249e3
ci: authenticate the Claude action with the org CLAUDE_CODE_OAUTH_TOK…
riglar Oct 23, 2026
8f195dd
Merge origin/dev into fix/agent-devices-qa
riglar Oct 23, 2026
83b2250
fix(auth): keep the stored session when a refresh fails
riglar Oct 23, 2026
86ffcab
fix(device): show run results as one line per flow
riglar Oct 23, 2026
df85a5d
fix(device): leave session out of --json for list, models and stop --all
riglar Oct 23, 2026
943a10c
fix(device): infer the platform from --model
riglar Oct 23, 2026
db44bb4
fix(device): warn when the video and log are still uploading
riglar Oct 23, 2026
efb9a99
feat(device): add request-human and human, like the MCP tools
riglar Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,26 @@ jobs:
# The plugin also stops early on PRs it judges trivial. The prompt above
# tells Claude to post a one-line note in that case, so a deliberate skip
# leaves a comment and only a real failure trips this check.
#
# Nor is a PR that edits THIS file: the action refuses to run unless the
# workflow is identical to the default branch's ("Workflow validation
# failed"), a deliberate guard against a PR rewriting its own reviewer. It
# exits green having posted nothing, so pass with a notice instead.
- name: Check a review comment was posted
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
SELF: ${{ github.workflow_ref }}
run: |
# workflow_ref is "owner/repo/.github/workflows/<file>@<ref>".
self_path="${SELF#"$REPO"/}"
self_path="${self_path%@*}"
if gh api "repos/$REPO/pulls/$PR/files" --paginate --jq '.[].filename' | grep -qxF "$self_path"; then
echo "::notice::This PR edits $self_path, so the review action skipped it by design (it only runs the default branch's version of its own workflow). No review on this PR."
exit 0
fi

# Prints the time of every claude[bot] comment, inline comment and review.
claude_posts() {
gh api "repos/$REPO/issues/$PR/comments" --paginate \
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/cli-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,11 @@ jobs:
#
# It has to be on a NORMAL commit, not the merge commit — release-please's
# commit splitting is unreliable on merges.
#
# Release-please's own release PR also targets `production` but is not a
# promotion: it never carries a pin, so the check would always fail on it.
promotion-pin:
if: github.event_name == 'pull_request' && github.base_ref == 'production'
if: github.event_name == 'pull_request' && github.base_ref == 'production' && !startsWith(github.head_ref, 'release-please--')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand Down
27 changes: 15 additions & 12 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
# Gitleaks configuration — extends the default ruleset.
#
# The only allowlisted secrets are PUBLIC key material committed in
# src/config/environments.ts:
# - the two Supabase anon keys — anon-role JWTs, designed to be embedded in
# client code and gated by RLS (see the doc comment in that file).
# - the two KEK public keys (prod + dev) for client-side binary envelope
# encryption — base64 of the raw 32-byte X25519 PUBLIC half. They can only
# *encrypt*; the private halves live solely on the API, never in this repo.
# Both are intentionally not secret; gitleaks flags them on entropy alone
# src/config/environments.ts: the two KEK public keys (prod + dev) for
# client-side binary envelope encryption — base64 of the raw 32-byte X25519
# PUBLIC half. They can only *encrypt*; the private halves live solely on the
# API, never in this repo. gitleaks flags them on entropy alone
# (generic-api-key), not because it recognizes them as credentials.
#
# The two legacy Supabase anon keys (prod + dev) are also listed, but only
# because they remain in git history and CI scans the full history. The source
# no longer contains them: the CLI fetches its publishable key from the API's
# GET /cli-config at runtime (dcd#1112). Do not add a Supabase key back.
#
# They are allowlisted by EXACT VALUE, deliberately not by file path or by the
# whole `jwt` / `generic-api-key` rule: a Supabase service_role key is also a
# JWT, and a KEK *private* key is the same shape as its public half, so a
# path/rule allowlist would let a genuinely sensitive key pasted into the same
# file slip through. Matching exact values keeps that detection intact.
# whole `generic-api-key` rule: a KEK *private* key is the same shape as its
# public half, so a path/rule allowlist would let a genuinely sensitive key
# pasted into the same file slip through. Matching exact values keeps that
# detection intact.
#
# When a KEK is rotated, replace the corresponding value below — do not simply
# append, or the retired key stops being distinguishable from a live secret.
[extend]
useDefault = true

[allowlist]
description = "Public Supabase anon keys and KEK public keys (safe to commit)"
description = "KEK public keys, and retired Supabase anon keys left in history (safe)"
regexes = [
# retired legacy anon keys (prod, dev), history only
'''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0\.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8''',
'''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30\.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ''',
# prod kekPublicKey v1
Expand Down
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env sh
# Scan staged changes for secrets before they ever enter git history.
# Mirrors the gitleaks CI gate (.github/workflows/cli-ci.yml) and shares the
# same allowlist (.gitleaks.toml), so the public Supabase anon keys won't trip.
# same allowlist (.gitleaks.toml), so the public KEK keys won't trip.
#
# gitleaks is NOT a dependency of this package — contributors install the binary
# themselves. If it's missing we skip (exit 0) rather than block: CI is the
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "5.7.1"
".": "5.10.0"
}
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
# Changelog

## [5.10.0](https://github.com/devicecloud-dev/dcd-cli/compare/v5.9.0...v5.10.0) (2026-10-22)


### Features

* **encrypt:** list --encrypt in --help now encryption is on by defau… ([#250](https://github.com/devicecloud-dev/dcd-cli/issues/250)) ([55a0609](https://github.com/devicecloud-dev/dcd-cli/commit/55a0609abbc686021fe969c523bcaa1efa994658))
* **encrypt:** list --encrypt in --help now encryption is on by default (dcd[#1138](https://github.com/devicecloud-dev/dcd-cli/issues/1138)) ([55a0609](https://github.com/devicecloud-dev/dcd-cli/commit/55a0609abbc686021fe969c523bcaa1efa994658))
* fetch the KEK public key from the API instead of pinning it ([#251](https://github.com/devicecloud-dev/dcd-cli/issues/251)) ([452324b](https://github.com/devicecloud-dev/dcd-cli/commit/452324ba6f6313fe9e4d418c3db655e858f79540))


### Miscellaneous

* pin the 5.10.0 promotion ([d85115c](https://github.com/devicecloud-dev/dcd-cli/commit/d85115c1efc1da80b7eaeb9dacca49dd0d37257b))

## [5.9.0](https://github.com/devicecloud-dev/dcd-cli/compare/v5.8.0...v5.9.0) (2026-10-22)


### Features

* **auth:** fetch the Supabase key from the API instead of compiling it in ([#245](https://github.com/devicecloud-dev/dcd-cli/issues/245)) ([8cedb4e](https://github.com/devicecloud-dev/dcd-cli/commit/8cedb4e2f133f4300b669f877896e65c61c38820))
* **login:** reuse the last org and add --org ([#243](https://github.com/devicecloud-dev/dcd-cli/issues/243)) ([5f710d5](https://github.com/devicecloud-dev/dcd-cli/commit/5f710d55d024c6869d529c606f261530ac9013ad))


### Dependencies

* bump the minor-and-patch group across 1 directory with 5 updates ([#238](https://github.com/devicecloud-dev/dcd-cli/issues/238)) ([5fffd7a](https://github.com/devicecloud-dev/dcd-cli/commit/5fffd7ae00e2e6749271ae859c6cf6c47d98678f))


### Miscellaneous

* pin the 5.9.0 promotion ([57cf6b1](https://github.com/devicecloud-dev/dcd-cli/commit/57cf6b1fc0cb74ac4fe9f30b6659095d4321fbfc))

## [5.8.0](https://github.com/devicecloud-dev/dcd-cli/compare/v5.7.1...v5.8.0) (2026-10-16)


### Features

* **cloud:** add --priority flag for within-org run ordering ([#222](https://github.com/devicecloud-dev/dcd-cli/issues/222)) ([510d64b](https://github.com/devicecloud-dev/dcd-cli/commit/510d64baa34e3f7f19c5483f693aced20c0f0758))
* **cloud:** add --priority flag for within-org run ordering (dcd[#1581](https://github.com/devicecloud-dev/dcd-cli/issues/1581)) ([510d64b](https://github.com/devicecloud-dev/dcd-cli/commit/510d64baa34e3f7f19c5483f693aced20c0f0758))


### Bug Fixes

* **cloud:** re-send a flow submit whose response was lost ([#226](https://github.com/devicecloud-dev/dcd-cli/issues/226)) ([f4db8f9](https://github.com/devicecloud-dev/dcd-cli/commit/f4db8f9d0b51d358b62153f40c2164abc67cb078))
* **device:** drop Android API 29-31 from the --android-api-level help ([#227](https://github.com/devicecloud-dev/dcd-cli/issues/227)) ([05ea2dc](https://github.com/devicecloud-dev/dcd-cli/commit/05ea2dc292b1c8d2d453a4ec05b3c34577cf2c61))
* **validation:** mirror the API's Pixel 7 / API 36 Android default ([#225](https://github.com/devicecloud-dev/dcd-cli/issues/225)) ([3daeb94](https://github.com/devicecloud-dev/dcd-cli/commit/3daeb94e077a3819f4568c74cc5059bd8ac9ba9b))


### Performance

* **cloud:** poll results with ?view=summary and If-None-Match ([#219](https://github.com/devicecloud-dev/dcd-cli/issues/219)) ([e916c59](https://github.com/devicecloud-dev/dcd-cli/commit/e916c59631ee10aee0c315af016582747afa0b81))


### Dependencies

* bump the minor-and-patch group with 6 updates ([#220](https://github.com/devicecloud-dev/dcd-cli/issues/220)) ([2b88b64](https://github.com/devicecloud-dev/dcd-cli/commit/2b88b6489d77591f3f47b00c980e11d5e6cc2e27))

## [5.7.1](https://github.com/devicecloud-dev/dcd-cli/compare/v5.7.0...v5.7.1) (2026-10-07)


Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Top-level `defineCommand` in `src/index.ts` wires eleven subcommands (`cloud`, `

**Auth.** Every command calls `resolveAuth({ apiKeyFlag })` (`src/utils/auth.ts`) once and threads the returned `AuthContext` into gateways/services. `ApiGateway` and `fetchCompatibilityData` spread `auth.headers` into fetch headers — they no longer accept a raw api key. Precedence: `--api-key` flag > `DEVICE_CLOUD_API_KEY` env > stored session from `dcd login`. `resolveAuth` refreshes expiring Supabase sessions via `CliAuthGateway.refresh` and rewrites the config atomically. A session's access token lasts about an hour, so code that can outlive it re-checks with `isAuthExpiring` / `refreshAuth`: the `dcd cloud` poll loop and the report downloads after it, the MCP context, and `dcd_run_cloud_test`'s wait loop. `refreshAuth` keeps the org the command started with and leaves API-key contexts untouched.

**Config store.** `dcd login` writes `$XDG_CONFIG_HOME/dcd/config.json` (fallback `~/.dcd/config.json`, 0600). Shape: `{ version, env, api_url, supabase_url, session: { access_token, refresh_token, expires_at, user_email, user_id }, current_org_id, current_org_name }`. `DCD_CONFIG_DIR` overrides the directory (used by tests). The login command itself (`src/commands/login.ts`) uses PKCE (S256) with a server rendezvous — no loopback server: it mints `state`, `code_verifier`, and `code_challenge`, opens `<frontend>/cli-login?state=...&code_challenge=...`, then polls the dcd API's `POST /cli-login/claim` with `{state, code_verifier}` while the frontend POSTs proof of identity (the browser's access token) to `POST /cli-login/handoff`; the API verifies the token, **mints a dedicated Supabase session for the CLI** (its own refresh-token family — sharing the browser's tokens caused "Invalid Refresh Token: Already Used" whenever either client rotated them), stores it keyed by state, then on claim verifies `sha256(verifier) === challenge` and returns it. After claiming, the CLI fetches `/me/orgs` and prompts for an org (the same picker `dcd switch-org` uses). The frontend lives in `../dcd/frontend/app/features/cli-login/CliLoginScreen.tsx`.
**Config store.** `dcd login` writes `$XDG_CONFIG_HOME/dcd/config.json` (fallback `~/.dcd/config.json`, 0600). Shape: `{ version, env, api_url, supabase_url, session: { access_token, refresh_token, expires_at, user_email, user_id }, current_org_id, current_org_name }`. `DCD_CONFIG_DIR` overrides the directory (used by tests). The login command itself (`src/commands/login.ts`) uses PKCE (S256) with a server rendezvous — no loopback server: it mints `state`, `code_verifier`, and `code_challenge`, opens `<frontend>/cli-login?state=...&code_challenge=...`, then polls the dcd API's `POST /cli-login/claim` with `{state, code_verifier}` while the frontend POSTs proof of identity (the browser's access token) to `POST /cli-login/handoff`; the API verifies the token, **mints a dedicated Supabase session for the CLI** (its own refresh-token family — sharing the browser's tokens caused "Invalid Refresh Token: Already Used" whenever either client rotated them), stores it keyed by state, then on claim verifies `sha256(verifier) === challenge` and returns it. After claiming, the CLI fetches `/me/orgs` and picks the org with `selectOrg` (`src/utils/orgs.ts`): `--org <name|id>` if given, else the only org, else the picker `dcd switch-org` uses, starting on the last login's org. That org only counts when the stored `api_url` matches, because org ids are per environment. Without a TTY (`canPrompt` in `src/utils/ci.ts`) the prompts can't be answered: the "already logged in" confirm takes its default and signs in again, the last login's org is used, and with none the command fails pointing at `--org`. `dcd logout` deletes the whole config, so nothing is remembered after it. The frontend lives in `../dcd/frontend/app/features/cli-login/CliLoginScreen.tsx`.

**Cross-repo auth surface.** The dcd API's `ApiKeyGuard` accepts either `x-app-api-key` (existing) or `Authorization: Bearer <jwt>` + `x-dcd-org: <id>`. For Bearer it verifies the JWT, checks `user_org_profile` membership, and injects the org's api_key back into the request headers so existing `@Headers(APP_API_KEY_HEADER)` controller code keeps working unchanged. `dcd switch-org` calls `GET /me/orgs`, a JWT-only endpoint at `../dcd/api/src/apps/me/me.controller.ts`.

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"chai": "^6.2.2",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-unicorn": "^77.0.0",
"eslint-plugin-unicorn": "^78.0.0",
"husky": "^9.1.7",
"mocha": "^12.0.0",
"prettier": "^3.8.4",
Expand Down Expand Up @@ -68,7 +68,7 @@
"test:unit": "node scripts/test-runner.mjs --unit",
"typecheck": "tsc --noEmit -p tsconfig.test.json"
},
"version": "5.7.1",
"version": "5.10.0",
"bugs": {
"url": "https://discord.gg/gm3mJwcNw8"
},
Expand Down Expand Up @@ -110,7 +110,7 @@
"esbuild@<0.28.1": ">=0.28.1",
"qs@<6.16.0": "6.16.0",
"fast-uri@>=3.0.0 <3.1.8": "3.1.8",
"hono@>=4.0.0 <4.13.7": "4.13.7",
"hono@>=4.0.0 <4.13.11": "4.13.11",
"source-map-js@<1.2.2": "1.2.2",
"micromatch>picomatch": "^2.3.2",
"tinyglobby>picomatch": "^4.0.4"
Expand Down
Loading
Loading