@cloudflare/workers-auth 0.3.1 → 0.4.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.
package/dist/index.d.mts CHANGED
@@ -1,14 +1,32 @@
1
1
  import { ApiCredentials, ComplianceConfig } from '@cloudflare/workers-utils';
2
+ import { SpawnSyncReturns } from 'node:child_process';
2
3
 
3
4
  /**
4
- * Pluggable persistence for a typed config blob
5
+ * Pluggable persistence for a typed config blob.
6
+ *
5
7
  */
6
8
  interface ConfigStorage<T> {
7
9
  /**
8
10
  * Read and parse the stored config.
9
- * @throws if the backing store is missing or cannot be parsed.
11
+ *
12
+ * Consumers can treat a `read()` that resolves with `undefined` as
13
+ * "not logged in / no temporary account / etc.", and leave error handling
14
+ * for the surfaces that benefit from it.
15
+ *
16
+ * @returns `undefined` for the *empty* state — i.e. when the
17
+ * backing store does not exist yet, or when it exists but no longer
18
+ * contains usable data (e.g. encrypted-file present but the keyring entry
19
+ * is missing, ciphertext fails its auth tag, deserialised payload is not
20
+ * shaped like `T`). All of these collapse to "nothing usable stored
21
+ * here" from the consumer's perspective, so the default is to return
22
+ * `undefined` rather than throw.
23
+ *
24
+ * @throws *genuine* errors that the consumer needs to act on — typically
25
+ * filesystem or permission failures (`EACCES`, `EISDIR`, disk full).
26
+ * Those propagate so the user can see what's wrong, rather than silently
27
+ * behaving as "not logged in".
10
28
  */
11
- read(): T;
29
+ read(): T | undefined;
12
30
  /** Serialize and persist the config. */
13
31
  write(config: T): void;
14
32
  /** Remove the backing store; returns whether anything existed beforehand. */
@@ -168,9 +186,23 @@ interface OAuthFlowContext {
168
186
  */
169
187
  redirectUri: string;
170
188
  /**
171
- * Persistence backend for the stored auth config.
189
+ * Factory that returns a persistence backend for the given auth profile.
190
+ *
191
+ * Called with the active profile name (e.g. `"default"`) on every storage
192
+ * access so the flow always reads/writes the correct backing store.
193
+ * The consumer is responsible for mapping profile names to concrete storage
194
+ * backends (e.g. wrangler maps each profile to a separate TOML file under
195
+ * the global config directory).
196
+ *
197
+ * Consumers that want OS-keyring-backed encryption pass the
198
+ * `storageFactory` from {@link createCredentialStorageContext} (in the
199
+ * `credential-store` module) — for each profile that adapter resolves
200
+ * between the plaintext TOML file and the encrypted-file-with-keyring-key
201
+ * implementation on every call, so runtime preference changes
202
+ * (`--use-keyring` / `CLOUDFLARE_AUTH_USE_KEYRING`) take effect without
203
+ * rebuilding the flow.
172
204
  */
173
- storage: AuthConfigStorage;
205
+ storageFactory: (profile?: string) => AuthConfigStorage;
174
206
  /**
175
207
  * Whether the flow's credential resolvers (`getAPIToken` / `requireApiToken`)
176
208
  * should honour the global API key + email pair in addition to scoped API
@@ -265,6 +297,20 @@ declare function getAccessHeaders(domain: string, options: {
265
297
  * `WRANGLER_API_ENVIRONMENT=staging` environment variable instead.
266
298
  */
267
299
  declare const getAuthUrlFromEnv: () => string;
300
+ /**
301
+ * `CLOUDFLARE_AUTH_USE_KEYRING` overrides where OAuth credentials are stored.
302
+ *
303
+ * - `true` — force-store credentials in the OS keychain. If the keychain
304
+ * cannot be reached, the resolver throws rather than silently
305
+ * falling back, so security-sensitive callers know their
306
+ * explicit opt-in did not take effect.
307
+ * - `false` — force-store credentials in the plaintext TOML file,
308
+ * even if the consumer's persistent `keyring_enabled` preference
309
+ * is set.
310
+ * - unset — fall back to the consumer's persistent preference (e.g. the
311
+ * one written by `wrangler login --use-keyring`).
312
+ */
313
+ declare const getCloudflareAuthUseKeyringFromEnv: () => boolean | undefined;
268
314
 
269
315
  /**
270
316
  * Reason why {@link OAuthFlowAPI.loginOrRefreshIfRequired} could not
@@ -311,11 +357,25 @@ interface LoginProps {
311
357
  callbackHost?: string;
312
358
  /** Port the local callback server listens on. Defaults to `8976`. */
313
359
  callbackPort?: number;
360
+ /**
361
+ * Named auth profile to store the token under. When omitted, the default
362
+ * profile (`default.toml`) is used.
363
+ * Only for use by 'auth create', not exposed to the user
364
+ */
365
+ profile?: string;
314
366
  }
315
367
  /**
316
368
  * Public surface returned by {@link createOAuthFlow}.
317
369
  */
318
370
  interface OAuthFlowAPI {
371
+ /**
372
+ * Set the active auth profile for all subsequent storage lookups.
373
+ *
374
+ * Called once at top of command dispatch by the consumer after resolving the
375
+ * active profile. `"default"` if never called.
376
+ */
377
+ setProfile(profile: string): void;
378
+ getActiveProfile(): string;
319
379
  /**
320
380
  * Open the authorize URL in the user's browser, wait for the callback to be
321
381
  * hit on the local HTTP server, exchange the code for an access token, and
@@ -324,6 +384,9 @@ interface OAuthFlowAPI {
324
384
  * Refuses to start when `ctx.hasEnvCredentials()` returns `true`.
325
385
  * Refuses to start when the compliance region is `fedramp_high`.
326
386
  *
387
+ * When `props.profile` is set, the token is stored under that profile
388
+ * instead of the active one. This is used by `auth create <name>`.
389
+ *
327
390
  * @returns `true` on success, `false` when env credentials are present.
328
391
  */
329
392
  login(props: LoginProps): Promise<boolean>;
@@ -333,8 +396,11 @@ interface OAuthFlowAPI {
333
396
  *
334
397
  * No-op when `ctx.hasEnvCredentials()` returns `true` (env credentials
335
398
  * cannot be revoked).
399
+ *
400
+ * When `profile` is passed, operates on that profile instead of the
401
+ * active one. This is used by `auth delete <name>`.
336
402
  */
337
- logout(): Promise<void>;
403
+ logout(profile?: string): Promise<void>;
338
404
  /**
339
405
  * If the user has no stored OAuth token, attempt an interactive login.
340
406
  * If they have one but it is expired, attempt a refresh; if refresh fails,
@@ -371,6 +437,11 @@ interface OAuthFlowAPI {
371
437
  * available.
372
438
  */
373
439
  requireApiToken(): ApiCredentials;
440
+ /**
441
+ * Return the scopes granted to the stored OAuth token for the active
442
+ * profile, or `undefined` when no OAuth token is stored.
443
+ */
444
+ getScopes(): string[] | undefined;
374
445
  /**
375
446
  * Establish whether `--temporary` is permitted for this invocation. Called
376
447
  * once at command dispatch by the consumer. Also drops any temporary account
@@ -416,6 +487,43 @@ declare const TEMPORARY_TERMS_NOTICE: string;
416
487
  */
417
488
  declare const PKCE_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
418
489
 
490
+ interface ProfileConfigOperations {
491
+ exists(profile: string): boolean;
492
+ list(): string[];
493
+ delete(profile: string): void;
494
+ }
495
+ interface DirectoryBindingsStorage {
496
+ read(): Record<string, string>;
497
+ write(bindings: Record<string, string>): void;
498
+ }
499
+ interface DirectoryBindingOperations extends DirectoryBindingsStorage {
500
+ activate(profile: string, dir: string): void;
501
+ deactivate(dir: string): DeactivateDirectoryResult;
502
+ getProfileForDirectory(startDir: string): string | undefined;
503
+ getBindingsForProfile(profile: string): string[];
504
+ removeAllBindingsForProfile(profile: string): string[];
505
+ }
506
+ interface ProfileStore {
507
+ configs: ProfileConfigOperations;
508
+ bindings: DirectoryBindingOperations;
509
+ resolve(args: {
510
+ profile?: string;
511
+ cwd: string;
512
+ }): string;
513
+ }
514
+ interface DeactivateDirectoryResult {
515
+ removedProfile: string;
516
+ newResolution: {
517
+ profile: string | undefined;
518
+ source: string;
519
+ };
520
+ }
521
+ declare function createProfileStore(args: {
522
+ configs: ProfileConfigOperations;
523
+ bindings: DirectoryBindingsStorage;
524
+ }): ProfileStore;
525
+ declare function validateProfileName(name: string): void;
526
+
419
527
  interface RefreshToken {
420
528
  value: string;
421
529
  }
@@ -460,4 +568,658 @@ declare function readStoredAuthState(options: {
460
568
  storage: AuthConfigStorage;
461
569
  }): StoredAuthState;
462
570
 
463
- export { type AuthConfigStorage, type ConfigStorage, type LoginOrRefreshFailureReason, type LoginOrRefreshResult, type LoginProps, PKCE_CHARSET, TEMPORARY_TERMS_NOTICE, TEMPORARY_TERMS_PROMPT, type TemporaryPreviewAccount, type UserAuthConfig, clearAccessCaches, createOAuthFlow, domainUsesAccess, generateAuthUrl, generateRandomState, getAccessHeaders, getAuthFromEnv, getAuthUrlFromEnv, getCloudflareAPITokenFromEnv, getCloudflareGlobalAuthEmailFromEnv, getCloudflareGlobalAuthKeyFromEnv, readStoredAuthState };
571
+ /**
572
+ * Pluggable backend for the persisted OAuth credentials.
573
+ *
574
+ * Concrete implementations:
575
+ * - {@link FileCredentialStore} — the plaintext TOML file at
576
+ * `<globalWranglerConfigPath>/config/<env>.toml`. Used by default and as
577
+ * a fallback when keyring storage is unavailable.
578
+ * - {@link EncryptedFileCredentialStore} — an AES-256-GCM-encrypted file at
579
+ * `<globalWranglerConfigPath>/config/<env>.enc`, with the encryption key
580
+ * stored in the OS keyring via a {@link KeyProvider}.
581
+ *
582
+ * Extends the consumer-facing {@link AuthConfigStorage} contract with two
583
+ * keyring-specific extras:
584
+ * - `kind`: discriminant so consumers can tell apart "plain file" and
585
+ * "encrypted file" stores without sniffing for methods.
586
+ * - `describe()`: human-readable, possibly multi-line description (e.g.
587
+ * `"Encrypted file (path) with key in macOS Keychain"`) for surfaces
588
+ * like `wrangler whoami` that want richer copy than the raw `path()`.
589
+ *
590
+ * The interface is synchronous so it can be plugged into existing
591
+ * `AuthConfigStorage` call sites without forcing every caller to become
592
+ * async. Under the hood, both implementations use synchronous primitives
593
+ * (subprocess `spawnSync`, `@napi-rs/keyring`'s sync `Entry` class,
594
+ * `node:crypto` sync APIs, synchronous filesystem calls).
595
+ */
596
+ interface CredentialStore extends AuthConfigStorage {
597
+ readonly kind: "file" | "encrypted-file";
598
+ /**
599
+ * Human-readable description of where credentials are stored, suitable
600
+ * for consumers' `whoami`-style output. May be multi-line / richer
601
+ * than the raw `path()`.
602
+ */
603
+ describe(): string;
604
+ }
605
+
606
+ /**
607
+ * Absolute path to the plaintext TOML credentials file for the given auth
608
+ * profile (defaulting to the active Cloudflare API environment's default
609
+ * profile).
610
+ *
611
+ * `configPath` is the consumer's global config directory — the client (e.g.
612
+ * wrangler, or a future `cf` CLI) owns where its config lives, so it is passed
613
+ * in rather than resolved here. The environment is appended to the
614
+ * default-profile filename so callers running with
615
+ * `WRANGLER_API_ENVIRONMENT=staging` get a separate file from production; named
616
+ * profiles get `<profile>.toml`. The path stays exposed so the migration code,
617
+ * defensive scrubs on logout, and tests that assert against it can all point at
618
+ * the same location as the {@link FileCredentialStore}.
619
+ */
620
+ declare function getAuthConfigFilePath(configPath: string, profile?: string): string;
621
+ /**
622
+ * The plaintext-TOML credentials store.
623
+ *
624
+ * Used as the default backend when the user hasn't opted into keyring
625
+ * storage, and as the soft-fallback when keyring storage is requested
626
+ * but a backend isn't available.
627
+ */
628
+ declare class FileCredentialStore implements CredentialStore {
629
+ private readonly configPath;
630
+ private readonly profile?;
631
+ readonly kind: "file";
632
+ /**
633
+ * @param configPath consumer-provided global config directory (the CLI
634
+ * owns where its config lives, so workers-auth never resolves it itself).
635
+ * @param profile the auth profile (defaults to the active environment's
636
+ * default profile).
637
+ */
638
+ constructor(configPath: string, profile?: string | undefined);
639
+ read(): UserAuthConfig | undefined;
640
+ write(config: UserAuthConfig): void;
641
+ clear(): boolean;
642
+ path(): string;
643
+ describe(): string;
644
+ }
645
+
646
+ /**
647
+ * A small abstraction over OS keyring backends, scoped to "store/retrieve
648
+ * an arbitrary fixed-size key" rather than "store a credential blob".
649
+ *
650
+ * Keeping the responsibility this narrow means:
651
+ * - The 2.5 KB macOS Keychain item limit (and similar limits elsewhere)
652
+ * is never hit — we only ever store ~44 bytes of base64.
653
+ * - Per-platform code stays trivial; richer credential shapes can grow
654
+ * freely inside the encrypted file without touching the keyring.
655
+ * - The same `KeyProvider` is reusable for non-OAuth secrets in the
656
+ * future (e.g. account-scoped API tokens).
657
+ *
658
+ * Implementations are instantiated with the consumer's `serviceName`
659
+ * (e.g. "wrangler") so the same library can serve multiple Cloudflare
660
+ * CLIs without collisions.
661
+ */
662
+ interface KeyProvider {
663
+ /**
664
+ * Return the previously-stored key, or `undefined` when nothing has
665
+ * been stored for this `(serviceName, accountName)` pair. Backend
666
+ * failures throw so callers can surface meaningful errors.
667
+ */
668
+ getKey(): Uint8Array | undefined;
669
+ /** Persist the given key, overwriting any previous value. */
670
+ setKey(key: Uint8Array): void;
671
+ /** Remove the stored key. Idempotent — no-op when nothing is stored. */
672
+ deleteKey(): void;
673
+ /**
674
+ * Human-readable description of where the key is stored, surfaced via
675
+ * the {@link CredentialStore.describe} chain.
676
+ */
677
+ describe(): string;
678
+ }
679
+
680
+ /**
681
+ * Absolute path to the encrypted credentials file for the given auth profile
682
+ * (defaulting to the active Cloudflare API environment's default profile).
683
+ *
684
+ * `configPath` is the consumer's global config directory (see
685
+ * {@link getAuthConfigFilePath}). Sibling of the plaintext `<profile>.toml` so
686
+ * the migration code can non-destructively read the old file before writing the
687
+ * new one.
688
+ */
689
+ declare function getEncryptedAuthConfigFilePath(configPath: string, profile?: string): string;
690
+ /**
691
+ * Result of a successful migration from a plaintext TOML file
692
+ * into an encrypted file backed by a `KeyProvider`. Surfaced so the
693
+ * resolver can log a one-line summary when migration runs.
694
+ */
695
+ interface PlaintextMigrationResult {
696
+ plaintextPath: string;
697
+ encryptedPath: string;
698
+ keyProviderDescription: string;
699
+ }
700
+ /**
701
+ * Optional callback invoked by {@link EncryptedFileCredentialStore.read}
702
+ * when it transparently migrates a plaintext TOML file into the
703
+ * encrypted file on first read.
704
+ *
705
+ * The resolver wires this to its logger; left undefined when the store
706
+ * is constructed standalone (e.g. by tests).
707
+ */
708
+ type OnPlaintextMigration = (result: PlaintextMigrationResult) => void;
709
+ /**
710
+ * Credentials store backed by an AES-256-GCM-encrypted file on disk and a
711
+ * 32-byte encryption key held in the OS keyring via a {@link KeyProvider}.
712
+ *
713
+ * The combination decouples credential payload size from any per-platform
714
+ * keyring item size limit (notably the ~2.5 KB macOS Keychain limit on
715
+ * generic-password items): the keyring entry is always small (~44 bytes
716
+ * of base64), while the credential blob lives in the encrypted file and
717
+ * is free to grow as the schema evolves.
718
+ *
719
+ * Threat model:
720
+ * - File leaked from a backup without the keyring entry: ciphertext is
721
+ * useless, GCM auth tag prevents tampering.
722
+ * - Keyring entry leaked without the file: a bare 32-byte key, useless
723
+ * without the ciphertext.
724
+ * - Attacker with full local user access: can decrypt (same as
725
+ * direct-keyring storage — both backends expose secrets to root /
726
+ * same-user processes).
727
+ */
728
+ declare class EncryptedFileCredentialStore implements CredentialStore {
729
+ private readonly configPath;
730
+ private readonly keyProvider;
731
+ private readonly onPlaintextMigration?;
732
+ private readonly profile?;
733
+ readonly kind: "encrypted-file";
734
+ /**
735
+ * @param configPath consumer-provided global config directory (the CLI
736
+ * owns where its config lives, so workers-auth never resolves it itself).
737
+ * @param keyProvider the OS-keyring backend holding the encryption key.
738
+ * @param onPlaintextMigration optional callback invoked when a plaintext
739
+ * TOML file is migrated into the encrypted layout on first read.
740
+ * @param profile the auth profile (defaults to the active environment's
741
+ * default profile).
742
+ */
743
+ constructor(configPath: string, keyProvider: KeyProvider, onPlaintextMigration?: OnPlaintextMigration | undefined, profile?: string | undefined);
744
+ read(): UserAuthConfig | undefined;
745
+ write(config: UserAuthConfig): void;
746
+ clear(): boolean;
747
+ path(): string;
748
+ describe(): string;
749
+ private readEncryptedFile;
750
+ private migrateFromPlaintext;
751
+ /**
752
+ * Return the existing encryption key, or generate + persist a fresh
753
+ * one when none exists yet. Called from `write()` so the first
754
+ * `wrangler login --use-keyring` is fully bootstrapping.
755
+ */
756
+ private ensureKey;
757
+ }
758
+
759
+ /**
760
+ * Per-consumer configuration for the credential-storage resolver.
761
+ *
762
+ * Captured by {@link createCredentialStorageContext} in a closure so the
763
+ * returned `storage` adapter and `getActiveStore` function can both
764
+ * re-resolve the active store on every call without re-reading shared
765
+ * mutable module state.
766
+ */
767
+ interface CredentialStorageContext {
768
+ /**
769
+ * Keyring service identifier (e.g. `"wrangler"`). Becomes the `-s`
770
+ * argument to `/usr/bin/security`, the `service` attribute for
771
+ * `secret-tool`, and the `service` argument to `@napi-rs/keyring`'s
772
+ * `Entry`. Must be non-empty.
773
+ */
774
+ serviceName: string;
775
+ /**
776
+ * The consumer's global config directory (e.g. wrangler's
777
+ * `~/.wrangler`), resolved lazily on every credential operation.
778
+ *
779
+ * The client owns where its config lives — `cf` uses a different global
780
+ * config path than wrangler — so `@cloudflare/workers-auth` never resolves
781
+ * it itself. A getter (rather than a captured string) is required because
782
+ * consumers create this context once at module load while tests re-stub
783
+ * `HOME` / `XDG_CONFIG_HOME` per case, so the path must be re-read on each
784
+ * call. The credential files (`.toml` / `.enc`) and the Windows keyring
785
+ * binding all live under this directory.
786
+ */
787
+ getConfigPath: () => string;
788
+ /**
789
+ * Whether the user has opted into keyring storage. Consulted on every
790
+ * credential read/write so runtime preference changes (e.g. a user
791
+ * toggling the option mid-session) take effect.
792
+ */
793
+ isKeyringEnabled: () => boolean;
794
+ /** Drop-in replacement for the consumer's logger singleton. */
795
+ logger: OAuthFlowLogger;
796
+ /** Whether the process should not prompt the user. */
797
+ isNonInteractiveOrCI: () => boolean;
798
+ /**
799
+ * Consumer's CLI name for error-message templating, e.g. `"wrangler"`.
800
+ * Used in hints like ``Run `<cliName> login --use-keyring` …``.
801
+ * Defaults to `"your CLI"` when omitted.
802
+ */
803
+ cliName?: string;
804
+ }
805
+ /**
806
+ * Bundle returned by {@link createCredentialStorageContext}.
807
+ *
808
+ * - `storageFactory`: maps an auth profile to an {@link AuthConfigStorage}
809
+ * adapter that delegates to that profile's active {@link CredentialStore}
810
+ * on every method call. Pass this as `ctx.storageFactory` to
811
+ * {@link createOAuthFlow}, which calls it with the active profile on every
812
+ * credential access.
813
+ * - `getActiveStore`: the live `CredentialStore` lookup for a given profile,
814
+ * suitable for `whoami`-style consumers that want to call `describe()`.
815
+ *
816
+ * Both surfaces are wired against the same closure, so a runtime preference
817
+ * flip (e.g. `wrangler login --no-use-keyring`) is observable through both
818
+ * on the very next call. The profile argument selects which profile's files
819
+ * / keyring entry the resolved store reads and writes.
820
+ */
821
+ interface CredentialStorageBundle {
822
+ storageFactory: (profile?: string) => AuthConfigStorage;
823
+ getActiveStore: (profile?: string) => CredentialStore;
824
+ }
825
+ /**
826
+ * Build a credential-storage bundle for a consumer (wrangler, future
827
+ * Cloudflare CLIs).
828
+ *
829
+ * The bundle's `storage` is an `AuthConfigStorage` that re-resolves the
830
+ * underlying store on every read/write/clear/path call. Selection order
831
+ * (highest precedence first):
832
+ *
833
+ * 1. `CLOUDFLARE_AUTH_USE_KEYRING=false` env var — forces the file store.
834
+ * 2. `CLOUDFLARE_AUTH_USE_KEYRING=true` env var — forces keyring storage;
835
+ * failures throw rather than soft-falling-back.
836
+ * 3. `isKeyringEnabled()` callback (the consumer's persistent preference) —
837
+ * uses keyring storage; failures soft-fall-back with a one-time warning.
838
+ * 4. Otherwise — defaults to the plaintext file store.
839
+ *
840
+ * The env var and the `isKeyringEnabled` callback are re-read on every
841
+ * call so runtime preference changes take effect without rebuilding the
842
+ * storage layer.
843
+ */
844
+ declare function createCredentialStorageContext(context: CredentialStorageContext): CredentialStorageBundle;
845
+ /**
846
+ * Outcome of {@link scrubEncryptedCredentials}, so callers can tailor their
847
+ * messaging (e.g. warn that a keyring entry may remain).
848
+ */
849
+ interface ScrubEncryptedCredentialsResult {
850
+ /**
851
+ * Whether the keyring backend was *reachable* — i.e. a `KeyProvider`
852
+ * resolved for this platform. When `true`, the encrypted store's `clear()`
853
+ * removed the `.enc` file (and any plaintext `.toml`) and attempted to
854
+ * delete the keyring key. When `false`, only the `.enc` file was removed
855
+ * best-effort and a keyring entry (if one exists) was left behind.
856
+ *
857
+ * Note this reflects reachability, not that every operation succeeded: the
858
+ * key deletion is best-effort (see `EncryptedFileCredentialStore.clear()`),
859
+ * so in the rare case a reachable keyring fails mid-delete the `.enc`
860
+ * ciphertext is still gone and only a bare, unusable key could linger.
861
+ */
862
+ backendAvailable: boolean;
863
+ /** Whether an encrypted `.enc` file existed before the scrub. */
864
+ encryptedFileExisted: boolean;
865
+ }
866
+ /**
867
+ * Best-effort removal of a profile's encrypted credentials (`.enc` file) and
868
+ * the keyring entry holding its encryption key.
869
+ *
870
+ * Shared by the `--no-use-keyring` opt-out (default profile) and
871
+ * `wrangler auth delete <name>` (named profile) so both clear the encrypted
872
+ * backend the same way, independent of the current keyring preference (a
873
+ * profile may have been encrypted in a previous session even though keyring
874
+ * storage is currently disabled).
875
+ *
876
+ * When the keyring backend is reachable, the encrypted store's `clear()`
877
+ * removes the `.enc` file *and* the keyring key (and any plaintext
878
+ * `.toml`). When it isn't (Linux without `secret-tool`, Windows without the
879
+ * binding, an unsupported platform), the `.enc` file is removed best-effort
880
+ * and `backendAvailable` is `false` so the caller can warn that the keyring
881
+ * entry may remain. This never attempts a (Windows) binding install — a scrub
882
+ * must not block on provisioning a backend it is trying to tear down.
883
+ */
884
+ declare function scrubEncryptedCredentials(options: {
885
+ serviceName: string;
886
+ configPath: string;
887
+ profile?: string;
888
+ }): ScrubEncryptedCredentialsResult;
889
+
890
+ /**
891
+ * Reset module-level per-session resolver flags (memoized warnings, the
892
+ * Windows install-failed latch).
893
+ *
894
+ * Tests use this to start each case from a clean slate. In production
895
+ * the flags reset naturally when the wrangler process exits.
896
+ */
897
+ declare function resetCredentialStorageState(): void;
898
+ /**
899
+ * Alias kept for tests that previously called this to fully tear down
900
+ * both the configuration and the session flags. With the per-consumer
901
+ * configuration captured in `createCredentialStorageContext`, this now
902
+ * does the same thing as {@link resetCredentialStorageState}: clear the
903
+ * session flags.
904
+ */
905
+ declare const clearCredentialStorageState: typeof resetCredentialStorageState;
906
+
907
+ /**
908
+ * Resolve the keyring account name for the given auth profile.
909
+ *
910
+ * Shares {@link resolveAuthProfileBaseName} with the file paths so the
911
+ * keyring entry tracks the same identity as the on-disk files: the default
912
+ * profile gets `default` in production or the environment name otherwise
913
+ * (so a single OS user can hold production and staging credentials
914
+ * side-by-side), while a named profile gets the profile name. This means
915
+ * each profile holds its own encryption key, so clearing one profile's
916
+ * credentials never disturbs another's.
917
+ *
918
+ * The service name is consumer-configured (passed to each `KeyProvider`'s
919
+ * constructor) — this account-name derivation is Cloudflare-wide and stays
920
+ * inside `@cloudflare/workers-auth`.
921
+ */
922
+ declare function getKeyringAccountName(profile?: string): string;
923
+
924
+ /** Signature of the `/usr/bin/security` invoker. Overridable for tests. */
925
+ type MacSecurityCommandRunner = (args: string[], options?: {
926
+ input?: string;
927
+ }) => SpawnSyncReturns<string>;
928
+ /**
929
+ * Override the `/usr/bin/security` invoker for tests. Pass `undefined` to
930
+ * restore the default real-process runner.
931
+ */
932
+ declare function setMacSecurityCommandRunner(fn: MacSecurityCommandRunner | undefined): void;
933
+ /**
934
+ * macOS Keychain backend that stores the encryption key for the active
935
+ * {@link EncryptedFileCredentialStore} via the `/usr/bin/security` CLI.
936
+ *
937
+ * The `security` binary is part of every macOS install so this path has
938
+ * zero install cost. The `serviceName` is consumer-provided so different
939
+ * Cloudflare CLIs (wrangler, future tools) can coexist on the same
940
+ * keychain without colliding.
941
+ *
942
+ * Trade-off: `add-generic-password -w <secret>` puts the secret on the
943
+ * argv of a short-lived subprocess, briefly visible to other processes
944
+ * running as the same user (e.g. via `ps`). This is the same trade-off
945
+ * accepted by `git credential-osxkeychain`. Because the secret here is
946
+ * only the 32-byte encryption key (not the OAuth tokens themselves), the
947
+ * exposure window is narrower than the previous direct-keyring design.
948
+ */
949
+ declare class MacSecurityKeyProvider implements KeyProvider {
950
+ private readonly serviceName;
951
+ private readonly profile?;
952
+ constructor(serviceName: string, profile?: string | undefined);
953
+ getKey(): Uint8Array | undefined;
954
+ setKey(key: Uint8Array): void;
955
+ deleteKey(): void;
956
+ describe(): string;
957
+ }
958
+
959
+ /** Signature of the `secret-tool` invoker. Overridable for tests. */
960
+ type LinuxSecretToolRunner = (args: string[], options?: {
961
+ input?: string;
962
+ }) => SpawnSyncReturns<string>;
963
+ /**
964
+ * Override the `secret-tool` invoker for tests. Pass `undefined` to restore
965
+ * the default real-process runner. Resets the memoized
966
+ * {@link probeSecretTool} result so the next call re-probes through the
967
+ * new runner.
968
+ */
969
+ declare function setLinuxSecretToolRunner(fn: LinuxSecretToolRunner | undefined): void;
970
+ /**
971
+ * Probe whether `secret-tool` is callable in the current environment.
972
+ *
973
+ * Returns `true` when `secret-tool --version` exits 0. The probe does not
974
+ * exercise the keyring backend itself — a missing D-Bus session surfaces
975
+ * on the first real read/write rather than every consumer invocation, so
976
+ * we avoid the extra latency on every command for users whose desktop
977
+ * session is fully working.
978
+ *
979
+ * The result is memoized per-process: `secret-tool` is not going to be
980
+ * uninstalled mid-command, and the resolver re-resolves the active store
981
+ * on every credential operation (see `resolver.ts`), so caching keeps the
982
+ * probe off the hot path. Tests reset the cache via
983
+ * {@link setLinuxSecretToolRunner}.
984
+ */
985
+ declare function probeSecretTool(): boolean;
986
+ /**
987
+ * Linux backend that stores the encryption key via libsecret's
988
+ * `secret-tool` CLI.
989
+ *
990
+ * The key is passed to `secret-tool store` via stdin so it never appears
991
+ * on the subprocess argv. Lookup writes the key envelope to stdout, which
992
+ * is captured by `spawnSync`.
993
+ *
994
+ * `secret-tool` is part of the `libsecret-tools` package on most Linux
995
+ * distros. The resolver in {@link "../resolver"} probes for its presence
996
+ * and surfaces actionable install hints when missing; this class assumes
997
+ * the tool is available.
998
+ */
999
+ declare class LinuxSecretToolKeyProvider implements KeyProvider {
1000
+ private readonly serviceName;
1001
+ private readonly profile?;
1002
+ constructor(serviceName: string, profile?: string | undefined);
1003
+ getKey(): Uint8Array | undefined;
1004
+ setKey(key: Uint8Array): void;
1005
+ deleteKey(): void;
1006
+ describe(): string;
1007
+ }
1008
+
1009
+ /**
1010
+ * Backend that stores the encryption key via `@napi-rs/keyring`'s
1011
+ * native `Entry` class.
1012
+ *
1013
+ * Used on Windows once the binding has been lazy-installed (via the
1014
+ * resolver), and by tests on every platform that register an in-memory
1015
+ * `KeyringEntryFactory` via {@link setKeyringEntryFactory}.
1016
+ *
1017
+ * On Windows the binding talks to the Credential Manager wincred API.
1018
+ * On macOS / Linux (when used by tests), the test factory short-circuits
1019
+ * the lazy load so no real keychain is touched.
1020
+ */
1021
+ declare class NapiKeyringKeyProvider implements KeyProvider {
1022
+ private readonly serviceName;
1023
+ private readonly installDir;
1024
+ private readonly profile?;
1025
+ /**
1026
+ * @param serviceName keyring service identifier (e.g. `"wrangler"`).
1027
+ * @param installDir directory hosting the lazy-installed `@napi-rs/keyring`
1028
+ * binding, derived from the consumer's config path.
1029
+ * @param profile the auth profile (selects the keyring account name).
1030
+ */
1031
+ constructor(serviceName: string, installDir: string, profile?: string | undefined);
1032
+ private entry;
1033
+ getKey(): Uint8Array | undefined;
1034
+ setKey(key: Uint8Array): void;
1035
+ deleteKey(): void;
1036
+ describe(): string;
1037
+ }
1038
+
1039
+ /**
1040
+ * Pinned version of `@napi-rs/keyring` that gets installed lazily on
1041
+ * Windows when the user opts into keyring storage.
1042
+ *
1043
+ * Hard-coded so we stamp the same version into both the user-facing
1044
+ * "install it globally" hint and the actual `npm install` command —
1045
+ * ensuring CI users running the global-install workaround see exactly
1046
+ * the version we tested against.
1047
+ */
1048
+ declare const PINNED_KEYRING_VERSION = "1.3.0";
1049
+ /** Signature of the `npm` invoker. Overridable for tests. */
1050
+ type NpmRunner = (args: string[]) => SpawnSyncReturns<string>;
1051
+ /**
1052
+ * Override the `npm` invoker for tests. Pass `undefined` to restore the
1053
+ * default real-process runner. Resets the memoized
1054
+ * {@link findKeyringBinding} cache so the next call re-probes through
1055
+ * the new runner.
1056
+ */
1057
+ declare function setNpmRunner(fn: NpmRunner | undefined): void;
1058
+ /**
1059
+ * Directory used to host the lazy-installed `@napi-rs/keyring` binding, under
1060
+ * the consumer-provided global config directory (`configPath`). A sibling of
1061
+ * the credentials dir so `runInTempDir()` test fixtures (which redirect `HOME`
1062
+ * / `XDG_CONFIG_HOME`) isolate the install dir alongside everything else stored
1063
+ * under the config path.
1064
+ *
1065
+ * `configPath` is supplied by the client (wrangler, or a future `cf` CLI) — the
1066
+ * same value used for the credential files — so each CLI's native binding lives
1067
+ * under its own config directory rather than a hard-coded wrangler location.
1068
+ */
1069
+ declare function getKeyringInstallDir(configPath: string): string;
1070
+ /**
1071
+ * Locate an installed `@napi-rs/keyring` binding usable from this process.
1072
+ *
1073
+ * Search order:
1074
+ * 1. The private install dir written by {@link installKeyringBindingSync}.
1075
+ * 2. The user's global npm root, so a manual `npm install -g
1076
+ * @napi-rs/keyring` works in CI environments where the lazy install
1077
+ * path is unavailable.
1078
+ *
1079
+ * Returns `null` when no binding is available; callers handle the missing
1080
+ * binding case explicitly so they can surface remediation instructions.
1081
+ *
1082
+ * The result is memoized per-process, keyed by `installDir`. The resolver
1083
+ * calls this on every credential operation, so without caching we would spawn
1084
+ * `npm root -g` each time the binding is not in the lazy dir. The cache is
1085
+ * invalidated by {@link installKeyringBindingSync} on a successful install and
1086
+ * by {@link setNpmRunner} for test isolation.
1087
+ */
1088
+ declare function findKeyringBinding(installDir: string): string | null;
1089
+ /**
1090
+ * Install `@napi-rs/keyring` into the private install dir using the user's
1091
+ * `npm`.
1092
+ *
1093
+ * Throws a {@link UserError} when `npm` cannot be spawned or returns a
1094
+ * non-zero exit code, so callers can surface actionable remediation hints
1095
+ * rather than a raw stack trace.
1096
+ */
1097
+ declare function installKeyringBindingSync(installDir: string): void;
1098
+ /**
1099
+ * Minimal slice of `@napi-rs/keyring`'s `Entry` class that {@link
1100
+ * NapiKeyringKeyProvider} depends on.
1101
+ *
1102
+ * Defined locally so we can load the underlying module lazily — the native
1103
+ * binary must not be required (and `dlopen`-ed) unless the user has
1104
+ * actually opted into keyring storage.
1105
+ */
1106
+ interface KeyringEntry {
1107
+ setPassword(password: string): void;
1108
+ getPassword(): string | null;
1109
+ deletePassword(): boolean;
1110
+ }
1111
+ /** Factory signature used by `NapiKeyringKeyProvider` to create entries. */
1112
+ type KeyringEntryFactory = (service: string, account: string) => KeyringEntry;
1113
+ /**
1114
+ * Override the keyring entry factory used by `NapiKeyringKeyProvider`.
1115
+ *
1116
+ * Tests use this to inject an in-memory fake so they never touch the
1117
+ * developer machine's real keychain. Pass `undefined` to restore the
1118
+ * default (lazy dynamic require).
1119
+ */
1120
+ declare function setKeyringEntryFactory(factory: KeyringEntryFactory | undefined): void;
1121
+
1122
+ /**
1123
+ * Outcome of trying to create a `KeyProvider` for the current platform.
1124
+ *
1125
+ * - `available`: a provider exists and is ready to use immediately.
1126
+ * - `needs-install`: a provider is in principle supported but the local
1127
+ * binding/tool isn't installed yet. The resolver decides whether to
1128
+ * attempt an interactive install or surface remediation.
1129
+ * - `unsupported`: the platform has no `KeyProvider` implementation
1130
+ * (e.g. FreeBSD). The resolver falls back to file storage.
1131
+ */
1132
+ type KeyProviderResolution = {
1133
+ kind: "available";
1134
+ provider: KeyProvider;
1135
+ } | {
1136
+ kind: "needs-install";
1137
+ install: () => void;
1138
+ afterInstall: () => KeyProvider;
1139
+ } | {
1140
+ kind: "unsupported";
1141
+ };
1142
+ /**
1143
+ * Inject a `KeyProvider` factory for testing. Pass `undefined` to restore
1144
+ * the real per-platform resolution. The factory receives the auth profile
1145
+ * so profile-aware tests can hand back a provider scoped to that profile.
1146
+ */
1147
+ declare function setKeyProviderFactoryForTesting(factory: ((serviceName: string, profile?: string) => KeyProvider) | undefined): void;
1148
+ /**
1149
+ * Resolve the appropriate `KeyProvider` for the current platform.
1150
+ *
1151
+ * Pure resolution: this function never spawns long-running work, never
1152
+ * installs anything, and never logs. The "needs-install" arm returns
1153
+ * thunks the caller can invoke once it has decided how to handle the
1154
+ * missing-binding case (interactive install vs. hard-error).
1155
+ *
1156
+ * `configPath` is the consumer's global config directory; on Windows it locates
1157
+ * the lazy-installed `@napi-rs/keyring` binding under the CLI's own config dir.
1158
+ * The macOS / Linux backends don't use it.
1159
+ */
1160
+ declare function resolveKeyProvider(serviceName: string, profile: string | undefined, configPath: string): KeyProviderResolution;
1161
+
1162
+ /**
1163
+ * Self-documenting algorithm identifier stamped into the on-disk envelope.
1164
+ */
1165
+ declare const ALGORITHM_LABEL = "AES-256-GCM";
1166
+ /**
1167
+ * Envelope format for the encrypted payload written to disk.
1168
+ *
1169
+ * The `v` field lets us evolve the format (e.g. switch algorithm, add key
1170
+ * IDs) without breaking older readers — they can detect an unknown version
1171
+ * and refuse to decrypt rather than fail in an obscure way deep inside
1172
+ * the cipher.
1173
+ */
1174
+ interface EncryptedEnvelope {
1175
+ /** Schema version. Bump on incompatible changes. */
1176
+ v: 1;
1177
+ /** Algorithm label. Informational; the reader still validates against {@link CIPHER_NAME}. */
1178
+ alg: typeof ALGORITHM_LABEL;
1179
+ /** Base64-encoded 12-byte IV. */
1180
+ iv: string;
1181
+ /** Base64-encoded 16-byte GCM auth tag. */
1182
+ tag: string;
1183
+ /** Base64-encoded ciphertext. */
1184
+ ciphertext: string;
1185
+ }
1186
+ /**
1187
+ * Generate a fresh 32-byte symmetric key suitable for AES-256.
1188
+ *
1189
+ * Uses `node:crypto.randomBytes`, which is a thin wrapper over the OS
1190
+ * CSPRNG (`/dev/urandom` on Unix, `BCryptGenRandom` on Windows).
1191
+ */
1192
+ declare function generateKey(): Uint8Array;
1193
+ /**
1194
+ * Encrypt a UTF-8 plaintext into an {@link EncryptedEnvelope} using
1195
+ * AES-256-GCM with a freshly-generated IV.
1196
+ *
1197
+ * GCM is authenticated encryption: the auth tag is computed over the
1198
+ * ciphertext and validated by {@link decryptString}. Any tampering with
1199
+ * the IV, tag, or ciphertext causes decryption to throw.
1200
+ *
1201
+ * IV uniqueness is critical for GCM security — generating a fresh random
1202
+ * IV per call (rather than counter-based) keeps callers from accidentally
1203
+ * reusing one when the same key encrypts multiple payloads.
1204
+ */
1205
+ declare function encryptString(plaintext: string, key: Uint8Array): EncryptedEnvelope;
1206
+ /**
1207
+ * Decrypt an {@link EncryptedEnvelope} back into the original UTF-8
1208
+ * plaintext.
1209
+ *
1210
+ * @throws {Error} when the key length is wrong, the envelope version is
1211
+ * unknown, the IV/tag lengths don't match what AES-GCM expects, or the
1212
+ * GCM auth tag fails to verify (tampering or wrong key).
1213
+ */
1214
+ declare function decryptString(envelope: EncryptedEnvelope, key: Uint8Array): string;
1215
+ /**
1216
+ * Type-narrow `unknown` to an {@link EncryptedEnvelope}, returning
1217
+ * `undefined` when the input doesn't match the schema.
1218
+ *
1219
+ * Used by {@link EncryptedFileCredentialStore.read} to handle corrupted /
1220
+ * truncated / pre-format-v1 files as "no credentials stored" rather than
1221
+ * crashing the consumer.
1222
+ */
1223
+ declare function parseEncryptedEnvelope(raw: unknown): EncryptedEnvelope | undefined;
1224
+
1225
+ export { type AuthConfigStorage, type ConfigStorage, type CredentialStorageBundle, type CredentialStorageContext, type CredentialStore, type DeactivateDirectoryResult, type DirectoryBindingOperations, type DirectoryBindingsStorage, type EncryptedEnvelope, EncryptedFileCredentialStore, FileCredentialStore, type KeyProvider, type KeyProviderResolution, type KeyringEntry, type KeyringEntryFactory, LinuxSecretToolKeyProvider, type LinuxSecretToolRunner, type LoginOrRefreshFailureReason, type LoginOrRefreshResult, type LoginProps, type MacSecurityCommandRunner, MacSecurityKeyProvider, NapiKeyringKeyProvider, type NpmRunner, type OAuthFlowContext, type OAuthFlowLogger, PINNED_KEYRING_VERSION, PKCE_CHARSET, type ProfileConfigOperations, type ProfileStore, type ScrubEncryptedCredentialsResult, TEMPORARY_TERMS_NOTICE, TEMPORARY_TERMS_PROMPT, type TemporaryPreviewAccount, type UserAuthConfig, clearAccessCaches, clearCredentialStorageState, createCredentialStorageContext, createOAuthFlow, createProfileStore, decryptString, domainUsesAccess, encryptString, findKeyringBinding, generateAuthUrl, generateKey, generateRandomState, getAccessHeaders, getAuthConfigFilePath, getAuthFromEnv, getAuthUrlFromEnv, getCloudflareAPITokenFromEnv, getCloudflareAuthUseKeyringFromEnv, getCloudflareGlobalAuthEmailFromEnv, getCloudflareGlobalAuthKeyFromEnv, getEncryptedAuthConfigFilePath, getKeyringAccountName, getKeyringInstallDir, installKeyringBindingSync, parseEncryptedEnvelope, probeSecretTool, readStoredAuthState, resetCredentialStorageState, resolveKeyProvider, scrubEncryptedCredentials, setKeyProviderFactoryForTesting, setKeyringEntryFactory, setLinuxSecretToolRunner, setMacSecurityCommandRunner, setNpmRunner, validateProfileName };