credential broker: Play tokens from POST /api/google/token (cached, single flight, local key fallback), grants and APIs per package in validate, Play write preflight, apps check and the desktop Accounts view; Google 403 names the API or grant, no retry
This commit is contained in:
parent
304407e523
commit
2c5e30090f
27 changed files with 1145 additions and 43 deletions
|
|
@ -26,6 +26,8 @@ The [README](../README.md) is the short tour; everything in detail lives here.
|
|||
- [Skills](skills.md)
|
||||
- [MCP server and the desktop machine interface](mcp.md)
|
||||
- [Safety model](safety.md)
|
||||
- [Google access through a credential broker](credential-broker.md): Play tokens and permission checks from a server of
|
||||
yours, no Google key on the machine
|
||||
- [Idempotency: every write converges](idempotency.md)
|
||||
- [Pitfalls: lessons from real use](pitfalls.md)
|
||||
|
||||
|
|
|
|||
85
docs/credential-broker.md
Normal file
85
docs/credential-broker.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Google access through a credential broker
|
||||
|
||||
A credential broker is a server of yours that holds the Google service-account keys, the Cloud project facts and the
|
||||
Play permission grants. With one configured, storebound keeps no Google key on the machine: it asks the broker for a
|
||||
short-lived access token for each Google API call and reads the package's Play permissions from it before a command
|
||||
writes anything. The local key (`PLAY_SERVICE_ACCOUNT`) keeps working and is the fallback.
|
||||
|
||||
## Setup
|
||||
|
||||
`~/.storebound/broker.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://broker.example.com",
|
||||
"apiKeyFile": "~/.storebound/broker.key",
|
||||
"account": "play-publisher@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
- `url`: the broker's base URL. Only `https://` (plain `http://` only for `localhost`, a local test broker).
|
||||
- `apiKeyFile`: a file with one scoped API key on one line, with the scopes `google:read` and `google:token`. On macOS
|
||||
and Linux it must be `chmod 600`: storebound refuses a key file that the group or others can read. Default:
|
||||
`broker.key` next to `broker.json`. The key is never printed, never written into a config or a log.
|
||||
- `account` (optional): the service account e-mail the broker mints tokens for. A credential profile or the project
|
||||
can name its own with `PLAY_SERVICE_ACCOUNT_EMAIL` (env, `~/.storebound/credentials/<profile>.json`,
|
||||
`.storebound/credentials.json`); without either, the `client_email` of a local key is used, and for Play commands
|
||||
the account the broker names for the package.
|
||||
|
||||
Overrides: `STOREBOUND_BROKER_URL`, `STOREBOUND_BROKER_KEY_FILE` (env wins over the file), `STOREBOUND_BROKER_CONFIG`
|
||||
(another broker.json). Off: `STOREBOUND_BROKER=off` for one run, `"broker": false` in a credential profile (that profile
|
||||
keeps its key file), or env `STOREBOUND_PROFILE_<NAME>_BROKER=off`.
|
||||
|
||||
## What storebound calls
|
||||
|
||||
| call | when |
|
||||
|---|---|
|
||||
| `POST /api/google/token` `{ "account", "scopes": ["androidpublisher"] }` -> `{ "accessToken", "expiresAt" }` | before Google calls. Kept in memory until 5 minutes before `expiresAt`; parallel calls share one request |
|
||||
| `GET /api/google/play/<package>/storebound` | `storebound validate`, the preflight of Play write commands, `apps check` and the desktop's Accounts view |
|
||||
|
||||
Scopes: `androidpublisher` for the Play Developer API, `cloud-platform.read-only` for Play statistics exports and Play
|
||||
Games configuration reads. The broker hands out no scope for the Play Developer Reporting API (vitals, the app list of
|
||||
`apps list`) or for Play Games configuration writes: those use the local key, and say so when there is none.
|
||||
|
||||
## Fallback and failures
|
||||
|
||||
- Broker not configured: the local key, as before.
|
||||
- Broker not answering (network error, timeout, 5xx) or not knowing the account (400 / 404 on a token): the local key,
|
||||
with one warning per run. No local key: the error names the broker.
|
||||
- Broker refusing the API key (401 / 403): storebound stops with "the key lacks google:token" and does not retry or
|
||||
fall back. Give the key its scopes on the broker.
|
||||
- A package the broker does not know (404 on `/storebound`): a warning to register it there. It is not a missing
|
||||
Play access; the command still runs and Google answers for itself.
|
||||
|
||||
## Permission checks
|
||||
|
||||
The broker's answer lists the service account's Play permissions for the package and the APIs enabled in its Cloud
|
||||
projects. Before a Play command writes, storebound compares them with what the command needs and stops before any
|
||||
Play call, naming the exact permission or API and the link to fix it:
|
||||
|
||||
| work | Play permission | commands |
|
||||
|---|---|---|
|
||||
| uploads, releases, rollouts | `CAN_MANAGE_TRACK_APKS` | `play upload`, `play release`, `play rollout`, `play share` |
|
||||
| store listing and images | `CAN_MANAGE_PUBLIC_LISTING` | `play listing`, `play images` |
|
||||
| products and subscriptions | `CAN_MANAGE_ORDERS` | `play products`, `play subscriptions` |
|
||||
| Data safety, app content | `CAN_MANAGE_APP_CONTENT` | `play data-safety` |
|
||||
| review replies | `CAN_REPLY_TO_REVIEWS` | (listed in the checks) |
|
||||
| Play Games Services | API `gamesconfiguration.googleapis.com` | `play games` |
|
||||
|
||||
Every Play call also needs `androidpublisher.googleapis.com` enabled. A dry run (`--dry`) and reads only warn.
|
||||
`storebound validate` reports the same gaps as blockers, the permissions and APIs the broker lists, and SDK config
|
||||
files (`google-services.json`, `GoogleService-Info.plist`) the broker reports as drifted or whose sha256 differs from
|
||||
the repo's copy.
|
||||
|
||||
When Google itself answers 403 `SERVICE_DISABLED` or `PERMISSION_DENIED`, the error names the API (with its enable
|
||||
link) or the missing app access (with the Users and permissions link), adds what the broker lists for the package, and
|
||||
is never retried.
|
||||
|
||||
## Where it shows
|
||||
|
||||
- `storebound doctor`: `Play: tokens from the credential broker <url> for p***@...` (or the local key, and why the
|
||||
broker is not usable). `--json`: `play.source` is `"broker"` or `"file"`.
|
||||
- `storebound accounts`: a "Play via" column (credential broker / key file) per profile.
|
||||
- `storebound validate --json` (and MCP `validate`): `credentials.play.source`.
|
||||
- `storebound apps check --store play --json` and the desktop's Accounts view: `source` and, per Play app of the
|
||||
machine, the permissions and APIs the broker lists and what is missing.
|
||||
|
|
@ -115,12 +115,16 @@ Store accounts belong to the machine, everything else to the project:
|
|||
`.storebound/credentials.json`.
|
||||
- **Order:** environment variables win over everything, then a profile the project names, then the project file,
|
||||
then the default profile.
|
||||
- **Google keys on a server of yours:** with a credential broker in `~/.storebound/broker.json`, Play tokens come from
|
||||
it and the machine needs no Google key; the local key stays the fallback. See
|
||||
[Google access through a credential broker](credential-broker.md).
|
||||
|
||||
The names (env, profile and project file alike; the project file is gitignored by `init`):
|
||||
|
||||
| name | what |
|
||||
|---|---|
|
||||
| `PLAY_SERVICE_ACCOUNT` | path to the Google Cloud service account JSON key (or the JSON itself) with access to your app in Play Console |
|
||||
| `PLAY_SERVICE_ACCOUNT_EMAIL` | with a [credential broker](credential-broker.md): the service account the broker mints tokens for (no key file needed) |
|
||||
| `ASC_KEY_ID`, `ASC_ISSUER_ID` | App Store Connect API team key (Users and Access > Integrations) |
|
||||
| `ASC_P8` | path to `AuthKey_<id>.p8` (default `~/.appstoreconnect/private_keys/AuthKey_<id>.p8`) |
|
||||
| `ASC_VENDOR_NUMBER` | vendor number for `reports apple-sales` / `apple-finance` (Payments and Financial Reports) |
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ fails without the fix.
|
|||
|
||||
| # | date | lesson | guard |
|
||||
|---|---|---|---|
|
||||
| 95 | 2026-10 | The Google service-account keys moved to the owner's credential broker, but storebound still wanted a local `PLAY_SERVICE_ACCOUNT`, and a Play write with a missing permission ran its edit before Google's bare 403. | `cli/lib/broker.ts` (broker.json, key file mode 600, token cache and single flight, grants, `playPreflight` before Play write commands), `playToken` in `cli/lib/google-auth.ts` (broker first, local key fallback when it does not answer), `cli/lib/google-access.ts` (403 SERVICE_DISABLED / PERMISSION_DENIED -> exact API or grant with its link, never retried), `validate` blockers; `tests/credential-broker.test.ts` against a local fake broker. Skill pitfall 122, docs/credential-broker.md. |
|
||||
| 94 | 2026-10 | An app with managed publishing was approved but never went live: `play status` said "In review / ready to publish" with no next step, and the Publishing overview "Publish N changes" button (production + Data safety) was easy to miss or to confuse with Discard changes. | `console plan play-publish-changes` (final "Publish N changes" is the owner's, Discard changes / Stop managed publishing never-press), `console verify play-publish-changes` (API), the hint in `play status`, `status` and the desktop Releases tab (`cli/lib/play-publish.ts`, `tests/console-play-plans.test.ts`). |
|
||||
| 93 | 2026-10 | A git-ignored, never-captured frames folder rendered store images with no app or game in them; render only logged 404s and `shots check` said 0 errors. | `shots render` refuses to start while a scene frame is missing (`framesForRender` in `cli/shots/lib.ts`) and fails when any load failed; report.json `missing` is a `frame-missing` error in `shots check` / `validate` (`cli/shots/report.ts`, `tests/shots-missing-frames.test.ts`). |
|
||||
| 90 | 2026-10 | `apple submit --build N --version X --yes` after `apple submit --no-submit --yes` (the documented two-step flow) printed the plan (every item "in it already", then "submit PATCH submitted: true"), logged "not added ...: already in the draft submission" and ended with "nothing to submit" without the PATCH: the submit only ran for new items or an UNRESOLVED_ISSUES submission. | `willSubmit` (cli/lib/review-submit.ts) also submits a READY_FOR_REVIEW draft that already holds the named app version (iap-submit: one of the asked-for items), with the same "not ready yet" retry. `tests/resubmit.test.ts` ("the second step submits the prepared draft"). |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue