@agentxm/registry-auth 0.28.11 → 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.
Files changed (42) hide show
  1. package/dist/src/auth-client.d.ts +13 -5
  2. package/dist/src/auth-client.js +80 -39
  3. package/dist/src/credential-store.d.ts +39 -2
  4. package/dist/src/credential-store.js +50 -46
  5. package/dist/src/device-login.d.ts +20 -8
  6. package/dist/src/device-login.js +9 -4
  7. package/dist/src/errors.d.ts +37 -12
  8. package/dist/src/errors.js +20 -0
  9. package/dist/src/identity.d.ts +46 -0
  10. package/dist/src/identity.js +49 -0
  11. package/dist/src/index.d.ts +12 -2
  12. package/dist/src/index.js +14 -1
  13. package/dist/src/internal/environment.d.ts +23 -5
  14. package/dist/src/internal/environment.js +59 -9
  15. package/dist/src/live.d.ts +1 -0
  16. package/dist/src/live.js +1 -0
  17. package/dist/src/login-presenter.d.ts +55 -0
  18. package/dist/src/login-presenter.js +22 -0
  19. package/dist/src/login-suggestion.d.ts +18 -0
  20. package/dist/src/login-suggestion.js +34 -0
  21. package/dist/src/login.d.ts +80 -0
  22. package/dist/src/login.js +155 -0
  23. package/dist/src/logout.d.ts +35 -0
  24. package/dist/src/logout.js +37 -0
  25. package/dist/src/loopback-login.js +2 -1
  26. package/dist/src/loopback-server.d.ts +3 -1
  27. package/dist/src/loopback-server.js +51 -7
  28. package/dist/src/pending-publish-authorization-store.d.ts +33 -0
  29. package/dist/src/pending-publish-authorization-store.js +90 -0
  30. package/dist/src/publish-authorization-polling.d.ts +13 -0
  31. package/dist/src/publish-authorization-polling.js +176 -0
  32. package/dist/src/publish-authorization.d.ts +4 -1
  33. package/dist/src/publish-authorization.js +24 -5
  34. package/dist/src/selected-registry.d.ts +25 -0
  35. package/dist/src/selected-registry.js +18 -0
  36. package/dist/src/step-up.d.ts +49 -0
  37. package/dist/src/step-up.js +174 -0
  38. package/dist/src/testing.d.ts +1 -0
  39. package/dist/src/testing.js +1 -0
  40. package/dist/src/tokens.d.ts +57 -0
  41. package/dist/src/tokens.js +78 -0
  42. package/package.json +7 -6
