Files
wehub-resource-sync 70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:11 +08:00

141 lines
9.1 KiB
Markdown

# AGENTS.md — workers-auth
OAuth-2.0-with-PKCE flow against Cloudflare's `dash.cloudflare.com` (or staging /
custom-overridden) endpoints. Used by wrangler and (in future) other Cloudflare
CLIs. Internal-only — published as `prerelease: true`.
## STRUCTURE
- `src/pkce.ts` — PKCE code-verifier / code-challenge generation (RFC 7636)
- `src/errors.ts``ErrorOAuth2` class hierarchy + `toErrorClass` mapper
- `src/generate-auth-url.ts` — authorize URL builder
- `src/generate-random-state.ts` — CSRF state generator
- `src/env-vars.ts``WRANGLER_*` and `CLOUDFLARE_AUTH_*` env-var getters
- `src/access.ts` — Cloudflare Access detection + service-token / `cloudflared` headers
- `src/config-file/auth.ts` — the `AuthConfigStorage` / `UserAuthConfig` storage contract (interfaces only). The default plaintext-TOML implementation now lives alongside the credential-store layer at `src/credential-store/file-store.ts` (see "Credential storage" below). Wrangler's `src/user/auth-config-file.ts` exposes a generic `createTomlFileStorage<T>` helper for non-credential TOML stores (the temporary-preview-account storage) and re-exports `getAuthConfigFilePath` for back-compat.
- `src/config-file/temporary.ts``TemporaryAccountStorage` / `TemporaryPreviewAccount` storage contract for the temporary-preview-account flow
- `src/config-file/index.ts` — generic `ConfigStorage<T>` interface shared by the auth and temporary-account contracts
- `src/state.ts``readStoredAuthState()` + `StoredAuthState` shape
- `src/token-exchange.ts` — auth-code → token + refresh-token rotation + `fetchAuthToken`
- `src/callback-server.ts` — local HTTP server for the OAuth callback (listens on the host/port from the consumer's `redirectUri`)
- `src/flow.ts``createOAuthFlow(ctx)` factory wiring everything together
- `src/context.ts``OAuthFlowContext` interface (DI surface)
- `src/credential-store/` — opt-in OS-keyring-backed credential persistence (see below)
- `src/test-helpers/` — MSW handlers for consumers' tests (`@cloudflare/workers-auth/test-helpers`)
### Credential storage (`src/credential-store/`)
Pluggable credential persistence layer that consumers can wire into the
OAuth flow via `ctx.storageFactory`. Default backend is the plaintext
TOML file (`FileCredentialStore`); an opt-in `EncryptedFileCredentialStore`
writes AES-256-GCM-encrypted credentials to a sibling `.enc` file using a
key held in the OS keyring. Every store is profile-aware: the file path,
the encrypted `.enc` path, and the keyring account name are all derived from
`resolveAuthProfileBaseName(profile)` (default profile → environment-based
name; named profile → the profile name), so each auth profile gets its own
files and its own encryption key.
- `interface.ts``CredentialStore` interface (extends `AuthConfigStorage` with `kind` and `describe()`)
- `file-store.ts``FileCredentialStore` (plaintext TOML, default)
- `encrypted-file-store.ts``EncryptedFileCredentialStore` + plaintext-TOML migration
- `crypto.ts` — AES-256-GCM `encryptString` / `decryptString` helpers
- `resolver.ts``createCredentialStorageContext({...})` factory; returns `{ storageFactory, getActiveStore }` (both take an optional `profile`) for the consumer to plug into `createOAuthFlow` (as `ctx.storageFactory`) and `whoami`-style reporting respectively
- `state.ts` — module-level per-session resolver flags (one-time warnings, the Windows install-failed latch)
- `key-providers/` — per-platform OS-keyring backends that store only the 32-byte encryption key (never the credential blob itself, so the macOS Keychain 2.5 KB item limit is never a concern):
- `interface.ts``KeyProvider` interface
- `mac-security.ts``/usr/bin/security` shell-out
- `linux-secret-tool.ts``secret-tool` shell-out (probes `libsecret-tools`)
- `napi-keyring.ts``@napi-rs/keyring` wincred binding on Windows
- `lazy-installer.ts` — Windows-only `npm install @napi-rs/keyring` on first opt-in
- `factory.ts``resolveKeyProvider(serviceName)` picks the right per-platform implementation
- `shared.ts` — account-name derivation + keyring JSON envelope encoding
## DI SURFACE
`createOAuthFlow(ctx)` accepts a context object:
- `logger` — drop-in replacement for wrangler's logger singleton
- `isNonInteractiveOrCI()` — whether to suppress interactive prompts
- `openInBrowser(url)` — opens the browser to the OAuth authorize URL
- `hasEnvCredentials()` — short-circuits refresh logic when env-based auth is set
- `clientId` (required) — the consumer's registered OAuth app ID; `string` or
`() => string` for lazy (e.g. env-driven prod/staging) resolution
- `consent` (required) — the consumer's branded granted/denied consent pages
- `redirectUri` (required) — the registered redirect URI / local callback URL.
The callback server's listen host/port and route path are all derived from it
(per-call bind overrides via `LoginProps.callbackHost`/`callbackPort`)
- `storageFactory` (required) — maps an auth profile to the consumer's
`AuthConfigStorage` token-persistence backend. The flow calls it with the
active profile on every credential access. For keyring opt-in, pass the
`storageFactory` from `createCredentialStorageContext(...)` rather than a raw
file store, so both the active profile and the encrypted-file / plaintext
choice are re-resolved on every credential operation.
- `purgeOnLoginOrLogout?()` — invalidate consumer-side caches after login/logout
- `generateAuthUrl?` / `generateRandomState?` — test overrides for deterministic
snapshot tests (defaults pull from `./generate-auth-url` / `./generate-random-state`)
`clientId`, `consent`, `redirectUri`, and `storageFactory` are consumer-specific
(Wrangler's live in `packages/wrangler/src/user/`), so they are required rather
than defaulted here.
Wrangler wires the credential-storage layer once in
`packages/wrangler/src/user/user.ts` via `createCredentialStorageContext(...)`
and exposes the resulting `getActiveStore` (called with the active profile) as
`getCredentialStore()` for `whoami`-style code that wants to surface the active
storage location.
## CONVENTIONS
- License: dual MIT/Apache-2.0. Files derived from
[BitySA/oauth2-auth-code-pkce](https://github.com/BitySA/oauth2-auth-code-pkce)
carry the Apache-2.0 header.
- No `console.*` — use the injected `ctx.logger`.
- No global `fetch` — use undici's `fetch`.
- `UserError` instances must carry stable `telemetryMessage` labels
(`<area> <sub-area> <failure>`, e.g. `user oauth invalid scope`).
These labels are part of the telemetry contract — preserve them verbatim.
- No direct Cloudflare REST API calls. This package talks to OAuth endpoints
(`/oauth2/auth`, `/oauth2/token`, `/oauth2/revoke`) only.
- OAuth callback server listens on the host/port derived from the consumer's
required `ctx.redirectUri`; override the bind address per-call via
`LoginProps.callbackHost` / `callbackPort`.
## BUILD
- tsup: two entry points — `src/index.ts` and `src/test-helpers/index.ts`
- ESM-only output to `dist/`
- `@cloudflare/*`, `undici`, `msw`, and `vitest` are kept external
## CREDENTIAL STORAGE NOTES
- The encrypted file uses `AES-256-GCM` via `node:crypto` (no third-party
crypto deps). The 12-byte IV is generated fresh per write; the 16-byte
GCM auth tag is verified on every read.
- The keyring entry holds only a 32-byte AES key wrapped in a small JSON
envelope (`{v, key, created}`). It's well under the macOS Keychain ~2.5 KB
per-item limit no matter how the credential schema grows.
- `@napi-rs/keyring` (the Windows backend's native binding) is installed
lazily on first opt-in via `npm install` into `<configPath>/native/keyring/`,
where `configPath` is the consumer-provided global config directory (see
`getConfigPath` below) — so each CLI's binding lives under its own config dir.
Pinned to `PINNED_KEYRING_VERSION` so CI users running
`npm install -g @napi-rs/keyring` by hand get the same version as the
lazy-install path.
- The credential files (`.toml` / `.enc`) and the keyring install dir are all
rooted at the consumer's config directory. `@cloudflare/workers-auth` never
resolves that path itself (wrangler and a future `cf` CLI use different global
config paths); instead the consumer passes `getConfigPath: () => string` into
`createCredentialStorageContext`, and the path helpers
(`getAuthConfigFilePath` / `getEncryptedAuthConfigFilePath` /
`getKeyringInstallDir`) and store constructors all take it explicitly.
- The consumer's `createCredentialStorageContext` call captures `serviceName`,
`getConfigPath`, `isKeyringEnabled`, `logger`, `isNonInteractiveOrCI`, and
`cliName` in a closure. The returned `storageFactory(profile)` re-resolves
the active store on every call so the active profile, `--use-keyring` /
`--no-use-keyring`, and the `CLOUDFLARE_AUTH_USE_KEYRING` env var all take
effect without rebuilding the OAuth flow. Per-session memoization flags
(`hasWarnedAboutKeyringFallback`, `installFailedThisSession`, ...) still
live at module scope in `state.ts`; tests use
`resetCredentialStorageState` to clear them between cases and
`setKeyProviderFactoryForTesting` to swap in stubs.