@agentxm/registry-auth 0.28.12 → 0.28.13

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.
@@ -17,7 +17,7 @@ import * as ServiceMap from "effect/Context";
17
17
  import * as Effect from "effect/Effect";
18
18
  import * as Layer from "effect/Layer";
19
19
  import { type Handle } from "@agentxm/extension-model/unstable/extensions/handle";
20
- import { type PublishVisibility } from "@agentxm/registry-protocol/unstable/publish/visibility";
20
+ import { type PublishVisibility } from "@agentxm/registry-protocol/unstable/publish";
21
21
  import { type PreviewPublicationSetRequest, type PreviewPublicationSetResponse, type Sha256Hex } from "@agentxm/registry-protocol/unstable/registry/publication-set";
22
22
  import { type NormalizedTokenResponse } from "./oauth-contract.js";
23
23
  import { GeneratedRegistryClient, RegistryUrl } from "@agentxm/registry-client";
@@ -23,7 +23,7 @@ import * as Schedule from "effect/Schedule";
23
23
  import * as Schema from "effect/Schema";
24
24
  import { DateTimeUtcSchema } from "@agentxm/extension-model/unstable/date-time";
25
25
  import { normalizeHandle } from "@agentxm/extension-model/unstable/extensions/handle";
26
- import { PublishVisibilitySchema, } from "@agentxm/registry-protocol/unstable/publish/visibility";
26
+ import { PublishVisibilitySchema, } from "@agentxm/registry-protocol/unstable/publish";
27
27
  import { PreviewPublicationSetResponseSchema, } from "@agentxm/registry-protocol/unstable/registry/publication-set";
28
28
  import {} from "./oauth-contract.js";
29
29
  import { GeneratedRegistryClient, executeRegistryRequest, RegistryUrl, captureRegistryErrorResponseBodies, getString, isHttpClientError, isRegistryClientError, isRegistryClientFailure, isTransientHttpClientError, mapRegistryFailure, } from "@agentxm/registry-client";
@@ -68,8 +68,45 @@ export declare const makePersistedCredentialsUnsupportedError: () => AuthTokenPo
68
68
  export declare const CredentialStoreLive: Layer.Layer<CredentialStore, never, FileSystem.FileSystem | Path.Path>;
69
69
  /**
70
70
  * Decorates a credential store with a per-layer, per-origin read memo.
71
- * Successful empty reads are memoized; failures remain retryable. Every
72
- * successful write invalidates only the affected origin.
71
+ *
72
+ * Reads go through an `Effect.Cache` keyed by registry origin, which owns the
73
+ * concurrent-lookup sharing, per-key invalidation, and result-dependent expiry
74
+ * that this layer previously hand-rolled with a value map, a semaphore map, and
75
+ * a double-checked read.
76
+ *
77
+ * Capacity is `Number.POSITIVE_INFINITY`: the cache never evicts on size, which
78
+ * preserves the unbounded map this layer already used. The key space is the set
79
+ * of distinct registry origins a single process contacts — the configured
80
+ * registry plus any origins named by workspace extension sources — so there is
81
+ * no measured workload that justifies a numeric bound. The bound that matters
82
+ * is lifetime, and it is the layer's: the cache is created per
83
+ * `CredentialStoreSessionLive` construction and released with it, so a CLI
84
+ * session is its longest possible life.
85
+ *
86
+ * Expiry is result-dependent. Successful reads — including a successful empty
87
+ * read — live for the session, so repeated authentication paths pay one load
88
+ * per origin. Failed reads expire immediately, so a failure is never memoized
89
+ * and a later request retries against the store.
90
+ *
91
+ * Callers that arrive while a lookup is in flight share it rather than issuing
92
+ * their own; callers that arrive after a failed lookup resolves start a new
93
+ * one. That replaces the old per-origin semaphore, under which every waiter on
94
+ * a failing origin retried serially. Sharing is safe here because each auth
95
+ * path issues a single load and none of them depends on serial retry.
96
+ *
97
+ * `Effect.onInterrupt` is load-path scaffolding, not decoration. `Cache.get`
98
+ * runs its lookup on a detached daemon fiber and leaves the pending entry in
99
+ * the map when the only caller is interrupted, so without this guard the next
100
+ * read of that origin would await an abandoned lookup forever. Invalidating on
101
+ * interrupt restores the pre-cache behavior, where an interrupted read left
102
+ * nothing behind and the next read simply ran again.
103
+ *
104
+ * Known limitation, unchanged by this layer's move to `Cache` and not fixed by
105
+ * it: a `save` or `clear` whose invalidation lands while a read of the same
106
+ * origin is still in flight can be overwritten when that read populates the
107
+ * memo, leaving the pre-write value cached for the rest of the session. The
108
+ * previous value-map-and-semaphore memo lost the same invalidation through its
109
+ * own write-after-invalidate window.
73
110
  */
74
111
  export declare const CredentialStoreSessionLive: Layer.Layer<CredentialStore, never, CredentialStore>;
