@cloudflare/workers-auth 0.4.2 → 0.5.1

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,472 @@
1
+ import { Logger, 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
+ type OAuthFlowLogger = Logger;
130
+ /**
131
+ * Dependency-injection surface for {@link createOAuthFlow}.
132
+ *
133
+ * The OAuth flow only talks to OAuth endpoints (`/oauth2/auth`, `/oauth2/token`,
134
+ * `/oauth2/revoke`) using `undici`'s `fetch` directly — there is no Cloudflare
135
+ * API client wired into this context.
136
+ */
137
+ interface OAuthFlowContext {
138
+ logger: OAuthFlowLogger;
139
+ /**
140
+ * Whether the process should not prompt the user. The OAuth flow uses this to
141
+ * decide whether to short-circuit interactive login attempts.
142
+ */
143
+ isNonInteractiveOrCI: () => boolean;
144
+ /**
145
+ * Open the given URL in the user's default browser. Called during the
146
+ * interactive OAuth login flow with the authorize URL.
147
+ */
148
+ openInBrowser: (url: string) => Promise<void>;
149
+ /**
150
+ * Whether environment-based credentials are present. When `true`, the OAuth
151
+ * flow short-circuits because the env credentials take priority over stored
152
+ * OAuth tokens:
153
+ * - `login` refuses to start
154
+ * - `logout` is a no-op (it cannot revoke env credentials)
155
+ * - the refresh check returns `false` so an expired stored OAuth token does
156
+ * not trigger a needless refresh attempt
157
+ */
158
+ hasEnvCredentials: () => boolean;
159
+ /**
160
+ * Called after a successful `login` or `logout` so the consumer can invalidate
161
+ * any caches that depend on the active token (e.g. wrangler's selected-account
162
+ * cache).
163
+ */
164
+ purgeOnLoginOrLogout?: () => void;
165
+ /**
166
+ * The OAuth client ID identifying the consuming CLI to the Cloudflare OAuth
167
+ * server. Consumer-specific (each CLI registers its own OAuth app), so it is
168
+ * required. Pass a function to resolve it lazily — e.g. so an env-var read at
169
+ * call time can switch between production and staging apps.
170
+ */
171
+ clientId: string | (() => string);
172
+ /**
173
+ * The branded consent pages the provider redirects to after the user grants
174
+ * or denies consent.
175
+ */
176
+ consent: OAuthConsentPages;
177
+ /**
178
+ * The `redirect_uri` registered on the consumer's OAuth app
179
+ */
180
+ redirectUri: string;
181
+ /**
182
+ * Factory that returns a persistence backend for the given auth profile.
183
+ *
184
+ * Called with the active profile name (e.g. `"default"`) on every storage
185
+ * access so the flow always reads/writes the correct backing store.
186
+ * The consumer is responsible for mapping profile names to concrete storage
187
+ * backends (e.g. wrangler maps each profile to a separate TOML file under
188
+ * the global config directory).
189
+ *
190
+ * Consumers that want OS-keyring-backed encryption pass the
191
+ * `storageFactory` from {@link createCredentialStorageContext} (in the
192
+ * `credential-store` module) — for each profile that adapter resolves
193
+ * between the plaintext TOML file and the encrypted-file-with-keyring-key
194
+ * implementation on every call, so runtime preference changes
195
+ * (`--use-keyring` / `CLOUDFLARE_AUTH_USE_KEYRING`) take effect without
196
+ * rebuilding the flow.
197
+ */
198
+ storageFactory: (profile?: string) => AuthConfigStorage;
199
+ /**
200
+ * Whether the flow's credential resolvers (`getAPIToken` / `requireApiToken`)
201
+ * should honour the global API key + email pair in addition to scoped API
202
+ * tokens.
203
+ */
204
+ allowGlobalAuthKey: boolean;
205
+ /**
206
+ * Dependencies for minting/reusing a temporary preview account.
207
+ */
208
+ temporary: OAuthFlowTemporaryContext | undefined;
209
+ /**
210
+ * Override the OAuth authorize URL generator. Used by tests to produce a
211
+ * deterministic URL for snapshot testing. Defaults to the standard
212
+ * implementation.
213
+ */
214
+ generateAuthUrl?: typeof generateAuthUrl;
215
+ /**
216
+ * Override the random state generator. Used by tests to produce a
217
+ * deterministic state value for snapshot testing. Defaults to the standard
218
+ * implementation.
219
+ */
220
+ generateRandomState?: typeof generateRandomState;
221
+ }
222
+
223
+ /**
224
+ * Reason why {@link OAuthFlowAPI.loginOrRefreshIfRequired} could not
225
+ * authenticate the user.
226
+ */
227
+ type LoginOrRefreshFailureReason =
228
+ /** no stored credentials and the environment is non-interactive (CI, piped stdin, etc.) so a browser login cannot be started. */
229
+ "no-credentials-non-interactive"
230
+ /** stored credentials and the interactive login attempt was unsuccessful (user cancelled, etc.). */
231
+ | "no-credentials-login-failed"
232
+ /** the stored token has expired, refresh failed, and the environment is non-interactive so a browser login cannot be started. */
233
+ | "token-expired-non-interactive"
234
+ /** the stored token has expired, refresh failed, and the interactive login attempt was unsuccessful. */
235
+ | "token-expired-login-failed";
236
+ /**
237
+ * Discriminated union returned by {@link OAuthFlowAPI.loginOrRefreshIfRequired}.
238
+ *
239
+ * When `loggedIn` is `true` the caller can proceed. When `false`, `reason`
240
+ * describes why authentication failed so the caller can surface a
241
+ * targeted error message.
242
+ */
243
+ type LoginOrRefreshResult = {
244
+ loggedIn: true;
245
+ } | {
246
+ loggedIn: false;
247
+ reason: LoginOrRefreshFailureReason;
248
+ };
249
+ /**
250
+ * Options for an interactive OAuth login.
251
+ */
252
+ interface LoginProps {
253
+ complianceConfig: ComplianceConfig;
254
+ /**
255
+ * The OAuth scopes to request. The catalog of valid scope keys is
256
+ * consumer-defined; this package treats scopes as opaque strings.
257
+ */
258
+ scopes: string[];
259
+ /**
260
+ * Whether to open the authorize URL in a browser. Defaults to `true`.
261
+ * When `false` the URL is printed for the user to copy/paste.
262
+ */
263
+ browser?: boolean;
264
+ /** Host the local callback server listens on. Defaults to `localhost`. */
265
+ callbackHost?: string;
266
+ /** Port the local callback server listens on. Defaults to `8976`. */
267
+ callbackPort?: number;
268
+ /**
269
+ * Named auth profile to store the token under. When omitted, the default
270
+ * profile (`default.toml`) is used.
271
+ * Only for use by 'auth create', not exposed to the user
272
+ */
273
+ profile?: string;
274
+ }
275
+ /**
276
+ * Public surface returned by {@link createOAuthFlow}.
277
+ */
278
+ interface OAuthFlowAPI {
279
+ /**
280
+ * Set the active auth profile for all subsequent storage lookups.
281
+ *
282
+ * Called once at top of command dispatch by the consumer after resolving the
283
+ * active profile. `"default"` if never called.
284
+ */
285
+ setProfile(profile: string): void;
286
+ getActiveProfile(): string;
287
+ /**
288
+ * Open the authorize URL in the user's browser, wait for the callback to be
289
+ * hit on the local HTTP server, exchange the code for an access token, and
290
+ * persist the result to disk.
291
+ *
292
+ * Refuses to start when `ctx.hasEnvCredentials()` returns `true`.
293
+ * Refuses to start when the compliance region is `fedramp_high`.
294
+ *
295
+ * When `props.profile` is set, the token is stored under that profile
296
+ * instead of the active one. This is used by `auth create <name>`.
297
+ *
298
+ * @returns `true` on success, `false` when env credentials are present.
299
+ */
300
+ login(props: LoginProps): Promise<boolean>;
301
+ /**
302
+ * Revoke the stored refresh token at the Cloudflare OAuth endpoint and
303
+ * delete the on-disk auth config file.
304
+ *
305
+ * No-op when `ctx.hasEnvCredentials()` returns `true` (env credentials
306
+ * cannot be revoked).
307
+ *
308
+ * When `profile` is passed, operates on that profile instead of the
309
+ * active one. This is used by `auth delete <name>`.
310
+ */
311
+ logout(profile?: string): Promise<void>;
312
+ /**
313
+ * If the user has no stored OAuth token, attempt an interactive login.
314
+ * If they have one but it is expired, attempt a refresh; if refresh fails,
315
+ * fall back to an interactive login.
316
+ *
317
+ * Scopes are required in case an interactive login is triggered — the
318
+ * consumer's scope catalog lives outside this package.
319
+ *
320
+ * @returns `{ loggedIn: true }` when the user is authenticated (or env
321
+ * credentials are present). When authentication fails, returns
322
+ * `{ loggedIn: false, reason }` describing why — see
323
+ * {@link LoginOrRefreshFailureReason}.
324
+ */
325
+ loginOrRefreshIfRequired(props: LoginProps): Promise<LoginOrRefreshResult>;
326
+ /**
327
+ * Read the OAuth access token from local state, refreshing it first if
328
+ * needed. Returns `undefined` when there is no stored OAuth token or the
329
+ * refresh fails.
330
+ *
331
+ * This intentionally does NOT consult env credentials — callers that want
332
+ * env-or-OAuth resolution should check env first themselves.
333
+ */
334
+ getOAuthTokenFromLocalState(): Promise<string | undefined>;
335
+ /**
336
+ * Resolve API credentials, preferring an active temporary preview account
337
+ * (when one has been latched via {@link activateTemporaryAccount}) over the
338
+ * env / stored-OAuth resolution performed by the shared credential resolver.
339
+ *
340
+ * Returns `undefined` when no credentials are available.
341
+ */
342
+ getAPIToken(): ApiCredentials | undefined;
343
+ /**
344
+ * Like {@link getAPIToken}, but throws a `UserError` when no credentials are
345
+ * available.
346
+ */
347
+ requireApiToken(): ApiCredentials;
348
+ /**
349
+ * Return the scopes granted to the stored OAuth token for the active
350
+ * profile, or `undefined` when no OAuth token is stored.
351
+ */
352
+ getScopes(): string[] | undefined;
353
+ /**
354
+ * Establish whether `--temporary` is permitted for this invocation. Called
355
+ * once at command dispatch by the consumer. Also drops any temporary account
356
+ * latched by a previous dispatch, so that — when multiple commands share a
357
+ * process (e.g. in tests) — each invocation starts a fresh temporary session.
358
+ * No-op when the flow was created without a `temporary` context.
359
+ */
360
+ setTemporaryAllowed(allowed: boolean): void;
361
+ /**
362
+ * Whether `--temporary` is permitted for this invocation (see
363
+ * {@link setTemporaryAllowed}). Always `false` without a `temporary` context.
364
+ */
365
+ isTemporaryAllowed(): boolean;
366
+ /**
367
+ * The temporary preview account latched for this invocation, or `undefined`.
368
+ * Only set after {@link activateTemporaryAccount} has run.
369
+ */
370
+ getActiveTemporaryAccount(): TemporaryPreviewAccount | undefined;
371
+ /**
372
+ * The sole creator of the temporary-account latch: mint a fresh temporary
373
+ * preview account (or reuse a cached one), latch it for this invocation, and
374
+ * return it. Requires a `temporary` context.
375
+ */
376
+ activateTemporaryAccount(): Promise<{
377
+ account: TemporaryPreviewAccount;
378
+ cached: boolean;
379
+ }>;
380
+ }
381
+ /**
382
+ * Build an instance of the OAuth flow bound to the given context.
383
+ *
384
+ * The returned object owns module-private state (the transient OAuth flow
385
+ * state and the deprecated-v1 warning latch). In practice consumers create
386
+ * exactly one instance per process.
387
+ */
388
+ declare function createOAuthFlow(ctx: OAuthFlowContext): OAuthFlowAPI;
389
+
390
+ interface ProfileConfigOperations {
391
+ exists(profile: string): boolean;
392
+ list(): string[];
393
+ delete(profile: string): void;
394
+ }
395
+ interface DirectoryBindingsStorage {
396
+ read(): Record<string, string>;
397
+ write(bindings: Record<string, string>): void;
398
+ }
399
+ interface DirectoryBindingOperations extends DirectoryBindingsStorage {
400
+ activate(profile: string, dir: string): void;
401
+ deactivate(dir: string): DeactivateDirectoryResult;
402
+ getProfileForDirectory(startDir: string): string | undefined;
403
+ getBindingsForProfile(profile: string): string[];
404
+ removeAllBindingsForProfile(profile: string): string[];
405
+ }
406
+ interface ProfileStore {
407
+ configs: ProfileConfigOperations;
408
+ bindings: DirectoryBindingOperations;
409
+ resolve(args: {
410
+ profile?: string;
411
+ cwd: string;
412
+ }): string;
413
+ }
414
+ interface DeactivateDirectoryResult {
415
+ removedProfile: string;
416
+ newResolution: {
417
+ profile: string | undefined;
418
+ source: string;
419
+ };
420
+ }
421
+ declare function createProfileStore(args: {
422
+ configs: ProfileConfigOperations;
423
+ bindings: DirectoryBindingsStorage;
424
+ }): ProfileStore;
425
+ declare function validateProfileName(name: string): void;
426
+
427
+ /**
428
+ * Pluggable backend for the persisted OAuth credentials.
429
+ *
430
+ * Concrete implementations:
431
+ * - {@link FileCredentialStore} — the plaintext TOML file at
432
+ * `<globalWranglerConfigPath>/config/<env>.toml`. Used by default and as
433
+ * a fallback when keyring storage is unavailable.
434
+ * - {@link EncryptedFileCredentialStore} — an AES-256-GCM-encrypted file at
435
+ * `<globalWranglerConfigPath>/config/<env>.enc`, with the encryption key
436
+ * stored in the OS keyring via a {@link KeyProvider}.
437
+ *
438
+ * Extends the consumer-facing {@link AuthConfigStorage} contract with two
439
+ * keyring-specific extras:
440
+ * - `kind`: discriminant so consumers can tell apart "plain file" and
441
+ * "encrypted file" stores without sniffing for methods.
442
+ * - `describe()`: human-readable, possibly multi-line description (e.g.
443
+ * `"Encrypted file (path) with key in macOS Keychain"`) for surfaces
444
+ * like `wrangler whoami` that want richer copy than the raw `path()`.
445
+ *
446
+ * The interface is synchronous so it can be plugged into existing
447
+ * `AuthConfigStorage` call sites without forcing every caller to become
448
+ * async. Under the hood, both implementations use synchronous primitives
449
+ * (subprocess `spawnSync`, `@napi-rs/keyring`'s sync `Entry` class,
450
+ * `node:crypto` sync APIs, synchronous filesystem calls).
451
+ */
452
+ interface CredentialStore extends AuthConfigStorage {
453
+ readonly kind: "file" | "encrypted-file";
454
+ /**
455
+ * Human-readable description of where credentials are stored, suitable
456
+ * for consumers' `whoami`-style output. May be multi-line / richer
457
+ * than the raw `path()`.
458
+ */
459
+ describe(): string;
460
+ }
461
+
462
+ /**
463
+ * On-disk serialization format for a CLI's config/credential files.
464
+ *
465
+ * The whole auth layer is CLI-agnostic; each CLI (wrangler, cf, …) picks the
466
+ * format its files are written in. Wrangler uses `"toml"` for historical
467
+ * compatibility; newer CLIs (cf) use `"json"`. The format value doubles as the
468
+ * file extension (no leading dot).
469
+ */
470
+ type FileFormat = "toml" | "json";
471
+
472
+ 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 };