@@ -1,3 +1,4 @@
1
+ import type { PublishAuthorizationDelivery, PublishAuthorizationPollingStatus } from "@agentxm/registry-protocol/unstable/publish-authorization";
1
2
  /**
2
3
  * AuthClient Effect service — device flow login, token refresh, revocation, identity queries.
3
4
  *
@@ -16,10 +17,10 @@ import * as ServiceMap from "effect/Context";
16
17
  import * as Effect from "effect/Effect";
17
18
  import * as Layer from "effect/Layer";
18
19
  import { type Handle } from "@agentxm/extension-model/unstable/extensions/handle";
19
- import { type PublishVisibility } from "@agentxm/registry-protocol/unstable/publish/visibility";
20
+ import { type PublishVisibility } from "@agentxm/registry-protocol/unstable/publish";
20
21
  import { type PreviewPublicationSetRequest, type PreviewPublicationSetResponse, type Sha256Hex } from "@agentxm/registry-protocol/unstable/registry/publication-set";
21
22
  import { type NormalizedTokenResponse } from "./oauth-contract.js";
22
- import { RegistryUrl } from "@agentxm/registry-client";
23
+ import { GeneratedRegistryClient, RegistryUrl } from "@agentxm/registry-client";
23
24
  import { type AuthError, type StepUpRequest } from "./errors.js";
24
25
  export declare const OIDC_LOGIN_SCOPES: readonly ["openid", "profile", "email", "offline_access"];
25
26
  export declare const BASELINE_REGISTRY_LOGIN_SCOPES: readonly ["extensions:read", "account:read"];
@@ -102,16 +103,20 @@ export interface ExchangePkceCodeParams {
102
103
  }
103
104
  export interface CreatePublishAuthorizationRequestParams {
104
105
  readonly registryUrl: string;
105
- readonly redirectUri: string;
106
- readonly state: string;
107
- readonly codeChallenge: string;
106
+ readonly delivery: PublishAuthorizationDelivery;
108
107
  readonly publicationSet: PreviewPublicationSetRequest;
109
108
  }
110
109
  export interface PublishAuthorizationRequestResponse {
110
+ readonly interval: number;
111
111
  readonly requestId: string;
112
112
  readonly authorizationUrl: string;
113
113
  readonly expiresAt: DateTime.Utc;
114
114
  }
115
+ export interface PublishAuthorizationPollingParams {
116
+ readonly registryUrl: string;
117
+ readonly requestId: string;
118
+ readonly initiatorProof: string;
119
+ }
115
120
  export interface ExchangePublishAuthorizationCodeParams {
116
121
  readonly registryUrl: string;
117
122
  readonly code: string;
@@ -156,6 +161,8 @@ export interface AuthClientService {
156
161
  readonly getAuthorizationIssuer: () => string;
157
162
  readonly exchangePkceCode: (params: ExchangePkceCodeParams) => Effect.Effect<NormalizedTokenResponse, AuthError>;
158
163
  readonly createPublishAuthorizationRequest: (params: CreatePublishAuthorizationRequestParams) => Effect.Effect<PublishAuthorizationRequestResponse, AuthError>;
164
+ readonly pollPublishAuthorization: (params: PublishAuthorizationPollingParams) => Effect.Effect<PublishAuthorizationPollingStatus, AuthError>;
165
+ readonly exchangePublishAuthorization: (params: PublishAuthorizationPollingParams) => Effect.Effect<PublishAuthorizationExchangeResponse, AuthError>;
159
166
  readonly exchangePublishAuthorizationCode: (params: ExchangePublishAuthorizationCodeParams) => Effect.Effect<PublishAuthorizationExchangeResponse, AuthError>;
160
167
  readonly initiateDeviceFlow: (options?: LoginScopeOptions) => Effect.Effect<DeviceFlowResponse, AuthError>;
161
168
  readonly pollDeviceToken: (deviceCode: string, interval: number) => Effect.Effect<NormalizedTokenResponse, AuthError>;
@@ -167,6 +174,7 @@ export interface AuthClientService {
167
174
  readonly limit?: number;
168
175
  readonly cursor?: string;
169
176
  }) => Effect.Effect<TokenListResponse, AuthError>;
177
+ readonly getStepUpRequest: (accessToken: string, requestId: string) => Effect.Effect<GeneratedRegistryClient.StepUpRequestStatusResponse, AuthError>;
170
178
  readonly waitForStepUpRequest: (accessToken: string, statusUrl: string, intervalSeconds: number) => Effect.Effect<void, AuthError>;
171
179
  readonly deleteToken: (accessToken: string, tokenId: string, options?: DeleteTokenOptions) => Effect.Effect<void, AuthError>;
172
180
  }
@@ -23,10 +23,10 @@ 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
- import { GeneratedRegistryClient, RegistryUrl, captureRegistryErrorResponseBodies, getString, isHttpClientError, isRegistryClientError, isRegistryClientFailure, isTransientHttpClientError, mapRegistryFailure, } from "@agentxm/registry-client";
29
+ import { GeneratedRegistryClient, executeRegistryRequest, RegistryUrl, captureRegistryErrorResponseBodies, getString, isHttpClientError, isRegistryClientError, isRegistryClientFailure, isTransientHttpClientError, mapRegistryFailure, } from "@agentxm/registry-client";
30
30
  import { AuthExchangeFailed, DeviceLoginCodeExpired, DeviceLoginDenied, RegistryAuthFailed, StepUpRequired, } from "./errors.js";
31
31
  // -----------------------------------------------------------------------------
32
32
  // Constants
@@ -93,6 +93,29 @@ const PublishAuthorizationExchangeResponseSchema = Schema.Union([
93
93
  grants: Schema.Tuple([]),
94
94
  }),
95
95
  ]);
96
+ const decodePublishAuthorizationExchange = Effect.fn("AuthClient.decodePublishAuthorizationExchange")(function* (registryUrl, response) {
97
+ if (!Schema.is(PublishAuthorizationExchangeResponseSchema)(response)) {
98
+ return yield* mapRegistryAuthError(registryUrl, "The Registry is incompatible with exact publish authorization", new Error("Invalid publish capability response"));
99
+ }
100
+ if (response.status === "blocked") {
101
+ return response;
102
+ }
103
+ return {
104
+ status: "admitted",
105
+ preview: response.preview,
106
+ grants: response.grants.map((grant) => ({
107
+ accessToken: grant.access_token,
108
+ expiresAt: grant.expires_at,
109
+ scope: grant.scope,
110
+ publishRequestId: grant.publish_request_id,
111
+ visibilityContract: grant.visibility_contract,
112
+ visibility: grant.visibility,
113
+ condition: grant.condition,
114
+ publicationSetDigest: grant.publication_set_digest,
115
+ publicationDescriptorDigest: grant.publication_descriptor_digest,
116
+ })),
117
+ };
118
+ });
96
119
  const deriveAuthorizationOrigin = (registryUrl) => {
97
120
  const url = new URL(registryUrl);
98
121
  if (url.origin === "https://registry.agentxm.ai") {
@@ -101,14 +124,13 @@ const deriveAuthorizationOrigin = (registryUrl) => {
101
124
  if (url.origin === "https://registry-dev.agentxm.ai") {
102
125
  return "https://web-dev.agentxm.ai";
103
126
  }
104
- if (url.host === "localhost:4300") {
105
- return "http://localhost:4200";
106
- }
107
- if (url.host === "127.0.0.1:4300") {
108
- return "http://127.0.0.1:4200";
109
- }
110
- if (url.hostname === "127.0.0.1") {
111
- return `${url.protocol}//${url.hostname}:4200`;
127
+ const localHosts = ["localhost", "127.0.0.1", "[::1]"];
128
+ const registryPort = Number(url.port);
129
+ if (localHosts.some((host) => host === url.hostname) &&
130
+ Number.isInteger(registryPort) &&
131
+ registryPort >= 4300 &&
132
+ registryPort <= 4399) {
133
+ return `${url.protocol}//${url.hostname}:${String(registryPort - 100)}`;
112
134
  }
113
135
  return url.origin;
114
136
  };
@@ -298,24 +320,54 @@ export const AuthClientLive = Layer.effect(AuthClient, Effect.gen(function* () {
298
320
  const createPublishAuthorizationRequest = Effect.fn("AuthClient.createPublishAuthorizationRequest")(function* (params) {
299
321
  const publishClient = makeGeneratedAuthClient(httpClient, params.registryUrl);
300
322
  const publicationSet = yield* Schema.encodeUnknownEffect(GeneratedRegistryClient.PreviewPublicationSetRequest)(params.publicationSet).pipe(Effect.mapError((error) => mapRegistryAuthError(params.registryUrl, "Could not encode publish authorization request", error)));
301
- const response = yield* publishClient
302
- .AuthCreatePublishAuthorizationRequest({
323
+ const response = yield* executeRegistryRequest(publishClient.AuthCreatePublishAuthorizationRequest({
303
324
  payload: {
304
325
  client_id: CLIENT_ID,
305
- redirect_uri: params.redirectUri,
306
- state: params.state,
307
- code_challenge: params.codeChallenge,
308
- code_challenge_method: "S256",
326
+ delivery: params.delivery,
309
327
  publication_set: publicationSet,
310
328
  },
311
- })
312
- .pipe(Effect.mapError((error) => mapRegistryAuthError(params.registryUrl, "Could not create publish authorization request", error)));
329
+ }), {
330
+ operation: "createPublishAuthorizationRequest",
331
+ request: {
332
+ service: "registry",
333
+ method: "POST",
334
+ url: `${params.registryUrl.replace(/\/+$/, "")}/v1/auth/publish-requests`,
335
+ },
336
+ replaySafety: { kind: "mutation" },
337
+ mapError: (error) => mapRegistryAuthError(params.registryUrl, "Could not create publish authorization request; no automatic replacement was attempted", error),
338
+ });
313
339
  return {
340
+ interval: response.interval,
314
341
  requestId: response.request_id,
315
342
  authorizationUrl: response.authorization_url,
316
343
  expiresAt: response.expires_at,
317
344
  };
318
345
  });
346
+ const pollPublishAuthorization = Effect.fn("AuthClient.pollPublishAuthorization")(function* (params) {
347
+ const publishClient = makeGeneratedAuthClient(httpClient, params.registryUrl);
348
+ const requestRef = `${params.registryUrl.replace(/\/+$/, "")}/v1/auth/publish-requests/${params.requestId}`;
349
+ return yield* executeRegistryRequest(publishClient.AuthPollPublishAuthorization(params.requestId, {
350
+ payload: { initiator_proof: params.initiatorProof },
351
+ }), {
352
+ operation: "pollPublishAuthorization",
353
+ request: { service: "registry", method: "POST", url: `${requestRef}/status` },
354
+ replaySafety: { kind: "safe" },
355
+ mapError: (error) => mapRegistryAuthError(params.registryUrl, `Could not read publish authorization status. Resume the same command with --authorization-request ${requestRef}`, error),
356
+ });
357
+ });
358
+ const exchangePublishAuthorization = Effect.fn("AuthClient.exchangePublishAuthorization")(function* (params) {
359
+ const publishClient = makeGeneratedAuthClient(httpClient, params.registryUrl);
360
+ const requestRef = `${params.registryUrl.replace(/\/+$/, "")}/v1/auth/publish-requests/${params.requestId}`;
361
+ const response = yield* executeRegistryRequest(publishClient.AuthExchangePublishAuthorization(params.requestId, {
362
+ payload: { initiator_proof: params.initiatorProof },
363
+ }), {
364
+ operation: "exchangePublishAuthorization",
365
+ request: { service: "registry", method: "POST", url: `${requestRef}/exchange` },
366
+ replaySafety: { kind: "mutation" },
367
+ mapError: (error) => mapRegistryAuthError(params.registryUrl, `Publish authorization exchange did not complete. Resume with --authorization-request ${requestRef} to check its status before requesting new consent`, error),
368
+ });
369
+ return yield* decodePublishAuthorizationExchange(params.registryUrl, response);
370
+ });
319
371
  const exchangePublishAuthorizationCode = Effect.fn("AuthClient.exchangePublishAuthorizationCode")(function* (params) {
320
372
  const publishClient = makeGeneratedAuthClient(httpClient, params.registryUrl);
321
373
  const response = yield* publishClient
@@ -345,27 +397,7 @@ export const AuthClientLive = Layer.effect(AuthClient, Effect.gen(function* () {
345
397
  })
346
398
  : mapped;
347
399
  }));
348
- if (!Schema.is(PublishAuthorizationExchangeResponseSchema)(response)) {
349
- return yield* mapRegistryAuthError(params.registryUrl, "The Registry is incompatible with exact publish authorization", response);
350
- }
351
- if (response.status === "blocked") {
352
- return response;
353
- }
354
- return {
355
- status: "admitted",
356
- preview: response.preview,
357
- grants: response.grants.map((grant) => ({
358
- accessToken: grant.access_token,
359
- expiresAt: grant.expires_at,
360
- scope: grant.scope,
361
- publishRequestId: grant.publish_request_id,
362
- visibilityContract: grant.visibility_contract,
363
- visibility: grant.visibility,
364
- condition: grant.condition,
365
- publicationSetDigest: grant.publication_set_digest,
366
- publicationDescriptorDigest: grant.publication_descriptor_digest,
367
- })),
368
- };
400
+ return yield* decodePublishAuthorizationExchange(params.registryUrl, response);
369
401
  });
370
402
  const initiateDeviceFlow = Effect.fn("AuthClient.initiateDeviceFlow")(function* (options) {
371
403
  const response = yield* client
@@ -496,6 +528,9 @@ export const AuthClientLive = Layer.effect(AuthClient, Effect.gen(function* () {
496
528
  cursor: decoded.cursor,
497
529
  };
498
530
  });
531
+ const getStepUpRequest = (accessToken, requestId) => makeGeneratedAuthClient(httpClient, registryUrl, accessToken)
532
+ .AuthGetStepUpRequest(requestId, undefined)
533
+ .pipe(Effect.mapError((error) => mapRegistryAuthError(registryUrl, "Could not read step-up request", error)));
499
534
  const waitForStepUpRequest = Effect.fn("AuthClient.waitForStepUpRequest")(function* (accessToken, statusUrl, intervalSeconds) {
500
535
  const parsedStatusUrl = new URL(statusUrl);
501
536
  const requestId = parsedStatusUrl.pathname.slice(parsedStatusUrl.pathname.lastIndexOf("/") + 1);
@@ -558,6 +593,8 @@ export const AuthClientLive = Layer.effect(AuthClient, Effect.gen(function* () {
558
593
  getAuthorizationIssuer,
559
594
  exchangePkceCode,
560
595
  createPublishAuthorizationRequest,
596
+ pollPublishAuthorization,
597
+ exchangePublishAuthorization,
561
598
  exchangePublishAuthorizationCode,
562
599
  initiateDeviceFlow,
563
600
  pollDeviceToken,
@@ -566,6 +603,7 @@ export const AuthClientLive = Layer.effect(AuthClient, Effect.gen(function* () {
566
603
  getMe,
567
604
  createToken,
568
605
  listTokens,
606
+ getStepUpRequest,
569
607
  waitForStepUpRequest,
570
608
  deleteToken,
571
609
  };
@@ -584,6 +622,8 @@ export const AuthClientTest = (overrides) => Layer.succeed(AuthClient, {
584
622
  category: "auth",
585
623
  detail: "Not implemented in test",
586
624
  })),
625
+ pollPublishAuthorization: () => Effect.fail(new RegistryAuthFailed({ category: "auth", detail: "Not implemented in test" })),
626
+ exchangePublishAuthorization: () => Effect.fail(new RegistryAuthFailed({ category: "auth", detail: "Not implemented in test" })),
587
627
  exchangePublishAuthorizationCode: () => Effect.fail(new RegistryAuthFailed({
588
628
  category: "auth",
589
629
  detail: "Not implemented in test",
@@ -613,6 +653,7 @@ export const AuthClientTest = (overrides) => Layer.succeed(AuthClient, {
613
653
  category: "auth",
614
654
  detail: "Not implemented in test",
615
655
  })),
656
+ getStepUpRequest: () => Effect.fail(new RegistryAuthFailed({ category: "auth", detail: "Not implemented in test" })),
616
657
  waitForStepUpRequest: () => Effect.fail(new RegistryAuthFailed({
617
658
  category: "auth",
618
659
  detail: "Not implemented in test",
@@ -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
  // -----------------------------------------------------------------------------
@@ -53,11 +53,15 @@ export declare const DeviceLoginPendingResultSchema: Schema.Struct<{
53
53
  readonly interval: Schema.Number;
54
54
  readonly resume: Schema.String;
55
55
  readonly action: Schema.Struct<{
56
- readonly kind: Schema.Literal<"open-url">;
57
- readonly url: Schema.String;
56
+ readonly purpose: Schema.Literal<"login">;
58
57
  readonly fallbackUrl: Schema.String;
59
58
  readonly code: Schema.String;
59
+ readonly kind: Schema.Literal<"open-url">;
60
+ readonly requestRef: Schema.String;
61
+ readonly registryUrl: Schema.String;
62
+ readonly url: Schema.String;
60
63
  readonly expiresAt: Schema.String;
64
+ readonly intervalSeconds: Schema.Number;
61
65
  readonly resume: Schema.String;
62
66
  }>;
63
67
  }>;
@@ -77,11 +81,15 @@ export declare const DeviceLoginPendingDocumentSchema: Schema.Struct<{
77
81
  readonly interval: Schema.Number;
78
82
  readonly resume: Schema.String;
79
83
  readonly action: Schema.Struct<{
80
- readonly kind: Schema.Literal<"open-url">;
81
- readonly url: Schema.String;
84
+ readonly purpose: Schema.Literal<"login">;
82
85
  readonly fallbackUrl: Schema.String;
83
86
  readonly code: Schema.String;
87
+ readonly kind: Schema.Literal<"open-url">;
88
+ readonly requestRef: Schema.String;
89
+ readonly registryUrl: Schema.String;
90
+ readonly url: Schema.String;
84
91
  readonly expiresAt: Schema.String;
92
+ readonly intervalSeconds: Schema.Number;
85
93
  readonly resume: Schema.String;
86
94
  }>;
87
95
  }>;
@@ -100,15 +108,19 @@ export declare const initiateDeviceLogin: (registryUrl: string, options?: RunDev
100
108
  readonly interval: number;
101
109
  readonly resume: string;
102
110
  readonly action: {
103
- readonly kind: "open-url";
104
- readonly url: string;
111
+ readonly purpose: "login";
105
112
  readonly fallbackUrl: string;
106
113
  readonly code: string;
114
+ readonly kind: "open-url";
115
+ readonly requestRef: string;
116
+ readonly registryUrl: string;
117
+ readonly url: string;
107
118
  readonly expiresAt: string;
119
+ readonly intervalSeconds: number;
108
120
  readonly resume: string;
109
121
  };
110
122
  }, import("./errors.js").AuthError, AuthClient | CredentialStore | AuthLoginPresenter | PendingDeviceLoginStore | DeviceLoginInteraction>;
111
- 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, AuthClient | CredentialStore | AuthLoginPresenter | PendingDeviceLoginStore>;
112
- 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, 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>;
113
125
  export {};
114
126
  //# sourceMappingURL=device-login.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { HumanHandoffActionSchema } from "@agentxm/registry-protocol/unstable/human-handoff";
1
2
  /**
2
3
  * Shared device-code login flow for auth commands and guards.
3
4
  *
@@ -58,12 +59,10 @@ const persistLoginCredentials = (registryUrl, token) => Effect.gen(function* ()
58
59
  return Option.map(meResult, (me) => me.userHandle);
59
60
  });
60
61
  const DeviceLoginActionSchema = Schema.Struct({
61
- kind: Schema.Literal("open-url"),
62
- url: Schema.String,
62
+ ...HumanHandoffActionSchema.fields,
63
+ purpose: Schema.Literal("login"),
63
64
  fallbackUrl: Schema.String,
64
65
  code: Schema.String,
65
- expiresAt: Schema.String,
66
- resume: Schema.String,
67
66
  });
68
67
  export const DeviceLoginPendingResultSchema = Schema.Struct({
69
68
  status: Schema.Literal("pending-human"),
@@ -119,6 +118,10 @@ const makePendingResult = (pending, flow = "started") => {
119
118
  resume,
120
119
  action: {
121
120
  kind: "open-url",
121
+ purpose: "login",
122
+ requestRef: pending.verificationUriComplete,
123
+ registryUrl: pending.registryUrl,
124
+ intervalSeconds: pending.interval,
122
125
  url: pending.verificationUriComplete,
123
126
  fallbackUrl: pending.verificationUri,
124
127
  code: pending.userCode,
@@ -258,6 +261,8 @@ export const resumeDeviceLogin = (registryUrl, options = {}) => Effect.gen(funct
258
261
  const action = makePendingResult(pending).action;
259
262
  return Effect.fail(new DeviceAuthorizationPending({
260
263
  timeoutSeconds,
264
+ registryUrl: pending.registryUrl,
265
+ intervalSeconds: pending.interval,
261
266
  verificationUri: action.fallbackUrl,
262
267
  verificationUriComplete: action.url,
263
268
  userCode: action.code,
@@ -1,17 +1,8 @@
1
- /**
2
- * Typed failures for the registry-auth feature. The producer owns the
3
- * category choice and user-facing wording; the application boundary converts
4
- * the carried fields into its error envelope verbatim. Failures that carry a
5
- * registry transport failure keep it intact so the boundary can restore the
6
- * exact evidence, metadata, and semantics the former in-place conversion
7
- * produced.
8
- *
9
- * @experimental This API is unstable and may change without notice.
10
- */
1
+ import type { HumanHandoffAction } from "@agentxm/registry-protocol/unstable/human-handoff";
11
2
  import { type RegistryClientFailure } from "@agentxm/registry-client";