75
112
  export declare const CredentialStoreTest: (tier?: StorageTier, initialData?: CredentialFile, allowsPersistedCredentials?: boolean) => Layer.Layer<CredentialStore, never, never>;
@@ -20,12 +20,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
20
20
  import * as FileSystem from "effect/FileSystem";
21
21
  import * as Path from "effect/Path";
22
22
  import * as ServiceMap from "effect/Context";
23
+ import * as Cache from "effect/Cache";
24
+ import * as Duration from "effect/Duration";
23
25
  import * as Effect from "effect/Effect";
26
+ import * as Exit from "effect/Exit";
24
27
  import * as Layer from "effect/Layer";
25
28
  import * as Option from "effect/Option";
26
- import * as Ref from "effect/Ref";
27
29
  import * as Schema from "effect/Schema";
28
- import * as Semaphore from "effect/Semaphore";
29
30
  import * as lockfile from "proper-lockfile";
30
31
  import { decodeHandleSync } from "@agentxm/extension-model/unstable/extensions/handle";
31
32
  import { AuthTokenPolicyRequired, RegistryAuthFailed } from "./errors.js";
@@ -340,57 +341,60 @@ export const CredentialStoreLive = Layer.effect(CredentialStore, Effect.gen(func
340
341
  }));
341
342
  /**
342
343
  * Decorates a credential store with a per-layer, per-origin read memo.
343
- * Successful empty reads are memoized; failures remain retryable. Every
344
- * successful write invalidates only the affected origin.
344
+ *
345
+ * Reads go through an `Effect.Cache` keyed by registry origin, which owns the
346
+ * concurrent-lookup sharing, per-key invalidation, and result-dependent expiry
347
+ * that this layer previously hand-rolled with a value map, a semaphore map, and
348
+ * a double-checked read.
349
+ *
350
+ * Capacity is `Number.POSITIVE_INFINITY`: the cache never evicts on size, which
351
+ * preserves the unbounded map this layer already used. The key space is the set
352
+ * of distinct registry origins a single process contacts — the configured
353
+ * registry plus any origins named by workspace extension sources — so there is
354
+ * no measured workload that justifies a numeric bound. The bound that matters
355
+ * is lifetime, and it is the layer's: the cache is created per
356
+ * `CredentialStoreSessionLive` construction and released with it, so a CLI
357
+ * session is its longest possible life.
358
+ *
359
+ * Expiry is result-dependent. Successful reads — including a successful empty
360
+ * read — live for the session, so repeated authentication paths pay one load
361
+ * per origin. Failed reads expire immediately, so a failure is never memoized
362
+ * and a later request retries against the store.
363
+ *
364
+ * Callers that arrive while a lookup is in flight share it rather than issuing
365
+ * their own; callers that arrive after a failed lookup resolves start a new
366
+ * one. That replaces the old per-origin semaphore, under which every waiter on
367
+ * a failing origin retried serially. Sharing is safe here because each auth
368
+ * path issues a single load and none of them depends on serial retry.
369
+ *
370
+ * `Effect.onInterrupt` is load-path scaffolding, not decoration. `Cache.get`
371
+ * runs its lookup on a detached daemon fiber and leaves the pending entry in
372
+ * the map when the only caller is interrupted, so without this guard the next
373
+ * read of that origin would await an abandoned lookup forever. Invalidating on
374
+ * interrupt restores the pre-cache behavior, where an interrupted read left
375
+ * nothing behind and the next read simply ran again.
376
+ *
377
+ * Known limitation, unchanged by this layer's move to `Cache` and not fixed by
378
+ * it: a `save` or `clear` whose invalidation lands while a read of the same
379
+ * origin is still in flight can be overwritten when that read populates the
380
+ * memo, leaving the pre-write value cached for the rest of the session. The
381
+ * previous value-map-and-semaphore memo lost the same invalidation through its
382
+ * own write-after-invalidate window.
345
383
  */
