gclientid owns the Google credential store, $XDG_CONFIG_HOME/gclientid/, and
gclientid.creds is its API: client_file and token_file name the files for
the internal/desktop axes, oauth_creds, refresh_creds, and logout
load, refresh, and revoke tokens, token_name reads the account and axes back
out of a token filename, and reauth_cmd and reauthorize turn that into the
gclientid-auth command or the same call in Python. fastgws depends on gclientid
and re-exports the token functions from fastgws.auth; gclientid imports
nothing from fastgws.
Automatic re-authorization is a store setting, not a library default: provision
writes reauth = true and browser into config.ini, authorize_account
refreshes browser, and oauth_creds/refresh_creds read them from the token's
own directory when reauth=None. Any RefreshError (a RAPT Reauthentication is needed or an invalid_grant), a missing token, or missing scopes then runs one
authorize_account through reauthorize, which imports cli lazily to avoid
the cycle. A token outside a provisioned store, or reauth=False, raises with
the command instead. The September 2, 2026 live check broke the Desktop token's
refresh token by hand and oauth_creds(account=..., desktop=True) repaired it
through CDP Chrome without arguments.
gclientid uses Resource Manager and Service Usage directly over httpx2 when an
existing cloud-authorized --owner is available, including organization lookup,
project creation/deletion, operation polling, and API enablement. A run without
--owner finds or creates the project through Cloud Console, configures the
OAuth client, and obtains the owner's Cloud token before batch-enabling APIs
through Service Usage. fastcdp handles generic OAuth configuration and client
creation or update in the signed-in Console session; it never visits individual
API library pages.
Local account authorization uses CDP plus a one-shot loopback callback; remote
authorization uses the Web client through appapis copy/paste. No path
requires gcloud.
connect_browser() uses CDP.connect(timeout=60) by default. This connects to the normal Chrome profile after the developer enables Allow remote debugging in chrome://inspect/#remote-debugging; Chrome asks the developer to approve each new connection. connect_browser(default_browser=False) uses CDP.remote() and the dedicated CDP Chrome profile on port 9223. Both paths return (cdp, page) with a new tab, so setup never navigates the developer's focused tab away. fastcdp's active_page() skips chrome:// and devtools:// targets because Chrome accepts attachment but never answers page commands for them.
After initially adding this repository with ws-add, restart any persistent
Python/clikernel process so it sees the new workspace editable install.
The gclientid entry point is gclientid.cli:main; gclientid-auth is gclientid.cli:auth. Both are thin fastcore.script.call_parse wrappers over provision and authorize_account, which take the same options as keyword arguments. gclientid-auth takes the account email as its positional argument. A bare gclientid converges the project, OAuth app, Web client, owner Cloud token, and enabled APIs. gclientid --desktop does the same for the Desktop client. gclientid --owner <account> uses Resource Manager for project creation too; the owner must already have a gclientid token containing cloud-platform. gclientid --authorize additionally grants access to the requested data account. Local authorization reuses the Console CDP connection; --remote uses appapis for both owner setup and data authorization. A bare gclientid-auth connects to normal Chrome; --cdp-chrome selects the dedicated profile. Files live directly under $XDG_CONFIG_HOME/gclientid/ by default. --output replaces that directory; it does not add a project subdirectory.
The project is resolved in order: --project, the ID saved in config.ini, then project_id(owner), which is gclientids- plus ten hex characters of a SHA-256 of the casefolded owner email (-internal appended for the Internal project). The owner is --owner when given, otherwise the email console_account reads from the Console banner. Project IDs are globally unique, so the hash keeps the default stable across machines while avoiding collisions between owners. ensure_project_ui waits for the resolved Welcome page and checks its project ID. provision_project checks through Resource Manager. Existing projects are not recreated. enabled_apis lists enabled services through Service Usage, including all result pages. enable_apis submits only missing APIs in batches of at most 20. It makes no enable request when all requested APIs are enabled. The client step reuses a saved client and registers missing Web redirect URIs.
The default bootstrap declares the full scope set and creates the OAuth client first. The Console accepts manually pasted scopes before their APIs are enabled. _owner_creds loads or refreshes the owner's token from the selected output directory and client profile. A missing, insufficient, or rejected token triggers one cloud authorization using the current browser connection (or appapis for --remote). This does not use the store's general re-authorization path. The cloud preset contains only identity and cloud-platform scopes. Provisioning then enables missing APIs and optionally authorizes the data account.
Provisioning writes config.ini using fastcore.xdg.Config. It records the project, name, default preset, custom scopes/APIs, and audience, separately from the client JSON files. The declared scopes and enabled APIs are always MAX plus the custom extras, so the saved preset only chooses what gclientid-auth requests by default; its own --preset overrides that and repeatable --scope values augment the saved scopes for one authorization. Without config.ini, gclientid-auth defaults to google-apps, which allows authorization from a client JSON created elsewhere. Presets are Preset records of scopes and APIs; + unions two in order, which is how the built-ins compose and how a caller defines a new one.
--internal selects the Internal OAuth audience and an independent local profile: config-internal.ini, oauth-client-internal.json, and oauth-token-<account>-internal.json. Google allows one consent audience per project, so the Internal project is separate; the Web and Desktop clients of one audience share a project and its declared scopes. --desktop adds -desktop to the client and token filenames, after -internal when both apply. The workspace-addon preset combines identity and cloud-platform scopes with Cloud Resource Manager, Service Usage, IAM, and Workspace Add-ons APIs. Internal apps skip publication because only External apps have the testing-to-production lifecycle.
Authorization stores one oauth-token-<account>[-internal][-desktop].json per verified Google account and client. Email characters safe in filenames remain readable; other characters are percent-encoded. Token files use google-auth's authorized-user format with the verified account field retained. A refresh token is bound to the client that issued it, which is why each client has its own token files.
The max preset is the union of google-apps, developer, and workspace-admin. It is the broadest built-in preset, not every scope and API offered by Google; new scopes and APIs developers need are added to it. workspace-admin includes the Enterprise License Manager API and its read/write licensing scope.
Without --owner, project creation uses the signed-in Console and needs no prior gclientid credentials. The --owner token selects API project creation instead. For an Internal app, its email domain selects the single visible Cloud organization and the project is created under that organization; Internal provisioning therefore requires --owner. The same email must be offered as a support address in the active Console session, preventing accidental client creation under a different login. --account on gclientid --authorize, or the positional email of gclientid-auth, selects the data account during OAuth and must be an email. The provisioning CLI closes its setup tab and CDP connection on success or failure, and never prints client secrets or OAuth tokens. Provisioning and authorization both confirm the output directory is writable before opening an OAuth flow or mutating Google-side state.
cloud_creds(account) loads the owner's stored authorized-user token, which
must carry cloud-platform access, for the Resource Manager v3 and Service Usage v1 calls.
provision_project searches projects by global project ID and is idempotent: it
creates a missing project, validates an existing Internal project's parent,
grants the owner roles/serviceusage.serviceUsageConsumer only if missing, and
batch-enables missing services. Internal organization selection uses
organizations.search(query='domain:<domain>') and deliberately requires one
match rather than silently choosing. All long-running operations are polled
through each service's operations.get resource. delete_project uses Resource
Manager's recoverable project deletion API.
The API path is eight fixed REST calls (projects:search, projects create,
operations get, projects delete, :getIamPolicy, :setIamPolicy,
services list, services:batchEnable) made with httpx2 through _call, which refreshes the
stored token when needed and raises Google's error body. organizations:search
and projects:search are GETs with a query parameter. A discovery-driven
client bought nothing for so few known endpoints, and using fastgws here would
put gclientid above the library that depends on it.
Google's IAP API can create OAuth clients programmatically, but those clients are locked to IAP and do not support arbitrary redirect URIs. It therefore cannot create the general-purpose Web client needed here. The Auth Platform UI is the only CDP-controlled portion.
For an External app published without verification, use the repository as the application homepage and privacy policy:
https://answerdotai.github.io/gclientid/https://answerdotai.github.io/gclientid/privacy/
Add answerdotai.github.io to the consent screen's authorized domains. GitHub
blob URLs caused a generic branding-save failure, while the Pages URLs saved
successfully. This keeps setup independent of a particular developer's email.
The August 31, 2026 live Web OAuth check used
https://oauth.appapis.org/redirect, PKCE, state validation, offline access,
and the max preset. Authorization as jhoward@gmail.com succeeded without a
test-user allowlist, produced a refresh token, verified the returned UserInfo,
and wrote the standard account token. Google's token endpoint requires the Web
client secret despite PKCE, so create_client waits for the asynchronously
populated completion dialog and captures both values before dismissing it.
configure_app calls the public steps setup_auth, set_branding, set_scopes, and (for External apps) publish_app. Call these separately for interactive setup. setup_auth initializes the app when the Overview route offers Get started. It does not change an existing app's name, audience, or contact details. API Services terms are accepted automatically by default. accept_terms=False (--no-accept-terms in the CLI) pauses for manual acceptance. Normal Console waits default to 10 seconds. Manual terms acceptance retains its separate long human-interaction timeout.
create_client creates one Web or Desktop client and writes its JSON with mode 0600, in Google's web or installed shape. Web clients register REDIRECT_URIS: the fixed loopback callback, the appapis redirect, and the fasthtml set DEV_REDIRECT_URIS (localhost and 127.0.0.1 on ports 5001, 5002, 8000, and 8080 at /redirect; Google matches host and port literally, so both hosts are needed). The redirect fields are URIs <n> textboxes inside the Authorized redirect URIs group, on both the create form and the client edit page, so _add_redirect_fields serves create_client and add_redirects. add_redirects reads the URIs Google currently holds from the edit page, adds only the missing ones, and rewrites the JSON list from that truth. Desktop clients have no redirect list; their JSON records Google's conventional http://localhost.
The Data Access route is /auth/scopes, not /auth/dataaccess. set_scopes reads each page of the scope tables, removes display whitespace, and expands Google's .../auth/ abbreviation. It pastes only missing scopes and skips the editor when none are missing. set_branding compares the homepage, privacy-policy URL, and authorized domains before changing anything. It adds a missing domain without replacing existing domains. add_redirects does not save unchanged redirects or rewrite unchanged client JSON.
authorize_google creates a fresh state and PKCE verifier. Its default path opens OAuth in the supplied CDP connection (or the operating system's default browser for direct Python calls) and receives the response from a one-shot loopback server. A Web client binds the registered port 127.0.0.1:53682; a Desktop client binds port 0 and uses whichever port it gets, which Google permits for installed clients. The listener starts before the authorization URL is built, serves one completion page, and closes after the callback; a bind failure directs the user to --remote. The remote path prints and opens an appapis URL, then reads the copied payload; --no-open-browser only prints it. Desktop clients cannot register the appapis redirect, so remote=True raises for them. The PKCE verifier never leaves the waiting process in either path. An email-valued account becomes Google's login_hint, and returned UserInfo is verified before saving token JSON with mode 0600.
The CDP driver is a finite sequence, not a click loop: it selects one named account when offered, expands the unverified-app warning once when present, selects granular permissions once when present, and clicks at most one Continue and one Allow. Known Google OAuth error pages raise immediately instead of waiting for the callback timeout.
Console and OAuth waits detect Google's sign-in challenge and identity-confirmation routes. They print a message, bring the tab forward, and wait up to 600 seconds for the user to complete verification. They do not click through verification screens. _wait_ax allows 100 ms for the page to settle before inspecting it. Normal polling then uses fastcdp's 200 ms interval.
The redirect application displays only code and state on success, or Google's error, error_description, and state on failure. Extra callback parameters such as scopes and authuser are deliberately omitted from the copied payload. _callback_code requires exactly one of code or error and rejects a state mismatch before token exchange.
Before opening OAuth, authorize_google checks whether the saved refresh token matches the client, requested account, and requested scopes, then submits a refresh grant to Google. A missing, insufficient, or invalid_grant token makes the single authorization request include prompt=consent; a verified refresh token allows the lighter request and is retained if Google omits a new one. Authorization never starts a second browser or copy/paste round trip.
The August 27 max retest without forced consent still required a passkey and completed in 29.9 seconds after the user satisfied it. An immediate repeat completed in 3.1 seconds without a passkey or Chrome-profile prompt. Both attempts verified j@answer.ai, granted all 19 expected scopes, and returned a refresh token. Google therefore appears to require recent authentication for the broad grant rather than passkey authentication on every authorization; forced consent was not the sole trigger.
Broad scope grants can still trigger a native passkey or Touch ID prompt. The user completes those browser-native controls while the CLI waits for the callback. A Workspace login can also open chrome://managed-user-profile-notice/; _open_cdp watches Chrome targets during authorization, selects Use Chrome without an account, and reactivates the OAuth tab so that profile creation does not stall the flow.
Resource Manager deletion is a soft deletion: the project becomes inaccessible immediately but remains recoverable for 30 days before permanent deletion.
Google Cloud Console's full accessibility-tree response was about 3.1 MB in the
initial live test. fastcdp must connect with websockets.connect(..., max_size=None) rather than its former 1 MB default, or ax_tree() closes the
connection before returning.
Console route changes use fastcdp's normal goto load wait; they do not catch
navigation timeouts or infer readiness from post-hoc network silence. Save
actions on the branding and scope pages do not navigate, so they wait for the
page's Save control to settle (or its error dialog) before reading the result.
Save on a client's edit page navigates back to the clients list, so
add_redirects uses click_and_wait.
Project existence is read from the Welcome page's Copy Project ID to clipboard control. The resolved no-project page offers Create or select a project. The banner's No project selected state is not evidence of absence: it also appears briefly while existing projects load. The signed-in email is read from the banner's Account: button on the welcome route.
Google Cloud's Angular routes render after the document load event. Auth setup
therefore waits for the Overview route's OAuth Overview accessibility heading
before checking whether its Get started link is present.
The support-email picker is a custom cfc-select: Chrome reports its disabled
state in the accessibility tree even though the element has no stable DOM
disabled or label attributes. Setup polls that AX property and returns the fresh
tree used for the click.
The live create/delete check used gclientid-test-20260826-fcdc. Google
confirmed that it is shut down and scheduled for permanent deletion after
September 25, 2026.