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

9.1 KiB

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.tsErrorOAuth2 class hierarchy + toErrorClass mapper
  • src/generate-auth-url.ts — authorize URL builder
  • src/generate-random-state.ts — CSRF state generator
  • src/env-vars.tsWRANGLER_* 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.tsTemporaryAccountStorage / 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.tsreadStoredAuthState() + 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.tscreateOAuthFlow(ctx) factory wiring everything together
  • src/context.tsOAuthFlowContext 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.tsCredentialStore interface (extends AuthConfigStorage with kind and describe())
  • file-store.tsFileCredentialStore (plaintext TOML, default)
  • encrypted-file-store.tsEncryptedFileCredentialStore + plaintext-TOML migration
  • crypto.ts — AES-256-GCM encryptString / decryptString helpers
  • resolver.tscreateCredentialStorageContext({...}) 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.tsKeyProvider interface
    • mac-security.ts/usr/bin/security shell-out
    • linux-secret-tool.tssecret-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.tsresolveKeyProvider(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 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.