346
384
  export const CredentialStoreSessionLive = Layer.effect(CredentialStore, Effect.gen(function* () {
347
385
  const store = yield* CredentialStore;
348
- const cache = yield* Ref.make(new Map());
349
- const locks = yield* Ref.make(new Map());
350
- const getLock = (registryUrl) => Ref.modify(locks, (current) => {
351
- const existing = current.get(registryUrl);
352
- if (existing !== undefined)
353
- return [existing, current];
354
- const created = Semaphore.makeUnsafe(1);
355
- const updated = new Map(current);
356
- updated.set(registryUrl, created);
357
- return [created, updated];
358
- });
359
- const getCached = (registryUrl) => Effect.map(Ref.get(cache), (current) => {
360
- const cached = current.get(registryUrl);
361
- return cached === undefined
362
- ? Option.none()
363
- : Option.some(cached);
364
- });
365
- const invalidate = (registryUrl) => Ref.update(cache, (current) => {
366
- const updated = new Map(current);
367
- updated.delete(registryUrl);
368
- return updated;
369
- });
370
- const load = (registryUrl) => Effect.gen(function* () {
371
- const cached = yield* getCached(registryUrl);
372
- if (Option.isSome(cached))
373
- return cached.value;
374
- const lock = yield* getLock(registryUrl);
375
- return yield* lock.withPermits(1)(Effect.gen(function* () {
376
- const afterWait = yield* getCached(registryUrl);
377
- if (Option.isSome(afterWait))
378
- return afterWait.value;
379
- const loaded = yield* store.load(registryUrl);
380
- yield* Ref.update(cache, (current) => {
381
- const updated = new Map(current);
382
- updated.set(registryUrl, loaded);
383
- return updated;
384
- });
385
- return loaded;
386
- }));
386
+ const cache = yield* Cache.makeWith((registryUrl) => store.load(registryUrl), {
387
+ capacity: Number.POSITIVE_INFINITY,
388
+ timeToLive: (exit) => Exit.isSuccess(exit) ? Duration.infinity : Duration.zero,
387
389
  });
388
390
  return {
389
391
  tier: store.tier,
390
392
  allowsPersistedCredentials: store.allowsPersistedCredentials,
391
- load,
392
- save: (registryUrl, handle, credentials) => store.save(registryUrl, handle, credentials).pipe(Effect.andThen(invalidate(registryUrl))),
393
- clear: (registryUrl) => store.clear(registryUrl).pipe(Effect.andThen(invalidate(registryUrl))),
393
+ load: (registryUrl) => Cache.get(cache, registryUrl).pipe(Effect.onInterrupt(() => Cache.invalidate(cache, registryUrl))),
394
+ save: (registryUrl, handle, credentials) => store
395
+ .save(registryUrl, handle, credentials)
396
+ .pipe(Effect.andThen(Cache.invalidate(cache, registryUrl))),
397
+ clear: (registryUrl) => store.clear(registryUrl).pipe(Effect.andThen(Cache.invalidate(cache, registryUrl))),
394
398
  };
395
399
  }));
396
400
  // -----------------------------------------------------------------------------
@@ -120,7 +120,7 @@ export declare const initiateDeviceLogin: (registryUrl: string, options?: RunDev
120
120
  readonly resume: string;
121
121
  };
122
122
  }, import("./errors.js").AuthError, AuthClient | CredentialStore | AuthLoginPresenter | PendingDeviceLoginStore | DeviceLoginInteraction>;
123
- export declare const resumeDeviceLogin: (registryUrl: string, options?: ResumeDeviceLoginOptions) => Effect.Effect<undefined, RegistryAuthFailed | import("./errors.js").AuthLoginRequired | import("./errors.js").AuthTokenPolicyRequired | DeviceAuthorizationPending | import("./errors.js").StepUpRequired | import("@agentxm/registry-client").RegistryClientFailure | import("./errors.js").AuthExchangeFailed | import("./errors.js").PublishAuthorizationPending, AuthClient | CredentialStore | AuthLoginPresenter | PendingDeviceLoginStore>;
124
- export declare const runDeviceLogin: (registryUrl: string, options?: RunDeviceLoginOptions) => Effect.Effect<void, RegistryAuthFailed | import("./errors.js").AuthLoginRequired | import("./errors.js").AuthTokenPolicyRequired | import("./errors.js").DeviceLoginDenied | import("./errors.js").DeviceLoginCodeExpired | DeviceAuthorizationPending | import("./errors.js").StepUpRequired | import("@agentxm/registry-client").RegistryProblem | import("@agentxm/registry-client").RegistryRequestFailed | import("@agentxm/registry-client").RegistryOperationFailed | import("./errors.js").AuthExchangeFailed | import("./errors.js").PublishAuthorizationPending, AuthClient | CredentialStore | AuthLoginPresenter | PendingDeviceLoginStore | DeviceLoginInteraction>;
123
+ export declare const resumeDeviceLogin: (registryUrl: string, options?: ResumeDeviceLoginOptions) => Effect.Effect<undefined, RegistryAuthFailed | import("./errors.js").AuthLoginRequired | import("./errors.js").AuthTokenPolicyRequired | DeviceAuthorizationPending | import("./errors.js").StepUpRequired | import("@agentxm/registry-client").RegistryClientFailure | import("./errors.js").AuthExchangeFailed | import("./errors.js").PublishAuthorizationPending | import("./errors.js").StepUpVerificationPending | import("./errors.js").AuthInteractionAbandoned, AuthClient | CredentialStore | AuthLoginPresenter | PendingDeviceLoginStore>;
124
+ export declare const runDeviceLogin: (registryUrl: string, options?: RunDeviceLoginOptions) => Effect.Effect<void, RegistryAuthFailed | import("./errors.js").AuthLoginRequired | import("./errors.js").AuthTokenPolicyRequired | import("./errors.js").DeviceLoginDenied | import("./errors.js").DeviceLoginCodeExpired | DeviceAuthorizationPending | import("./errors.js").StepUpRequired | import("@agentxm/registry-client").RegistryProblem | import("@agentxm/registry-client").RegistryRequestFailed | import("@agentxm/registry-client").RegistryOperationFailed | import("./errors.js").AuthExchangeFailed | import("./errors.js").PublishAuthorizationPending | import("./errors.js").StepUpVerificationPending | import("./errors.js").AuthInteractionAbandoned, AuthClient | CredentialStore | AuthLoginPresenter | PendingDeviceLoginStore | DeviceLoginInteraction>;
125
125
  export {};
126
126
  //# sourceMappingURL=device-login.d.ts.map
@@ -2,7 +2,7 @@ import type { HumanHandoffAction } from "@agentxm/registry-protocol/unstable/hum
2
2
  import { type RegistryClientFailure } from "@agentxm/registry-client";
3
3
  import type { SuggestedAction } from "@agentxm/registry-protocol/unstable/suggested-action";
4
4
  /** Every category a registry-auth failure can carry. Identical strings to the CLI error codes. */
5
- export declare const REGISTRY_AUTH_ERROR_CATEGORIES: readonly ["auth", "auth_denied", "auth_expired", "conflict", "internal", "not_found", "validation"];
5
+ export declare const REGISTRY_AUTH_ERROR_CATEGORIES: readonly ["auth", "auth_denied", "auth_expired", "conflict", "internal", "not_found", "timeout", "usage", "validation"];
6
6
  export type RegistryAuthErrorCategory = (typeof REGISTRY_AUTH_ERROR_CATEGORIES)[number];
7
7
  declare const RegistryAuthFailed_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
8
8
  readonly _tag: "RegistryAuthFailed";
@@ -129,8 +129,32 @@ export declare class PublishAuthorizationPending extends PublishAuthorizationPen
129
129
  readonly timedOut: boolean;
130
130
  }> {
131
131
  }