12
3
  import type { SuggestedAction } from "@agentxm/registry-protocol/unstable/suggested-action";
13
4
  /** Every category a registry-auth failure can carry. Identical strings to the CLI error codes. */
14
- 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"];
15
6
  export type RegistryAuthErrorCategory = (typeof REGISTRY_AUTH_ERROR_CATEGORIES)[number];
16
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 & {
17
8
  readonly _tag: "RegistryAuthFailed";
@@ -80,6 +71,8 @@ declare const DeviceAuthorizationPending_base: new <A extends Record<string, any
80
71
  */
81
72
  export declare class DeviceAuthorizationPending extends DeviceAuthorizationPending_base<{
82
73
  readonly timeoutSeconds: number;
74
+ readonly registryUrl: string;
75
+ readonly intervalSeconds: number;
83
76
  readonly verificationUri: string;
84
77
  readonly verificationUriComplete: string;
85
78
  readonly userCode: string;
@@ -128,8 +121,40 @@ export declare class AuthExchangeFailed extends AuthExchangeFailed_base<{
128
121
  readonly failure: RegistryClientFailure;
129
122
  }> {
130
123
  }
124
+ declare const PublishAuthorizationPending_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 & {
125
+ readonly _tag: "PublishAuthorizationPending";
126
+ } & Readonly<A>;
127
+ export declare class PublishAuthorizationPending extends PublishAuthorizationPending_base<{
128
+ readonly action: HumanHandoffAction;
129
+ readonly timedOut: boolean;
130
+ }> {
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
+ }
131
156
  /** Every typed failure the registry-auth feature constructs. */
132
- export type RegistryAuthFailure = RegistryAuthFailed | AuthLoginRequired | AuthTokenPolicyRequired | DeviceLoginDenied | DeviceLoginCodeExpired | DeviceAuthorizationPending | StepUpRequired | AuthExchangeFailed;
157
+ export type RegistryAuthFailure = RegistryAuthFailed | AuthLoginRequired | AuthTokenPolicyRequired | DeviceLoginDenied | DeviceLoginCodeExpired | DeviceAuthorizationPending | PublishAuthorizationPending | StepUpVerificationPending | AuthInteractionAbandoned | StepUpRequired | AuthExchangeFailed;
133
158
  export declare const isRegistryAuthFailure: (error: unknown) => error is RegistryAuthFailure;
134
159
  /**
135
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
  /**
@@ -72,12 +74,30 @@ export class StepUpRequired extends Data.TaggedError("StepUpRequired") {
72
74
  */
73
75
  export class AuthExchangeFailed extends Data.TaggedError("AuthExchangeFailed") {
74
76
  }
77
+ export class PublishAuthorizationPending extends Data.TaggedError("PublishAuthorizationPending") {
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
+ }
75
92
  export const isRegistryAuthFailure = (error) => error instanceof RegistryAuthFailed ||
76
93
  error instanceof AuthLoginRequired ||
77
94
  error instanceof AuthTokenPolicyRequired ||
78
95
  error instanceof DeviceLoginDenied ||
79
96
  error instanceof DeviceLoginCodeExpired ||
80
97
  error instanceof DeviceAuthorizationPending ||
98
+ error instanceof PublishAuthorizationPending ||
99
+ error instanceof StepUpVerificationPending ||
100
+ error instanceof AuthInteractionAbandoned ||
81
101
  error instanceof StepUpRequired ||
82
102
  error instanceof AuthExchangeFailed;
83
103
  export const isAuthError = (error) => isRegistryAuthFailure(error) || isRegistryClientFailure(error);