@cloudflare/workers-auth 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,478 @@
1
+ import { ComplianceConfig, ApiCredentials } from '@cloudflare/workers-utils';
2
+
3
+ /**
4
+ * Pluggable persistence for a typed config blob.
5
+ *
6
+ */
7
+ interface ConfigStorage<T> {
8
+ /**
9
+ * Read and parse the stored config.
10
+ *
11
+ * Consumers can treat a `read()` that resolves with `undefined` as
12
+ * "not logged in / no temporary account / etc.", and leave error handling
13
+ * for the surfaces that benefit from it.
14
+ *
15
+ * @returns `undefined` for the *empty* state — i.e. when the
16
+ * backing store does not exist yet, or when it exists but no longer
17
+ * contains usable data (e.g. encrypted-file present but the keyring entry
18
+ * is missing, ciphertext fails its auth tag, deserialised payload is not
19
+ * shaped like `T`). All of these collapse to "nothing usable stored
20
+ * here" from the consumer's perspective, so the default is to return
21
+ * `undefined` rather than throw.
22
+ *
23
+ * @throws *genuine* errors that the consumer needs to act on — typically
24
+ * filesystem or permission failures (`EACCES`, `EISDIR`, disk full).
25
+ * Those propagate so the user can see what's wrong, rather than silently
26
+ * behaving as "not logged in".
27
+ */
28
+ read(): T | undefined;
29
+ /** Serialize and persist the config. */
30
+ write(config: T): void;
31
+ /** Remove the backing store; returns whether anything existed beforehand. */
32
+ clear(): boolean;
33
+ /** Human-readable location of the backing store, for display and warnings. */
34
+ path(): string;
35
+ }
36
+
37
+ /**
38
+ * The data that may be read from the on-disk user auth config file.
39
+ */
40
+ interface UserAuthConfig {
41
+ oauth_token?: string;
42
+ refresh_token?: string;
43
+ expiration_time?: string;
44
+ scopes?: string[];
45
+ /** @deprecated - this field was only provided by the deprecated v1 `wrangler config` command. */
46
+ api_token?: string;
47
+ }
48
+ type AuthConfigStorage = ConfigStorage<UserAuthConfig>;
49
+
50
+ /**
51
+ * A short-lived "temporary preview account"
52
+ */
53
+ type TemporaryPreviewAccount = {
54
+ account: {
55
+ id: string;
56
+ name: string;
57
+ apiToken: string;
58
+ expiresAt: string;
59
+ };
60
+ claim: {
61
+ url: string;
62
+ expiresAt: string;
63
+ };
64
+ };
65
+ type TemporaryAccountStorage = ConfigStorage<TemporaryPreviewAccount>;
66
+
67
+ interface GenerateAuthUrlProps {
68
+ authUrl: string;
69
+ clientId: string;
70
+ scopes: string[];
71
+ stateQueryParam: string;
72
+ codeChallenge: string;
73
+ redirectUri: string;
74
+ }
75
+ /**
76
+ * Build the OAuth 2.0 authorize URL for the Cloudflare auth endpoint.
77
+ *
78
+ * Extracted from the rest of the OAuth flow so consumers (or tests) can
79
+ * substitute a deterministic implementation when a stable URL is needed
80
+ * (e.g. for snapshot testing).
81
+ */
82
+ declare const generateAuthUrl: ({ authUrl, clientId, scopes, stateQueryParam, codeChallenge, redirectUri, }: GenerateAuthUrlProps) => string;
83
+
84
+ /**
85
+ * Generates random state to be passed for anti-csrf.
86
+ *
87
+ * Extracted from the rest of the OAuth flow so consumers (or tests) can
88
+ * substitute a deterministic implementation when a stable state value is
89
+ * needed (e.g. for snapshot testing).
90
+ */
91
+ declare function generateRandomState(lengthOfState: number): string;
92
+
93
+ /**
94
+ * The dependencies the OAuth flow needs to mint/reuse a short-lived "temporary
95
+ * preview account"
96
+ */
97
+ interface OAuthFlowTemporaryContext {
98
+ /** Persistence backend for the cached temporary preview account. */
99
+ storage: TemporaryAccountStorage;
100
+ /**
101
+ * Hook to customise the terms-acceptance interactive prompt
102
+ * - question: the question to ask a user in interactive mode.
103
+ * return answer === "yes" (must be the literal string)
104
+ * - notice: the notice to print on stderr if in non-interactive mode
105
+ * always return true
106
+ */
107
+ prompt: (question: string, notice: string) => Promise<boolean>;
108
+ }
109
+ /**
110
+ * The branded OAuth consent pages the provider redirects the browser to after
111
+ * the user grants or denies consent.
112
+ */
113
+ interface OAuthConsentPages {
114
+ /** Redirect target shown after the user grants consent. */
115
+ granted: {
116
+ url: string;
117
+ };
118
+ /** Redirect target shown after the user denies consent, plus the error
119
+ * surfaced to the terminal. */
120
+ denied: {
121
+ url: string;
122
+ error: string;
123
+ };
124
+ }
125
+ /**
126
+ * Subset of the wrangler `logger` singleton used by the OAuth flow.
127
+ * Consumers pass in an implementation that maps to their own logging surface.
128
+ */
129
+ interface OAuthFlowLogger {
130
+ debug(...args: unknown[]): void;
131
+ info(...args: unknown[]): void;
132
+ log(...args: unknown[]): void;
133
+ warn(...args: unknown[]): void;
134
+ error(...args: unknown[]): void;
135
+ }
136
+ /**
137
+ * Dependency-injection surface for {@link createOAuthFlow}.
138
+ *
139
+ * The OAuth flow only talks to OAuth endpoints (`/oauth2/auth`, `/oauth2/token`,
140
+ * `/oauth2/revoke`) using `undici`'s `fetch` directly — there is no Cloudflare
141
+ * API client wired into this context.
142
+ */
143
+ interface OAuthFlowContext {
144
+ logger: OAuthFlowLogger;
145
+ /**
146
+ * Whether the process should not prompt the user. The OAuth flow uses this to
147
+ * decide whether to short-circuit interactive login attempts.
148
+ */
149
+ isNonInteractiveOrCI: () => boolean;
150
+ /**
151
+ * Open the given URL in the user's default browser. Called during the
152
+ * interactive OAuth login flow with the authorize URL.
153
+ */
154
+ openInBrowser: (url: string) => Promise<void>;
155
+ /**
156
+ * Whether environment-based credentials are present. When `true`, the OAuth
157
+ * flow short-circuits because the env credentials take priority over stored
158
+ * OAuth tokens:
159
+ * - `login` refuses to start
160
+ * - `logout` is a no-op (it cannot revoke env credentials)
161
+ * - the refresh check returns `false` so an expired stored OAuth token does
162
+ * not trigger a needless refresh attempt
163
+ */
164
+ hasEnvCredentials: () => boolean;
165
+ /**
166
+ * Called after a successful `login` or `logout` so the consumer can invalidate
167
+ * any caches that depend on the active token (e.g. wrangler's selected-account
168
+ * cache).
169
+ */
170
+ purgeOnLoginOrLogout?: () => void;
171
+ /**
172
+ * The OAuth client ID identifying the consuming CLI to the Cloudflare OAuth
173
+ * server. Consumer-specific (each CLI registers its own OAuth app), so it is
174
+ * required. Pass a function to resolve it lazily — e.g. so an env-var read at
175
+ * call time can switch between production and staging apps.
176
+ */
177
+ clientId: string | (() => string);
178
+ /**
179
+ * The branded consent pages the provider redirects to after the user grants
180
+ * or denies consent.
181
+ */
182
+ consent: OAuthConsentPages;
183
+ /**
184
+ * The `redirect_uri` registered on the consumer's OAuth app
185
+ */
186
+ redirectUri: string;
187
+ /**
188
+ * Factory that returns a persistence backend for the given auth profile.
189
+ *
190
+ * Called with the active profile name (e.g. `"default"`) on every storage
191
+ * access so the flow always reads/writes the correct backing store.
192
+ * The consumer is responsible for mapping profile names to concrete storage
193
+ * backends (e.g. wrangler maps each profile to a separate TOML file under
194
+ * the global config directory).
195
+ *
196
+ * Consumers that want OS-keyring-backed encryption pass the
197
+ * `storageFactory` from {@link createCredentialStorageContext} (in the
198
+ * `credential-store` module) — for each profile that adapter resolves
199
+ * between the plaintext TOML file and the encrypted-file-with-keyring-key
200
+ * implementation on every call, so runtime preference changes
201
+ * (`--use-keyring` / `CLOUDFLARE_AUTH_USE_KEYRING`) take effect without
202
+ * rebuilding the flow.
203
+ */
204
+ storageFactory: (profile?: string) => AuthConfigStorage;
205
+ /**
206
+ * Whether the flow's credential resolvers (`getAPIToken` / `requireApiToken`)
207
+ * should honour the global API key + email pair in addition to scoped API
208
+ * tokens.
209
+ */
210
+ allowGlobalAuthKey: boolean;
211
+ /**
212
+ * Dependencies for minting/reusing a temporary preview account.
213
+ */
214
+ temporary: OAuthFlowTemporaryContext | undefined;
215
+ /**
216
+ * Override the OAuth authorize URL generator. Used by tests to produce a
217
+ * deterministic URL for snapshot testing. Defaults to the standard
218
+ * implementation.
219
+ */
220
+ generateAuthUrl?: typeof generateAuthUrl;
221
+ /**
222
+ * Override the random state generator. Used by tests to produce a
223
+ * deterministic state value for snapshot testing. Defaults to the standard
224
+ * implementation.
225
+ */
226
+ generateRandomState?: typeof generateRandomState;
227
+ }
228
+
229
+ /**
230
+ * Reason why {@link OAuthFlowAPI.loginOrRefreshIfRequired} could not
231
+ * authenticate the user.
232
+ */
233
+ type LoginOrRefreshFailureReason =
234
+ /** no stored credentials and the environment is non-interactive (CI, piped stdin, etc.) so a browser login cannot be started. */
235
+ "no-credentials-non-interactive"
236
+ /** stored credentials and the interactive login attempt was unsuccessful (user cancelled, etc.). */
237
+ | "no-credentials-login-failed"
238
+ /** the stored token has expired, refresh failed, and the environment is non-interactive so a browser login cannot be started. */
239
+ | "token-expired-non-interactive"
240
+ /** the stored token has expired, refresh failed, and the interactive login attempt was unsuccessful. */
241
+ | "token-expired-login-failed";
242
+ /**
243
+ * Discriminated union returned by {@link OAuthFlowAPI.loginOrRefreshIfRequired}.
244
+ *
245
+ * When `loggedIn` is `true` the caller can proceed. When `false`, `reason`
246
+ * describes why authentication failed so the caller can surface a
247
+ * targeted error message.
248
+ */
249
+ type LoginOrRefreshResult = {
250
+ loggedIn: true;
251
+ } | {
252
+ loggedIn: false;
253
+ reason: LoginOrRefreshFailureReason;
254
+ };
255
+ /**
256
+ * Options for an interactive OAuth login.
257
+ */
258
+ interface LoginProps {
259
+ complianceConfig: ComplianceConfig;
260
+ /**
261
+ * The OAuth scopes to request. The catalog of valid scope keys is
262
+ * consumer-defined; this package treats scopes as opaque strings.
263
+ */
264
+ scopes: string[];
265
+ /**
266
+ * Whether to open the authorize URL in a browser. Defaults to `true`.
267
+ * When `false` the URL is printed for the user to copy/paste.
268
+ */
269
+ browser?: boolean;
270
+ /** Host the local callback server listens on. Defaults to `localhost`. */
271
+ callbackHost?: string;
272
+ /** Port the local callback server listens on. Defaults to `8976`. */
273
+ callbackPort?: number;
274
+ /**
275
+ * Named auth profile to store the token under. When omitted, the default
276
+ * profile (`default.toml`) is used.
277
+ * Only for use by 'auth create', not exposed to the user
278
+ */
279
+ profile?: string;
280
+ }
281
+ /**
282
+ * Public surface returned by {@link createOAuthFlow}.
283
+ */
284
+ interface OAuthFlowAPI {
285
+ /**
286
+ * Set the active auth profile for all subsequent storage lookups.
287
+ *
288
+ * Called once at top of command dispatch by the consumer after resolving the
289
+ * active profile. `"default"` if never called.
290
+ */
291
+ setProfile(profile: string): void;
292
+ getActiveProfile(): string;
293
+ /**
294
+ * Open the authorize URL in the user's browser, wait for the callback to be
295
+ * hit on the local HTTP server, exchange the code for an access token, and
296
+ * persist the result to disk.
297
+ *
298
+ * Refuses to start when `ctx.hasEnvCredentials()` returns `true`.
299
+ * Refuses to start when the compliance region is `fedramp_high`.
300
+ *
301
+ * When `props.profile` is set, the token is stored under that profile
302
+ * instead of the active one. This is used by `auth create <name>`.
303
+ *
304
+ * @returns `true` on success, `false` when env credentials are present.
305
+ */
306
+ login(props: LoginProps): Promise<boolean>;
307
+ /**
308
+ * Revoke the stored refresh token at the Cloudflare OAuth endpoint and
309
+ * delete the on-disk auth config file.
310
+ *
311
+ * No-op when `ctx.hasEnvCredentials()` returns `true` (env credentials
312
+ * cannot be revoked).
313
+ *
314
+ * When `profile` is passed, operates on that profile instead of the
315
+ * active one. This is used by `auth delete <name>`.
316
+ */
317
+ logout(profile?: string): Promise<void>;
318
+ /**
319
+ * If the user has no stored OAuth token, attempt an interactive login.
320
+ * If they have one but it is expired, attempt a refresh; if refresh fails,
321
+ * fall back to an interactive login.
322
+ *
323
+ * Scopes are required in case an interactive login is triggered — the
324
+ * consumer's scope catalog lives outside this package.
325
+ *
326
+ * @returns `{ loggedIn: true }` when the user is authenticated (or env
327
+ * credentials are present). When authentication fails, returns
328
+ * `{ loggedIn: false, reason }` describing why — see
329
+ * {@link LoginOrRefreshFailureReason}.
330
+ */
331
+ loginOrRefreshIfRequired(props: LoginProps): Promise<LoginOrRefreshResult>;
332
+ /**
333
+ * Read the OAuth access token from local state, refreshing it first if
334
+ * needed. Returns `undefined` when there is no stored OAuth token or the
335
+ * refresh fails.
336
+ *
337
+ * This intentionally does NOT consult env credentials — callers that want
338
+ * env-or-OAuth resolution should check env first themselves.
339
+ */
340
+ getOAuthTokenFromLocalState(): Promise<string | undefined>;
341
+ /**
342
+ * Resolve API credentials, preferring an active temporary preview account
343
+ * (when one has been latched via {@link activateTemporaryAccount}) over the
344
+ * env / stored-OAuth resolution performed by the shared credential resolver.
345
+ *
346
+ * Returns `undefined` when no credentials are available.
347
+ */
348
+ getAPIToken(): ApiCredentials | undefined;
349
+ /**
350
+ * Like {@link getAPIToken}, but throws a `UserError` when no credentials are
351
+ * available.
352
+ */
353
+ requireApiToken(): ApiCredentials;
354
+ /**
355
+ * Return the scopes granted to the stored OAuth token for the active
356
+ * profile, or `undefined` when no OAuth token is stored.
357
+ */
358
+ getScopes(): string[] | undefined;
359
+ /**
360
+ * Establish whether `--temporary` is permitted for this invocation. Called
361
+ * once at command dispatch by the consumer. Also drops any temporary account
362
+ * latched by a previous dispatch, so that — when multiple commands share a
363
+ * process (e.g. in tests) — each invocation starts a fresh temporary session.
364
+ * No-op when the flow was created without a `temporary` context.
365
+ */
366
+ setTemporaryAllowed(allowed: boolean): void;
367
+ /**
368
+ * Whether `--temporary` is permitted for this invocation (see
369
+ * {@link setTemporaryAllowed}). Always `false` without a `temporary` context.
370
+ */
371
+ isTemporaryAllowed(): boolean;
372
+ /**
373
+ * The temporary preview account latched for this invocation, or `undefined`.
374
+ * Only set after {@link activateTemporaryAccount} has run.
375
+ */
376
+ getActiveTemporaryAccount(): TemporaryPreviewAccount | undefined;
377
+ /**
378
+ * The sole creator of the temporary-account latch: mint a fresh temporary
379
+ * preview account (or reuse a cached one), latch it for this invocation, and
380
+ * return it. Requires a `temporary` context.
381
+ */
382
+ activateTemporaryAccount(): Promise<{
383
+ account: TemporaryPreviewAccount;
384
+ cached: boolean;
385
+ }>;
386
+ }
387
+ /**
388
+ * Build an instance of the OAuth flow bound to the given context.
389
+ *
390
+ * The returned object owns module-private state (the transient OAuth flow
391
+ * state and the deprecated-v1 warning latch). In practice consumers create
392
+ * exactly one instance per process.
393
+ */
394
+ declare function createOAuthFlow(ctx: OAuthFlowContext): OAuthFlowAPI;
395
+
396
+ interface ProfileConfigOperations {
397
+ exists(profile: string): boolean;
398
+ list(): string[];
399
+ delete(profile: string): void;
400
+ }
401
+ interface DirectoryBindingsStorage {
402
+ read(): Record<string, string>;
403
+ write(bindings: Record<string, string>): void;
404
+ }
405
+ interface DirectoryBindingOperations extends DirectoryBindingsStorage {
406
+ activate(profile: string, dir: string): void;
407
+ deactivate(dir: string): DeactivateDirectoryResult;
408
+ getProfileForDirectory(startDir: string): string | undefined;
409
+ getBindingsForProfile(profile: string): string[];
410
+ removeAllBindingsForProfile(profile: string): string[];
411
+ }
412
+ interface ProfileStore {
413
+ configs: ProfileConfigOperations;
414
+ bindings: DirectoryBindingOperations;
415
+ resolve(args: {
416
+ profile?: string;
417
+ cwd: string;
418
+ }): string;
419
+ }
420
+ interface DeactivateDirectoryResult {
421
+ removedProfile: string;
422
+ newResolution: {
423
+ profile: string | undefined;
424
+ source: string;
425
+ };
426
+ }
427
+ declare function createProfileStore(args: {
428
+ configs: ProfileConfigOperations;
429
+ bindings: DirectoryBindingsStorage;
430
+ }): ProfileStore;
431
+ declare function validateProfileName(name: string): void;
432
+
433
+ /**
434
+ * Pluggable backend for the persisted OAuth credentials.
435
+ *
436
+ * Concrete implementations:
437
+ * - {@link FileCredentialStore} — the plaintext TOML file at
438
+ * `<globalWranglerConfigPath>/config/<env>.toml`. Used by default and as
439
+ * a fallback when keyring storage is unavailable.
440
+ * - {@link EncryptedFileCredentialStore} — an AES-256-GCM-encrypted file at
441
+ * `<globalWranglerConfigPath>/config/<env>.enc`, with the encryption key
442
+ * stored in the OS keyring via a {@link KeyProvider}.
443
+ *
444
+ * Extends the consumer-facing {@link AuthConfigStorage} contract with two
445
+ * keyring-specific extras:
446
+ * - `kind`: discriminant so consumers can tell apart "plain file" and
447
+ * "encrypted file" stores without sniffing for methods.
448
+ * - `describe()`: human-readable, possibly multi-line description (e.g.
449
+ * `"Encrypted file (path) with key in macOS Keychain"`) for surfaces
450
+ * like `wrangler whoami` that want richer copy than the raw `path()`.
451
+ *
452
+ * The interface is synchronous so it can be plugged into existing
453
+ * `AuthConfigStorage` call sites without forcing every caller to become
454
+ * async. Under the hood, both implementations use synchronous primitives
455
+ * (subprocess `spawnSync`, `@napi-rs/keyring`'s sync `Entry` class,
456
+ * `node:crypto` sync APIs, synchronous filesystem calls).
457
+ */
458
+ interface CredentialStore extends AuthConfigStorage {
459
+ readonly kind: "file" | "encrypted-file";
460
+ /**
461
+ * Human-readable description of where credentials are stored, suitable
462
+ * for consumers' `whoami`-style output. May be multi-line / richer
463
+ * than the raw `path()`.
464
+ */
465
+ describe(): string;
466
+ }
467
+
468
+ /**
469
+ * On-disk serialization format for a CLI's config/credential files.
470
+ *
471
+ * The whole auth layer is CLI-agnostic; each CLI (wrangler, cf, …) picks the
472
+ * format its files are written in. Wrangler uses `"toml"` for historical
473
+ * compatibility; newer CLIs (cf) use `"json"`. The format value doubles as the
474
+ * file extension (no leading dot).
475
+ */
476
+ type FileFormat = "toml" | "json";
477
+
478
+ export { type AuthConfigStorage as A, type CredentialStore as C, type DeactivateDirectoryResult as D, type FileFormat as F, type LoginOrRefreshFailureReason as L, type OAuthFlowLogger as O, type ProfileConfigOperations as P, type TemporaryPreviewAccount as T, type UserAuthConfig as U, type ConfigStorage as a, type OAuthFlowContext as b, createOAuthFlow as c, type LoginOrRefreshResult as d, type LoginProps as e, generateRandomState as f, generateAuthUrl as g, createProfileStore as h, type DirectoryBindingOperations as i, type DirectoryBindingsStorage as j, type ProfileStore as k, type OAuthConsentPages as l, validateProfileName as v };