132
+ declare const StepUpVerificationPending_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
133
+ readonly _tag: "StepUpVerificationPending";
134
+ } & Readonly<A>;
135
+ /**
136
+ * A challenged Registry write is waiting on human verification. No challenged
137
+ * write has completed: the operation is retried exactly once, after the
138
+ * verification the carried handoff describes.
139
+ */
140
+ export declare class StepUpVerificationPending extends StepUpVerificationPending_base<{
141
+ readonly action: HumanHandoffAction;
142
+ readonly timedOut: boolean;
143
+ }> {
144
+ }
145
+ declare const AuthInteractionAbandoned_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
146
+ readonly _tag: "AuthInteractionAbandoned";
147
+ } & Readonly<A>;
148
+ /**
149
+ * A person abandoned an auth interaction the capability asked the application
150
+ * to run (declining is not abandoning: it returns a decision).
151
+ */
152
+ export declare class AuthInteractionAbandoned extends AuthInteractionAbandoned_base<{
153
+ readonly message: string;
154
+ }> {
155
+ }
132
156
  /** Every typed failure the registry-auth feature constructs. */
133
- export type RegistryAuthFailure = RegistryAuthFailed | AuthLoginRequired | AuthTokenPolicyRequired | DeviceLoginDenied | DeviceLoginCodeExpired | DeviceAuthorizationPending | PublishAuthorizationPending | StepUpRequired | AuthExchangeFailed;
157
+ export type RegistryAuthFailure = RegistryAuthFailed | AuthLoginRequired | AuthTokenPolicyRequired | DeviceLoginDenied | DeviceLoginCodeExpired | DeviceAuthorizationPending | PublishAuthorizationPending | StepUpVerificationPending | AuthInteractionAbandoned | StepUpRequired | AuthExchangeFailed;
134
158
  export declare const isRegistryAuthFailure: (error: unknown) => error is RegistryAuthFailure;
135
159
  /**
136
160
  * Every failure a registry-auth use case can surface: the feature's own typed
@@ -18,6 +18,8 @@ export const REGISTRY_AUTH_ERROR_CATEGORIES = [
18
18
  "conflict",
19
19
  "internal",
20
20
  "not_found",
21
+ "timeout",
22
+ "usage",
21
23
  "validation",
22
24
  ];
23
25
  /**
@@ -74,6 +76,19 @@ export class AuthExchangeFailed extends Data.TaggedError("AuthExchangeFailed") {
74
76
  }
75
77
  export class PublishAuthorizationPending extends Data.TaggedError("PublishAuthorizationPending") {
76
78
  }
79
+ /**
80
+ * A challenged Registry write is waiting on human verification. No challenged
81
+ * write has completed: the operation is retried exactly once, after the
82
+ * verification the carried handoff describes.
83
+ */
84
+ export class StepUpVerificationPending extends Data.TaggedError("StepUpVerificationPending") {
85
+ }
86
+ /**
87
+ * A person abandoned an auth interaction the capability asked the application
88
+ * to run (declining is not abandoning: it returns a decision).
89
+ */
90
+ export class AuthInteractionAbandoned extends Data.TaggedError("AuthInteractionAbandoned") {
91
+ }
77
92
  export const isRegistryAuthFailure = (error) => error instanceof RegistryAuthFailed ||
78
93
  error instanceof AuthLoginRequired ||
79
94
  error instanceof AuthTokenPolicyRequired ||
@@ -81,6 +96,8 @@ export const isRegistryAuthFailure = (error) => error instanceof RegistryAuthFai
81
96
  error instanceof DeviceLoginCodeExpired ||
82
97
  error instanceof DeviceAuthorizationPending ||
83
98
  error instanceof PublishAuthorizationPending ||
99
+ error instanceof StepUpVerificationPending ||
100
+ error instanceof AuthInteractionAbandoned ||
84
101
  error instanceof StepUpRequired ||
85
102
  error instanceof AuthExchangeFailed;
86
103
  export const isAuthError = (error) => isRegistryAuthFailure(error) || isRegistryClientFailure(error);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The authenticated identity of the selected Registry, and the credential
3
+ * lifecycle rule that governs reading it: a stored credential the Registry
4
+ * rejects is refreshed once and the read is retried, while a rejected ambient
5
+ * credential is reported as sign-in required.
6
+ *
7
+ * @experimental This API is unstable and may change without notice.
8
+ */
9
+ import type * as DateTime from "effect/DateTime";
10
+ import * as Effect from "effect/Effect";
11
+ import type { Handle } from "@agentxm/extension-model/unstable/extensions/handle";
12
+ import { AuthClient } from "./auth-client.js";
13
+ import { CredentialStore } from "./credential-store.js";
14
+ /** The Registry's canonical answer to "who is this credential". */
15
+ export interface RegistryIdentity {
16
+ readonly user: Handle;
17
+ readonly registry: string;
18
+ readonly credentialType: string;
19
+ readonly scopes: ReadonlyArray<string>;
20
+ readonly resourceRestrictions: {
21
+ readonly extensions: ReadonlyArray<string> | null;
22
+ };
23
+ readonly expiresAt: DateTime.Utc | null;
24
+ }
25
+ /**
26
+ * Read the canonical Registry identity for the resolved credential.
27
+ *
28
+ * Only a credential this workspace stores can be refreshed: an ambient token
29
+ * the Registry rejects is the caller's to replace, so it maps to sign-in
30
+ * required rather than a silent refresh attempt.
31
+ */
32
+ export declare const currentIdentity: (registryUrl: string) => Effect.Effect<{
33
+ user: string & import("effect/Brand").Brand<"Handle">;
34
+ registry: string;
35
+ credentialType: string;
36
+ scopes: readonly string[];
37
+ resourceRestrictions: {
38
+ readonly extensions: ReadonlyArray<string> | null;
39
+ };
40
+ expiresAt: DateTime.Utc | null;
41
+ }, import("./errors.js").AuthError, AuthClient | CredentialStore>;
42
+ /** The token an invocation would present to the selected Registry. */
43
+ export declare const currentToken: (registryUrl: string) => Effect.Effect<string, import("./errors.js").AuthError, CredentialStore>;
44
+ /** Both members keep `CredentialStore` in `R`; declared for the reader. */
45
+ export type IdentityRequirements = AuthClient | CredentialStore;
46
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The authenticated identity of the selected Registry, and the credential
3
+ * lifecycle rule that governs reading it: a stored credential the Registry
4
+ * rejects is refreshed once and the read is retried, while a rejected ambient
5
+ * credential is reported as sign-in required.
6
+ *
7
+ * @experimental This API is unstable and may change without notice.
8
+ */
9
+ import * as Effect from "effect/Effect";
10
+ import { isRegistryClientFailure } from "@agentxm/registry-client";
11
+ import { AuthClient } from "./auth-client.js";
12
+ import { CredentialStore } from "./credential-store.js";
13
+ import { authLoginRequired } from "./errors.js";
14
+ import { refreshStoredToken, resolveRequiredToken } from "./token-resolution.js";
15
+ const isRejectedCredential = (error) => isRegistryClientFailure(error) && error.metadata?.response?.status === 401;
16
+ /**
17
+ * Read the canonical Registry identity for the resolved credential.
18
+ *
19
+ * Only a credential this workspace stores can be refreshed: an ambient token
20
+ * the Registry rejects is the caller's to replace, so it maps to sign-in
21
+ * required rather than a silent refresh attempt.
22
+ */
23
+ export const currentIdentity = Effect.fn("Identity.current")(function* (registryUrl) {
24
+ const authClient = yield* AuthClient;
25
+ const token = yield* resolveRequiredToken(registryUrl, {
26
+ missingTokenError: authLoginRequired("Not authenticated"),
27
+ });
28
+ const identity = yield* authClient.getMe(token.token).pipe(Effect.catch((error) => token._tag === "CredentialStore" && isRejectedCredential(error)
29
+ ? refreshStoredToken(token).pipe(Effect.flatMap((refreshed) => authClient.getMe(refreshed.token)))
30
+ : Effect.fail(error)), Effect.mapError((error) => isRejectedCredential(error)
31
+ ? authLoginRequired("Invalid or expired credential. Authenticate again.", error)
32
+ : error));
33
+ return {
34
+ user: identity.userHandle,
35
+ registry: registryUrl,
36
+ credentialType: identity.tokenType,
37
+ scopes: identity.scopes,
38
+ resourceRestrictions: identity.resourceRestrictions,
39
+ expiresAt: identity.expiresAt,
40
+ };
41
+ });
42
+ /** The token an invocation would present to the selected Registry. */
43
+ export const currentToken = Effect.fn("Identity.currentToken")(function* (registryUrl) {
44
+ const token = yield* resolveRequiredToken(registryUrl, {
45
+ missingTokenError: authLoginRequired("No token available"),
46
+ });
47
+ return token.token;
48
+ });
49
+ //# sourceMappingURL=identity.js.map
@@ -10,7 +10,7 @@
10
10
  * @experimental This API is unstable and may change without notice.
11
11
  * @packageDocumentation
12
12
  */
13
- export { AuthExchangeFailed, AuthLoginRequired, AuthTokenPolicyRequired, authLoginRequired, DeviceAuthorizationPending, PublishAuthorizationPending, DeviceLoginCodeExpired, DeviceLoginDenied, isAuthError, isRegistryAuthFailure, REGISTRY_AUTH_ERROR_CATEGORIES, RegistryAuthFailed, StepUpRequired, type AuthError, type RegistryAuthErrorCategory, type RegistryAuthFailure, type StepUpRequest, } from "./errors.js";
13
+ export { AuthExchangeFailed, AuthLoginRequired, AuthTokenPolicyRequired, authLoginRequired, DeviceAuthorizationPending, PublishAuthorizationPending, DeviceLoginCodeExpired, DeviceLoginDenied, AuthInteractionAbandoned, isAuthError, isRegistryAuthFailure, REGISTRY_AUTH_ERROR_CATEGORIES, RegistryAuthFailed, StepUpRequired, StepUpVerificationPending, type AuthError, type RegistryAuthErrorCategory, type RegistryAuthFailure, type StepUpRequest, } from "./errors.js";
14
14
  export type { CredentialEntry, CredentialFile, StorageTier, StoredCredentials, TokenSource, } from "./schema.js";
15
15
  export { CredentialEntrySchema, CredentialFileSchema, CredentialStoreTokenSource, EnvVarTokenSource, FileTokenSource, FlagTokenSource, RegistryAccountsSchema, } from "./schema.js";
16
16
  export type { CredentialStoreService, EnvironmentInfo } from "./credential-store.js";
@@ -28,6 +28,15 @@ export { LoopbackCallbackRejected, LoopbackLoginFallback, startLoopbackServer, }
28
28
  export { LoginDocumentSchema, LoginResultSchema, makeLoginResult, type LoginDocument, type LoginResult, } from "./login-output.js";
29
29
  export type { AuthLoginPresenterService, AuthLoginProgress, DeviceFlowPresentation, } from "./login-presenter.js";
30
30
  export { AuthLoginPresenter } from "./login-presenter.js";
31
+ export type { DeviceCodeFallbackReason, SessionReplacementDecision, StepUpChallengePresentation, } from "./login-presenter.js";
32
+ export { runWithStepUp, type StepUpOptions, type StepUpPresentation, type VerifiedWrite, } from "./step-up.js";
33
+ export { selectedRegistry, type SelectedRegistry } from "./selected-registry.js";
34
+ export { classifyLoopbackFailure, deviceLoginOptions, login, resumeLoginOptions, type LoginOutcome, type LoginRequest, } from "./login.js";
35
+ export { logout, type LogoutOutcome } from "./logout.js";
36
+ export { currentIdentity, currentToken, type RegistryIdentity } from "./identity.js";
37
+ export { createToken, listTokens, parseExpiresInSeconds, revokeToken, tokenPermissions, validateExpiresInSeconds, MAX_TOKEN_LIFETIME_SECONDS, MIN_TOKEN_LIFETIME_SECONDS, type CreateTokenRequest, type CreatedToken, type TokenAuthorityRequest, } from "./tokens.js";
38
+ export { hasCredentialsForAll } from "./login-suggestion.js";
39
+ export { AuthEnvironment } from "./internal/environment.js";
31
40
  export { runPublishAuthorization, type PublishAuthorizationInput, } from "./publish-authorization.js";
32
41
  export { selectLoginStrategy, type LoginStrategy, type LoginStrategyEnvironment, type LoginStrategyOptions, } from "./login-strategy.js";
33
42
  export type { AuthLoginInteractionService } from "./login-interaction.js";
package/dist/src/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * @packageDocumentation
12
12
  */
13
13
  // Typed failures
14
- export { AuthExchangeFailed, AuthLoginRequired, AuthTokenPolicyRequired, authLoginRequired, DeviceAuthorizationPending, PublishAuthorizationPending, DeviceLoginCodeExpired, DeviceLoginDenied, isAuthError, isRegistryAuthFailure, REGISTRY_AUTH_ERROR_CATEGORIES, RegistryAuthFailed, StepUpRequired, } from "./errors.js";
14
+ export { AuthExchangeFailed, AuthLoginRequired, AuthTokenPolicyRequired, authLoginRequired, DeviceAuthorizationPending, PublishAuthorizationPending, DeviceLoginCodeExpired, DeviceLoginDenied, AuthInteractionAbandoned, isAuthError, isRegistryAuthFailure, REGISTRY_AUTH_ERROR_CATEGORIES, RegistryAuthFailed, StepUpRequired, StepUpVerificationPending, } from "./errors.js";
15
15
  export { CredentialEntrySchema, CredentialFileSchema, CredentialStoreTokenSource, EnvVarTokenSource, FileTokenSource, FlagTokenSource, RegistryAccountsSchema, } from "./schema.js";
16
16
  export { canUsePersistedCredentials, CredentialStore, detectEnvironment, makePersistedCredentialsUnsupportedError, selectTier, } from "./credential-store.js";
17
17
  export { PendingDeviceLoginSchema, PendingDeviceLoginStore } from "./pending-device-login-store.js";
@@ -23,6 +23,18 @@ export { makeOAuthState, makePkceChallenge, makePkceVerifier, runLoopbackLogin,
23
23
  export { LoopbackCallbackRejected, LoopbackLoginFallback, startLoopbackServer, } from "./loopback-server.js";
24
24
  export { LoginDocumentSchema, LoginResultSchema, makeLoginResult, } from "./login-output.js";
25
25
  export { AuthLoginPresenter } from "./login-presenter.js";
26
+ // Step-up verification protocol
27
+ export { runWithStepUp, } from "./step-up.js";
28
+ // The Registry this invocation authenticates against
29
+ export { selectedRegistry } from "./selected-registry.js";
30
+ // Sign-in, sign-out, identity, and token policy use cases
31
+ export { classifyLoopbackFailure, deviceLoginOptions, login, resumeLoginOptions, } from "./login.js";
32
+ export { logout } from "./logout.js";
33
+ export { currentIdentity, currentToken } from "./identity.js";
34
+ export { createToken, listTokens, parseExpiresInSeconds, revokeToken, tokenPermissions, validateExpiresInSeconds, MAX_TOKEN_LIFETIME_SECONDS, MIN_TOKEN_LIFETIME_SECONDS, } from "./tokens.js";
35
+ export { hasCredentialsForAll } from "./login-suggestion.js";
36
+ // Environment source for auth policy decisions (default: the process environment)
37
+ export { AuthEnvironment } from "./internal/environment.js";
26
38
  export { runPublishAuthorization, } from "./publish-authorization.js";
27
39
  export { selectLoginStrategy, } from "./login-strategy.js";
28
40
  export { AuthLoginInteraction } from "./login-interaction.js";
@@ -1,16 +1,27 @@
1
1
  /**
2
2
  * Runtime environment detection for credential policy and login flows.
3
3
  *
4
- * Pure functions for env var / process checks. Effect-based for filesystem
5
- * checks. Mirrors the shared helpers the extracted kernels carry; a generic
6
- * utils package is deliberately not created.
4
+ * Environment values are read through Effect's `ConfigProvider`, whose default
5
+ * is the process environment. Tests and embedders replace the provider instead
6
+ * of mutating `process.env`, so no auth policy specification stubs globals.
7
7
  */
8
+ import * as ConfigProvider from "effect/ConfigProvider";
9
+ import * as ServiceMap from "effect/Context";
8
10
  import * as FileSystem from "effect/FileSystem";
9
11
  import * as Effect from "effect/Effect";
10
12
  import * as Option from "effect/Option";
11
- /** Read an optional env var. Centralized access point for process.env. */
13
+ /**
14
+ * The configuration source every auth environment decision reads. Defaults to
15
+ * the process environment; tests and embedders provide any `ConfigProvider`
16
+ * — `ConfigProvider.fromEnvRecord({ ... })` — instead of mutating globals.
17
+ */
18
+ export declare const AuthEnvironment: ServiceMap.Reference<ConfigProvider.ConfigProvider>;
19
+ /**
20
+ * Read an optional environment value. The single access point: every auth
21
+ * environment decision resolves through the ambient `ConfigProvider`.
22
+ */
12
23
  export declare const envOption: (name: string) => Effect.Effect<Option.Option<string>>;
13
- /** Returns true if SSH_CLIENT or SSH_TTY env var is set. */
24
+ /** Returns true if SSH_CLIENT or SSH_TTY is set. */
14
25
  export declare const isSSH: Effect.Effect<boolean>;
15
26
  /** Returns true if running as root (uid 0). */
16
27
  export declare const isRoot: () => boolean;
@@ -20,4 +31,11 @@ export declare const isContainer: Effect.Effect<boolean, never, FileSystem.FileS
20
31
  export declare const isWSL: Effect.Effect<boolean, never, FileSystem.FileSystem>;
21
32
  /** Returns true if CI env var is set. */
22
33
  export declare const isCI: Effect.Effect<boolean>;
34
+ /**
35
+ * The environment facts login-strategy selection reads. Assembled here so the
36
+ * strategy decision never touches globals.
37
+ */
38
+ export declare const loginStrategyEnvironment: Effect.Effect<{
39
+ [k: string]: string;
40
+ }, never, never>;
23
41
  //# sourceMappingURL=environment.d.ts.map
@@ -1,19 +1,53 @@
1
1
  /**
2
2
  * Runtime environment detection for credential policy and login flows.
3
3
  *
4
- * Pure functions for env var / process checks. Effect-based for filesystem
5
- * checks. Mirrors the shared helpers the extracted kernels carry; a generic
6
- * utils package is deliberately not created.
4
+ * Environment values are read through Effect's `ConfigProvider`, whose default
5
+ * is the process environment. Tests and embedders replace the provider instead
6
+ * of mutating `process.env`, so no auth policy specification stubs globals.
7
7
  */
8
+ import * as ConfigProvider from "effect/ConfigProvider";
9
+ import * as ServiceMap from "effect/Context";
8
10
  import * as FileSystem from "effect/FileSystem";
9
11
  import * as Effect from "effect/Effect";
10
12
  import * as Option from "effect/Option";
11
- // eslint-disable-next-line no-restricted-properties -- Centralized env var access point; all callers use these helpers
12
- const readEnv = (name) => process.env[name];
13
- /** Read an optional env var. Centralized access point for process.env. */
14
- export const envOption = (name) => Effect.sync(() => Option.fromUndefinedOr(readEnv(name)));
15
- /** Returns true if SSH_CLIENT or SSH_TTY env var is set. */
16
- export const isSSH = Effect.sync(() => readEnv("SSH_CLIENT") !== undefined || readEnv("SSH_TTY") !== undefined);
13
+ /**
14
+ * Reads the live process environment on every lookup. Effect's own
15
+ * `ConfigProvider` default snapshots the environment the first time any
16
+ * program reads configuration; auth reads must observe the environment the
17
+ * invocation actually runs under.
18
+ */
19
+ const processEnvironmentProvider = ConfigProvider.make((path) =>
20
+ // eslint-disable-next-line no-restricted-properties -- the one process-environment read in registry-auth
21
+ ConfigProvider.fromEnvRecord(process.env).load(path));
22
+ /**
23
+ * The configuration source every auth environment decision reads. Defaults to
24
+ * the process environment; tests and embedders provide any `ConfigProvider`
25
+ * — `ConfigProvider.fromEnvRecord({ ... })` — instead of mutating globals.
26
+ */
27
+ export const AuthEnvironment = ServiceMap.Reference("@agentxm/registry-auth/AuthEnvironment", {
28
+ defaultValue: () => processEnvironmentProvider,
29
+ });
30
+ /**
31
+ * The raw string a provider node carries. A `Record` or `Array` node still
32
+ * carries the value of the exact key when the environment also defines
33
+ * longer keys under it (`AXM_TOKEN` beside `AXM_TOKEN_FILE`).
34
+ */
35
+ const nodeValue = (node) => node === undefined
36
+ ? Option.none()
37
+ : node._tag === "Value"
38
+ ? Option.some(node.value)
39
+ : Option.fromUndefinedOr(node.value);
40
+ /**
41
+ * Read an optional environment value. The single access point: every auth
42
+ * environment decision resolves through the ambient `ConfigProvider`.
43
+ */
44
+ export const envOption = (name) => Effect.gen(function* () {
45
+ const provider = yield* AuthEnvironment;
46
+ const node = yield* provider.load([name]).pipe(Effect.orElseSucceed(() => undefined));
47
+ return nodeValue(node);
48
+ });
49
+ /** Returns true if SSH_CLIENT or SSH_TTY is set. */
50
+ export const isSSH = Effect.map(Effect.all([envOption("SSH_CLIENT"), envOption("SSH_TTY")]), ([client, tty]) => Option.isSome(client) || Option.isSome(tty));
17
51
  /** Returns true if running as root (uid 0). */
18
52
  export const isRoot = () => process.getuid?.() === 0;
19
53
  /** Returns true if /.dockerenv or /.containerenv exists. Requires FileSystem. */
@@ -42,4 +76,20 @@ export const isWSL = Effect.gen(function* () {
42
76
  });
43
77
  /** Returns true if CI env var is set. */
44
78
  export const isCI = Effect.map(envOption("CI"), (value) => Option.exists(value, (raw) => raw.length > 0 && raw !== "0" && raw.toLowerCase() !== "false"));
79
+ /**
80
+ * The environment facts login-strategy selection reads. Assembled here so the
81
+ * strategy decision never touches globals.
82
+ */
83
+ export const loginStrategyEnvironment = Effect.gen(function* () {
84
+ const env = yield* Effect.all({
85
+ SSH_CONNECTION: envOption("SSH_CONNECTION"),
86
+ SSH_CLIENT: envOption("SSH_CLIENT"),
87
+ SSH_TTY: envOption("SSH_TTY"),
88
+ DISPLAY: envOption("DISPLAY"),
89
+ WAYLAND_DISPLAY: envOption("WAYLAND_DISPLAY"),
90
+ CI: envOption("CI"),
91
+ CODESPACES: envOption("CODESPACES"),
92
+ });
93
+ return Object.fromEntries(Object.entries(env).flatMap(([key, value]) => Option.isSome(value) ? [[key, value.value]] : []));
94
+ });
45
95
  //# sourceMappingURL=environment.js.map