@agentxm/registry-auth 0.28.10 → 0.28.12

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.
@@ -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
  *
@@ -19,7 +20,7 @@ import { type Handle } from "@agentxm/extension-model/unstable/extensions/handle
19
20
  import { type PublishVisibility } from "@agentxm/registry-protocol/unstable/publish/visibility";
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
  }
@@ -26,7 +26,7 @@ import { normalizeHandle } from "@agentxm/extension-model/unstable/extensions/ha
26
26
  import { PublishVisibilitySchema, } from "@agentxm/registry-protocol/unstable/publish/visibility";
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,22 +93,44 @@ 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") {
99
122
  return "https://agentxm.ai";
100
123
  }
101
- if (url.origin === "https://registry-dev.agentxm-ai.workers.dev") {
102
- return "https://web-dev.agentxm-ai.workers.dev";
103
- }
104
- if (url.host === "localhost:4300") {
105
- return "http://localhost:4200";
124
+ if (url.origin === "https://registry-dev.agentxm.ai") {
125
+ return "https://web-dev.agentxm.ai";
106
126
  }
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",
@@ -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, 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>;
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,13 +1,4 @@
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. */
@@ -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,16 @@ 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
+ }
131
132
  /** Every typed failure the registry-auth feature constructs. */
132
- export type RegistryAuthFailure = RegistryAuthFailed | AuthLoginRequired | AuthTokenPolicyRequired | DeviceLoginDenied | DeviceLoginCodeExpired | DeviceAuthorizationPending | StepUpRequired | AuthExchangeFailed;
133
+ export type RegistryAuthFailure = RegistryAuthFailed | AuthLoginRequired | AuthTokenPolicyRequired | DeviceLoginDenied | DeviceLoginCodeExpired | DeviceAuthorizationPending | PublishAuthorizationPending | StepUpRequired | AuthExchangeFailed;
133
134
  export declare const isRegistryAuthFailure: (error: unknown) => error is RegistryAuthFailure;
134
135
  /**
135
136
  * Every failure a registry-auth use case can surface: the feature's own typed
@@ -72,12 +72,15 @@ export class StepUpRequired extends Data.TaggedError("StepUpRequired") {
72
72
  */
73
73
  export class AuthExchangeFailed extends Data.TaggedError("AuthExchangeFailed") {
74
74
  }
75
+ export class PublishAuthorizationPending extends Data.TaggedError("PublishAuthorizationPending") {
76
+ }
75
77
  export const isRegistryAuthFailure = (error) => error instanceof RegistryAuthFailed ||
76
78
  error instanceof AuthLoginRequired ||
77
79
  error instanceof AuthTokenPolicyRequired ||
78
80
  error instanceof DeviceLoginDenied ||
79
81
  error instanceof DeviceLoginCodeExpired ||
80
82
  error instanceof DeviceAuthorizationPending ||
83
+ error instanceof PublishAuthorizationPending ||
81
84
  error instanceof StepUpRequired ||
82
85
  error instanceof AuthExchangeFailed;
83
86
  export const isAuthError = (error) => isRegistryAuthFailure(error) || isRegistryClientFailure(error);
@@ -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, 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, isAuthError, isRegistryAuthFailure, REGISTRY_AUTH_ERROR_CATEGORIES, RegistryAuthFailed, StepUpRequired, 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";
@@ -18,7 +18,7 @@ export { canUsePersistedCredentials, CredentialStore, detectEnvironment, makePer
18
18
  export type { PendingDeviceLogin, PendingDeviceLoginStoreService, } from "./pending-device-login-store.js";
19
19
  export { PendingDeviceLoginSchema, PendingDeviceLoginStore } from "./pending-device-login-store.js";
20
20
  export { getCurrentUserHandle, resolveRequiredToken, resolveToken, resolveStoredToken, refreshStoredToken, resolveAmbientToken, resolveRequestToken, } from "./token-resolution.js";
21
- export type { AuthClientService, CreateTokenOptions, CreatePublishAuthorizationRequestParams, DeviceFlowResponse, ExchangePublishAuthorizationCodeParams, MeResponse, PollResult, PublishAuthorizationRequestResponse, PublishCapabilityResponse, } from "./auth-client.js";
21
+ export type { AuthClientService, CreateTokenOptions, CreatePublishAuthorizationRequestParams, DeviceFlowResponse, ExchangePublishAuthorizationCodeParams, MeResponse, PollResult, PublishAuthorizationRequestResponse, PublishAuthorizationExchangeResponse, PublishCapabilityResponse, } from "./auth-client.js";
22
22
  export { AuthClient, pollOnce, readStepUpRequest } from "./auth-client.js";
23
23
  export type { NormalizedTokenResponse } from "./oauth-contract.js";
24
24
  export type { DeviceLoginPendingResult, DeviceLoginInteractionService, ResumeDeviceLoginOptions, } from "./device-login.js";
@@ -33,4 +33,5 @@ export { selectLoginStrategy, type LoginStrategy, type LoginStrategyEnvironment,
33
33
  export type { AuthLoginInteractionService } from "./login-interaction.js";
34
34
  export { AuthLoginInteraction } from "./login-interaction.js";
35
35
  export { withAuthGuard } from "./guard.js";
36
+ export { PendingPublishAuthorizationStore, PendingPublishAuthorizationSchema, type PendingPublishAuthorization, type PendingPublishAuthorizationStoreService, } from "./pending-publish-authorization-store.js";
36
37
  //# sourceMappingURL=index.d.ts.map
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, DeviceLoginCodeExpired, DeviceLoginDenied, isAuthError, isRegistryAuthFailure, REGISTRY_AUTH_ERROR_CATEGORIES, RegistryAuthFailed, StepUpRequired, } from "./errors.js";
14
+ export { AuthExchangeFailed, AuthLoginRequired, AuthTokenPolicyRequired, authLoginRequired, DeviceAuthorizationPending, PublishAuthorizationPending, DeviceLoginCodeExpired, DeviceLoginDenied, isAuthError, isRegistryAuthFailure, REGISTRY_AUTH_ERROR_CATEGORIES, RegistryAuthFailed, StepUpRequired, } 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";
@@ -28,4 +28,5 @@ export { selectLoginStrategy, } from "./login-strategy.js";
28
28
  export { AuthLoginInteraction } from "./login-interaction.js";
29
29
  // Auth guard combinator
30
30
  export { withAuthGuard } from "./guard.js";
31
+ export { PendingPublishAuthorizationStore, PendingPublishAuthorizationSchema, } from "./pending-publish-authorization-store.js";
31
32
  //# sourceMappingURL=index.js.map
@@ -10,4 +10,5 @@ export { AuthMiddlewareLive, makeAuthMiddlewareLive } from "./auth-middleware.js
10
10
  export { CredentialStoreLive, CredentialStoreSessionLive } from "./credential-store.js";
11
11
  export { AuthLoginInteractionLive } from "./login-interaction.js";
12
12
  export { PendingDeviceLoginStoreLive } from "./pending-device-login-store.js";
13
+ export { PendingPublishAuthorizationStoreLive } from "./pending-publish-authorization-store.js";
13
14
  //# sourceMappingURL=live.d.ts.map
package/dist/src/live.js CHANGED
@@ -10,4 +10,5 @@ export { AuthMiddlewareLive, makeAuthMiddlewareLive } from "./auth-middleware.js
10
10
  export { CredentialStoreLive, CredentialStoreSessionLive } from "./credential-store.js";
11
11
  export { AuthLoginInteractionLive } from "./login-interaction.js";
12
12
  export { PendingDeviceLoginStoreLive } from "./pending-device-login-store.js";
13
+ export { PendingPublishAuthorizationStoreLive } from "./pending-publish-authorization-store.js";
13
14
  //# sourceMappingURL=live.js.map
@@ -52,7 +52,7 @@ export const runLoopbackLogin = (registryUrl, options = {}) => Effect.scoped(Eff
52
52
  const verifier = makePkceVerifier();
53
53
  const challenge = makePkceChallenge(verifier);
54
54
  const state = makeOAuthState();
55
- const server = yield* startLoopbackServer(state);
55
+ const server = yield* startLoopbackServer(state, "login");
56
56
  const authorizeUrl = authClient.buildAuthorizeUrl({
57
57
  challenge,
58
58
  expiresAt: DateTime.addDuration(yield* DateTime.now, LOOPBACK_TIMEOUT),
@@ -86,6 +86,7 @@ export const runLoopbackLogin = (registryUrl, options = {}) => Effect.scoped(Eff
86
86
  });
87
87
  return yield* persistLoginCredentials(registryUrl, token);
88
88
  }));
89
+ yield* server.complete;
89
90
  yield* emitLoginSuccess(registryUrl, handle);
90
91
  }));
91
92
  //# sourceMappingURL=loopback-login.js.map
@@ -29,11 +29,13 @@ export declare class LoopbackCallbackRejected extends LoopbackCallbackRejected_b
29
29
  export interface LoopbackServer {
30
30
  readonly port: number;
31
31
  readonly redirectUri: string;
32
+ readonly complete: Effect.Effect<void>;
32
33
  readonly awaitCallback: (timeoutMs: number) => Effect.Effect<LoopbackCallback, LoopbackLoginFallback | LoopbackCallbackRejected>;
33
34
  }
34
- export declare const startLoopbackServer: (expectedState: string) => Effect.Effect<{
35
+ export declare const startLoopbackServer: (expectedState: string, purpose: "login" | "publish") => Effect.Effect<{
35
36
  port: number;
36
37
  redirectUri: string;
38
+ complete: Effect.Effect<void, never, never>;
37
39
  awaitCallback: (timeoutMs: number) => Effect.Effect<LoopbackCallback, LoopbackLoginFallback | LoopbackCallbackRejected, never>;
38
40
  }, LoopbackLoginFallback, import("effect/Scope").Scope>;
39
41
  export {};
@@ -7,14 +7,14 @@ import * as Data from "effect/Data";
7
7
  import * as Deferred from "effect/Deferred";
8
8
  import * as Duration from "effect/Duration";
9
9
  import * as Effect from "effect/Effect";
10
+ import * as Option from "effect/Option";
10
11
  export class LoopbackLoginFallback extends Data.TaggedError("LoopbackLoginFallback") {
11
12
  }
12
13
  export class LoopbackCallbackRejected extends Data.TaggedError("LoopbackCallbackRejected") {
13
14
  }
14
15
  const page = (title, content) => `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body><main style="font-family: system-ui, sans-serif; margin: 3rem auto; max-width: 34rem;"><h1>${title}</h1><p>${content}</p></main></body></html>`;
15
- const successPage = page("You’re signed in to AgentXM.ai", 'Return to your terminal to continue. You can close this tab. <a href="https://agentxm.ai">AgentXM.ai</a>');
16
- const cancellationPage = page("Sign-in was cancelled", "No credentials were changed. Return to your terminal to try again.");
17
- const errorPage = page("AXM sign-in could not be completed", "Return to your terminal for details and recovery instructions.");
16
+ const cancellationPage = page("Authorization was denied", "Return to your terminal for recovery instructions.");
17
+ const errorPage = page("AXM authorization could not be completed", "Return to your terminal for details and recovery instructions.");
18
18
  const writeHtml = (response, statusCode, body) => {
19
19
  response.writeHead(statusCode, {
20
20
  "content-type": "text/html; charset=utf-8",
@@ -79,7 +79,7 @@ const makeCallbackOutcome = (request, expectedState) => {
79
79
  callback: { code, state, iss },
80
80
  };
81
81
  };
82
- export const startLoopbackServer = (expectedState) => Effect.gen(function* () {
82
+ export const startLoopbackServer = (expectedState, purpose) => Effect.gen(function* () {
83
83
  const http = yield* Effect.tryPromise({
84
84
  try: () => import("node:http"),
85
85
  catch: (cause) => new LoopbackLoginFallback({
@@ -89,16 +89,58 @@ export const startLoopbackServer = (expectedState) => Effect.gen(function* () {
89
89
  }),
90
90
  });
91
91
  const callback = yield* Deferred.make();
92
+ const browserResponse = yield* Deferred.make();
93
+ const finish = (completed) => Effect.gen(function* () {
94
+ const pending = yield* Deferred.poll(browserResponse);
95
+ if (Option.isNone(pending))
96
+ return;
97
+ const response = yield* pending.value;
98
+ if (response.writableEnded || response.destroyed)
99
+ return;
100
+ const title = completed
101
+ ? purpose === "login"
102
+ ? "You’re signed in to AgentXM.ai"
103
+ : "Publish authorization received"
104
+ : "AXM authorization could not be completed";
105
+ const content = completed
106
+ ? purpose === "login"
107
+ ? "Your credentials have been saved. Return to your terminal to continue. You can close this tab."
108
+ : "AXM received permission for the reviewed publication. Return to your terminal to check the publish result. You can close this tab."
109
+ : "Return to your terminal for details and recovery instructions.";
110
+ yield* Effect.callback((resume) => {
111
+ const done = () => resume(Effect.void);
112
+ response.once("close", done);
113
+ response.end(`<style>#receipt{display:none}</style><section role="status"><h1>${title}</h1><p>${content}</p></section></main></body></html>`, done);
114
+ return Effect.sync(() => {
115
+ response.off("close", done);
116
+ });
117
+ });
118
+ });
92
119
  const listener = yield* Effect.acquireRelease(Effect.callback((resume) => {
93
120
  let acquired = false;
94
121
  const server = http.createServer((request, response) => {
122
+ if (Deferred.isDoneUnsafe(callback)) {
123
+ writeHtml(response, 409, page("Callback already received", "Return to the original tab or your terminal to check the result."));
124
+ return;
125
+ }
95
126
  const outcome = makeCallbackOutcome(request, expectedState);
96
- writeHtml(response, outcome._tag === "success" ? 200 : 400, outcome._tag === "success"
97
- ? successPage
98
- : outcome.error._tag === "LoopbackCallbackRejected" &&
127
+ if (outcome._tag === "success") {
128
+ response.writeHead(200, {
129
+ "content-type": "text/html; charset=utf-8",
130
+ "cache-control": "no-store",
131
+ "referrer-policy": "no-referrer",
132
+ "x-content-type-options": "nosniff",
133
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'",
134
+ });
135
+ response.write('<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>AXM authorization</title></head><body><main style="font-family:system-ui,sans-serif;margin:3rem auto;max-width:34rem;padding:1rem"><section id="receipt" role="status"><h1>Callback received</h1><p>AXM is finishing authorization. Check your terminal if this page stops updating.</p></section>');
136
+ Deferred.doneUnsafe(browserResponse, Effect.succeed(response));
137
+ }
138
+ else {
139
+ writeHtml(response, 400, outcome.error._tag === "LoopbackCallbackRejected" &&
99
140
  outcome.error.reason === "access_denied"
100
141
  ? cancellationPage
101
142
  : errorPage);
143
+ }
102
144
  Deferred.doneUnsafe(callback, outcome._tag === "success"
103
145
  ? Effect.succeed(outcome.callback)
104
146
  : Effect.fail(outcome.error));
@@ -146,6 +188,7 @@ export const startLoopbackServer = (expectedState) => Effect.gen(function* () {
146
188
  },
147
189
  catch: () => undefined,
148
190
  }).pipe(Effect.ignore));
191
+ yield* Effect.addFinalizer(() => finish(false));
149
192
  listener.server.on("error", (cause) => {
150
193
  Deferred.doneUnsafe(callback, Effect.fail(new LoopbackLoginFallback({
151
194
  reason: "bind_failed",
@@ -156,6 +199,7 @@ export const startLoopbackServer = (expectedState) => Effect.gen(function* () {
156
199
  return {
157
200
  port: listener.port,
158
201
  redirectUri: `http://127.0.0.1:${listener.port}/callback`,
202
+ complete: finish(true),
159
203
  awaitCallback: (timeoutMs) => Deferred.await(callback).pipe(Effect.timeoutOrElse({
160
204
  duration: Duration.millis(timeoutMs),
161
205
  orElse: () => Effect.fail(new LoopbackLoginFallback({
@@ -0,0 +1,33 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import * as FileSystem from "effect/FileSystem";
4
+ import * as Layer from "effect/Layer";
5
+ import * as Option from "effect/Option";
6
+ import * as Path from "effect/Path";
7
+ import * as Schema from "effect/Schema";
8
+ import { RegistryAuthFailed } from "./errors.js";
9
+ export declare const PendingPublishAuthorizationSchema: Schema.Struct<{
10
+ readonly version: Schema.Literal<1>;
11
+ readonly purpose: Schema.Literal<"publish">;
12
+ readonly registryUrl: Schema.String;
13
+ readonly requestRef: Schema.String;
14
+ readonly requestId: Schema.String;
15
+ readonly authorizationUrl: Schema.String;
16
+ readonly expiresAt: Schema.decodeTo<Schema.DateTimeUtc, Schema.String, never, never>;
17
+ readonly interval: Schema.Int;
18
+ readonly publicationSetDigest: Schema.String;
19
+ readonly initiatorProof: Schema.String;
20
+ }>;
21
+ export type PendingPublishAuthorization = typeof PendingPublishAuthorizationSchema.Type;
22
+ export interface PendingPublishAuthorizationStoreService {
23
+ readonly save: (pending: PendingPublishAuthorization) => Effect.Effect<void, RegistryAuthFailed>;
24
+ readonly load: (requestRef: string) => Effect.Effect<Option.Option<PendingPublishAuthorization>, RegistryAuthFailed>;
25
+ readonly clear: (requestRef: string) => Effect.Effect<void, RegistryAuthFailed>;
26
+ }
27
+ declare const PendingPublishAuthorizationStore_base: Context.ServiceClass<PendingPublishAuthorizationStore, "@agentxm/registry-auth/PendingPublishAuthorizationStore", PendingPublishAuthorizationStoreService>;
28
+ export declare class PendingPublishAuthorizationStore extends PendingPublishAuthorizationStore_base {
29
+ }
30
+ export declare const PendingPublishAuthorizationStoreLive: Layer.Layer<PendingPublishAuthorizationStore, never, FileSystem.FileSystem | Path.Path>;
31
+ export declare const PendingPublishAuthorizationStoreTest: (initial?: ReadonlyArray<PendingPublishAuthorization>) => Layer.Layer<PendingPublishAuthorizationStore, never, never>;
32
+ export {};
33
+ //# sourceMappingURL=pending-publish-authorization-store.d.ts.map
@@ -0,0 +1,90 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as Context from "effect/Context";
3
+ import * as Effect from "effect/Effect";
4
+ import * as FileSystem from "effect/FileSystem";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Option from "effect/Option";
7
+ import * as Path from "effect/Path";
8
+ import * as Schema from "effect/Schema";
9
+ import { DateTimeUtcSchema } from "@agentxm/extension-model/unstable/date-time";
10
+ import { RegistryAuthFailed } from "./errors.js";
11
+ import { envOption } from "./internal/environment.js";
12
+ export const PendingPublishAuthorizationSchema = Schema.Struct({
13
+ version: Schema.Literal(1),
14
+ purpose: Schema.Literal("publish"),
15
+ registryUrl: Schema.String,
16
+ requestRef: Schema.String,
17
+ requestId: Schema.String,
18
+ authorizationUrl: Schema.String,
19
+ expiresAt: DateTimeUtcSchema,
20
+ interval: Schema.Int.check(Schema.isGreaterThan(0)),
21
+ publicationSetDigest: Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/)),
22
+ initiatorProof: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43,128}$/)),
23
+ });
24
+ export class PendingPublishAuthorizationStore extends Context.Service()("@agentxm/registry-auth/PendingPublishAuthorizationStore") {
25
+ }
26
+ const storageError = (operation) => new RegistryAuthFailed({
27
+ category: "auth",
28
+ detail: `Could not ${operation} the private publish authorization record. Its public request URL cannot replace the missing proof.`,
29
+ });
30
+ export const PendingPublishAuthorizationStoreLive = Layer.effect(PendingPublishAuthorizationStore, Effect.gen(function* () {
31
+ const fs = yield* FileSystem.FileSystem;
32
+ const path = yield* Path.Path;
33
+ const homes = [
34
+ yield* envOption("AXM_USER_HOME"),
35
+ yield* envOption("HOME"),
36
+ yield* envOption("USERPROFILE"),
37
+ ];
38
+ const home = homes.find(Option.isSome);
39
+ const storageDirectory = home === undefined
40
+ ? Effect.fail(storageError("locate"))
41
+ : Effect.succeed(path.join(home.value, ".axm", "publish-authorizations"));
42
+ const filename = (directory, reference) => path.join(directory, `${createHash("sha256").update(reference).digest("hex")}.json`);
43
+ return {
44
+ save: (pending) => Effect.gen(function* () {
45
+ const directory = yield* storageDirectory;
46
+ yield* fs.makeDirectory(directory, { recursive: true, mode: 0o700 });
47
+ yield* fs.chmod(directory, 0o700);
48
+ const file = filename(directory, pending.requestRef);
49
+ const encoded = yield* Schema.encodeEffect(PendingPublishAuthorizationSchema)(pending);
50
+ yield* Effect.acquireUseRelease(fs.makeTempDirectory({ directory, prefix: "publish-" }), (temporaryDirectory) => Effect.gen(function* () {
51
+ const temporary = path.join(temporaryDirectory, "proof.json");
52
+ yield* fs.writeFileString(temporary, JSON.stringify(encoded), {
53
+ mode: 0o600,
54
+ flag: "wx",
55
+ });
56
+ yield* fs.link(temporary, file);
57
+ }), (temporaryDirectory) => fs
58
+ .remove(temporaryDirectory, { recursive: true, force: true })
59
+ .pipe(Effect.catch(() => Effect.void)));
60
+ }).pipe(Effect.mapError(() => storageError("save"))),
61
+ load: (reference) => Effect.gen(function* () {
62
+ const directory = yield* storageDirectory;
63
+ const file = filename(directory, reference);
64
+ if (!(yield* fs.exists(file)))
65
+ return Option.none();
66
+ const content = yield* fs.readFileString(file);
67
+ const pending = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(PendingPublishAuthorizationSchema))(content);
68
+ if (pending.requestRef !== reference)
69
+ return yield* storageError("validate");
70
+ return Option.some(pending);
71
+ }).pipe(Effect.mapError(() => storageError("read"))),
72
+ clear: (reference) => Effect.gen(function* () {
73
+ const directory = yield* storageDirectory;
74
+ yield* fs.remove(filename(directory, reference), { force: true });
75
+ }).pipe(Effect.mapError(() => storageError("remove"))),
76
+ };
77
+ }));
78
+ export const PendingPublishAuthorizationStoreTest = (initial = []) => {
79
+ const records = new Map(initial.map((record) => [record.requestRef, record]));
80
+ return Layer.succeed(PendingPublishAuthorizationStore, {
81
+ save: (pending) => Effect.sync(() => {
82
+ records.set(pending.requestRef, pending);
83
+ }),
84
+ load: (reference) => Effect.sync(() => Option.fromNullishOr(records.get(reference))),
85
+ clear: (reference) => Effect.sync(() => {
86
+ records.delete(reference);
87
+ }),
88
+ });
89
+ };
90
+ //# sourceMappingURL=pending-publish-authorization-store.js.map
@@ -0,0 +1,13 @@
1
+ import { DeviceLoginInteraction } from "./device-login.js";
2
+ import { AuthLoginPresenter } from "./login-presenter.js";
3
+ import * as Effect from "effect/Effect";
4
+ import { AuthClient, type PublishAuthorizationExchangeResponse } from "./auth-client.js";
5
+ import { RegistryAuthFailed } from "./errors.js";
6
+ import { PendingPublishAuthorizationStore } from "./pending-publish-authorization-store.js";
7
+ import type { PublishAuthorizationInput } from "./publish-authorization.js";
8
+ export declare const readPublishAuthorizationReference: (reference: string, registryUrl: string) => Effect.Effect<{
9
+ requestId: string;
10
+ requestRef: string;
11
+ }, RegistryAuthFailed, never>;
12
+ export declare const runPollingPublishAuthorization: (input: PublishAuthorizationInput) => Effect.Effect<PublishAuthorizationExchangeResponse, import("./errors.js").AuthError, AuthClient | AuthLoginPresenter | DeviceLoginInteraction | PendingPublishAuthorizationStore>;
13
+ //# sourceMappingURL=publish-authorization-polling.d.ts.map
@@ -0,0 +1,176 @@
1
+ import { DeviceLoginInteraction } from "./device-login.js";
2
+ import { AuthLoginPresenter } from "./login-presenter.js";
3
+ import * as DateTime from "effect/DateTime";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Option from "effect/Option";
6
+ import { publicationSetDigest } from "@agentxm/registry-protocol/unstable/registry/publication-set";
7
+ import { AuthClient } from "./auth-client.js";
8
+ import { PublishAuthorizationPending, RegistryAuthFailed } from "./errors.js";
9
+ import { makePkceChallenge, makePkceVerifier } from "./loopback-login.js";
10
+ import { PendingPublishAuthorizationStore, } from "./pending-publish-authorization-store.js";
11
+ const invalidResume = (detail) => new RegistryAuthFailed({
12
+ category: "validation",
13
+ detail,
14
+ recover: "Resume with the original publication inputs and requestRef. To request new consent, rerun publish without --authorization-request.",
15
+ });
16
+ export const readPublishAuthorizationReference = (reference, registryUrl) => Effect.try({
17
+ try: () => {
18
+ const url = new URL(reference);
19
+ const registry = new URL(registryUrl);
20
+ const match = /^\/v1\/auth\/publish-requests\/(pubreq_[a-z0-9]{26})$/.exec(url.pathname);
21
+ if (url.origin !== registry.origin ||
22
+ url.username ||
23
+ url.password ||
24
+ url.search ||
25
+ url.hash ||
26
+ match?.[1] === undefined)
27
+ throw new Error("Invalid reference");
28
+ return { requestId: match[1], requestRef: url.href };
29
+ },
30
+ catch: () => invalidResume("The authorization request URL must identify a publish request on the selected Registry."),
31
+ });
32
+ const pendingHuman = (pending, timedOut = false) => new PublishAuthorizationPending({
33
+ timedOut,
34
+ action: {
35
+ kind: "open-url",
36
+ purpose: "publish",
37
+ registryUrl: pending.registryUrl,
38
+ requestRef: pending.requestRef,
39
+ url: pending.authorizationUrl,
40
+ expiresAt: DateTime.formatIso(pending.expiresAt),
41
+ intervalSeconds: pending.interval,
42
+ resume: `Rerun the same publish command with unchanged inputs and --authorization-request ${pending.requestRef}. Add --wait-for-human SECONDS for a bounded wait.`,
43
+ },
44
+ });
45
+ export const runPollingPublishAuthorization = Effect.fn("Auth.runPollingPublishAuthorization")(function* (input) {
46
+ const auth = yield* AuthClient;
47
+ const store = yield* PendingPublishAuthorizationStore;
48
+ const expectedDigest = publicationSetDigest(input.publicationSet.candidates);
49
+ const pending = yield* Effect.gen(function* () {
50
+ if (input.authorizationRequest !== undefined) {
51
+ const reference = yield* readPublishAuthorizationReference(input.authorizationRequest, input.registryUrl);
52
+ const saved = yield* store.load(reference.requestRef);
53
+ if (Option.isNone(saved))
54
+ return yield* invalidResume("The private proof for this publish request is unavailable on this machine. A public URL alone cannot resume it.");
55
+ if (saved.value.registryUrl !== input.registryUrl ||
56
+ saved.value.requestId !== reference.requestId ||
57
+ saved.value.publicationSetDigest !== expectedDigest)
58
+ return yield* invalidResume("The publication set, archive bytes, visibility inputs or Registry changed. This request cannot authorize the changed material; review a new request.");
59
+ return saved.value;
60
+ }
61
+ const initiatorProof = makePkceVerifier();
62
+ const request = yield* auth.createPublishAuthorizationRequest({
63
+ registryUrl: input.registryUrl,
64
+ publicationSet: input.publicationSet,
65
+ delivery: {
66
+ kind: "polling",
67
+ proof_challenge: makePkceChallenge(initiatorProof),
68
+ proof_challenge_method: "S256",
69
+ },
70
+ });
71
+ const reference = yield* readPublishAuthorizationReference(`${input.registryUrl.replace(/\/+$/, "")}/v1/auth/publish-requests/${request.requestId}`, input.registryUrl);
72
+ const value = {
73
+ version: 1,
74
+ purpose: "publish",
75
+ registryUrl: input.registryUrl,
76
+ ...reference,
77
+ authorizationUrl: request.authorizationUrl,
78
+ expiresAt: request.expiresAt,
79
+ interval: request.interval,
80
+ publicationSetDigest: expectedDigest,
81
+ initiatorProof,
82
+ };
83
+ yield* store.save(value);
84
+ return value;
85
+ });
86
+ const expired = () => new RegistryAuthFailed({
87
+ category: "auth_expired",
88
+ detail: "This publish authorization expired. Review a new request; no capability was acquired.",
89
+ recover: "Rerun publish without --authorization-request to request new consent.",
90
+ });
91
+ const now = yield* DateTime.now;
92
+ const remaining = DateTime.toEpochMillis(pending.expiresAt) - DateTime.toEpochMillis(now);
93
+ if (remaining <= 0) {
94
+ yield* store.clear(pending.requestRef);
95
+ return yield* expired();
96
+ }
97
+ if (input.authorizationRequest === undefined &&
98
+ input.waitForHumanSeconds === undefined &&
99
+ input.unattended === true)
100
+ return yield* pendingHuman(pending);
101
+ if (input.authorizationRequest === undefined && input.unattended !== true) {
102
+ const interaction = yield* DeviceLoginInteraction;
103
+ const presenter = yield* AuthLoginPresenter;
104
+ const browserOpened = yield* interaction.openBrowser(pending.authorizationUrl);
105
+ yield* presenter.notePublishReview({
106
+ browserOpened,
107
+ candidateCount: input.publicationSet.candidates.length,
108
+ authorizationUrl: pending.authorizationUrl,
109
+ });
110
+ }
111
+ const wait = Effect.gen(function* () {
112
+ while (true) {
113
+ const status = yield* auth.pollPublishAuthorization({
114
+ registryUrl: pending.registryUrl,
115
+ requestId: pending.requestId,
116
+ initiatorProof: pending.initiatorProof,
117
+ });
118
+ if (status.purpose !== "publish" ||
119
+ status.publication_set_digest !== expectedDigest ||
120
+ DateTime.toEpochMillis(status.expires_at) !== DateTime.toEpochMillis(pending.expiresAt))
121
+ return yield* invalidResume("The Registry returned a different publication binding or deadline. This request cannot be resumed.");
122
+ if (status.status === "expired") {
123
+ yield* store.clear(pending.requestRef);
124
+ return yield* expired();
125
+ }
126
+ if (status.status === "denied") {
127
+ yield* store.clear(pending.requestRef);
128
+ return yield* new RegistryAuthFailed({
129
+ category: "auth_denied",
130
+ detail: "Publish authorization was cancelled. No publication was authorized.",
131
+ recover: "Request new consent by rerunning publish without --authorization-request.",
132
+ });
133
+ }
134
+ if (status.status === "exchanged") {
135
+ yield* store.clear(pending.requestRef);
136
+ return yield* new RegistryAuthFailed({
137
+ category: "conflict",
138
+ detail: "This publish authorization was already exchanged. Verify the prior publication results before requesting new consent; the capabilities cannot be replayed.",
139
+ recover: "Rerun publish with the original inputs to verify existing versions and request consent for any remaining material.",
140
+ });
141
+ }
142
+ if (status.status === "approved")
143
+ return;
144
+ if (input.unattended === true && input.waitForHumanSeconds === undefined)
145
+ return yield* pendingHuman(pending);
146
+ const current = yield* DateTime.now;
147
+ const left = DateTime.toEpochMillis(pending.expiresAt) - DateTime.toEpochMillis(current);
148
+ if (left <= 0)
149
+ return yield* expired();
150
+ yield* Effect.sleep(Math.min(status.interval * 1000, left));
151
+ }
152
+ });
153
+ const bound = Math.min(remaining, input.waitForHumanSeconds === undefined ? remaining : input.waitForHumanSeconds * 1000);
154
+ const result = yield* wait.pipe(Effect.timeoutOption(bound));
155
+ if (Option.isSome(result)) {
156
+ // A lost exchange response is not retried: a later resume observes the
157
+ // retained request and takes explicit publication-outcome recovery.
158
+ const exchange = yield* auth.exchangePublishAuthorization({
159
+ registryUrl: pending.registryUrl,
160
+ requestId: pending.requestId,
161
+ initiatorProof: pending.initiatorProof,
162
+ });
163
+ if (exchange.preview.publicationSetDigest !== expectedDigest ||
164
+ exchange.grants.some((grant) => grant.publishRequestId !== pending.requestId ||
165
+ grant.publicationSetDigest !== expectedDigest))
166
+ return yield* invalidResume("The exchanged capabilities do not match the reviewed publication set.");
167
+ return exchange;
168
+ }
169
+ const afterWait = yield* DateTime.now;
170
+ if (DateTime.toEpochMillis(pending.expiresAt) <= DateTime.toEpochMillis(afterWait)) {
171
+ yield* store.clear(pending.requestRef);
172
+ return yield* expired();
173
+ }
174
+ return yield* pendingHuman(pending, true);
175
+ }, Effect.satisfiesSuccessType());
176
+ //# sourceMappingURL=publish-authorization-polling.js.map
@@ -6,6 +6,9 @@ import { AuthLoginPresenter } from "./login-presenter.js";
6
6
  export interface PublishAuthorizationInput {
7
7
  readonly registryUrl: string;
8
8
  readonly publicationSet: PreviewPublicationSetRequest;
9
+ readonly unattended?: boolean;
10
+ readonly authorizationRequest?: string;
11
+ readonly waitForHumanSeconds?: number;
9
12
  }
10
- export declare const runPublishAuthorization: (input: PublishAuthorizationInput) => Effect.Effect<PublishAuthorizationExchangeResponse, import("./errors.js").AuthError, AuthClient | AuthLoginPresenter | DeviceLoginInteraction>;
13
+ export declare const runPublishAuthorization: (input: PublishAuthorizationInput) => Effect.Effect<PublishAuthorizationExchangeResponse, import("./errors.js").AuthError, AuthClient | AuthLoginPresenter | DeviceLoginInteraction | import("./pending-publish-authorization-store.ts").PendingPublishAuthorizationStore>;
11
14
  //# sourceMappingURL=publish-authorization.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { runPollingPublishAuthorization } from "./publish-authorization-polling.js";
1
2
  import * as Effect from "effect/Effect";
2
3
  import { RegistryAuthFailed } from "./errors.js";
3
4
  import { AuthClient } from "./auth-client.js";
@@ -35,6 +36,16 @@ const loopbackFailureToAuthFailure = (error) => {
35
36
  });
36
37
  };
37
38
  export const runPublishAuthorization = Effect.fn("Auth.runPublishAuthorization")(function* (input) {
39
+ if (input.waitForHumanSeconds !== undefined &&
40
+ (!Number.isSafeInteger(input.waitForHumanSeconds) || input.waitForHumanSeconds <= 0))
41
+ return yield* new RegistryAuthFailed({
42
+ category: "validation",
43
+ detail: "--wait-for-human must be a positive whole number of seconds.",
44
+ });
45
+ if (input.unattended === true ||
46
+ input.authorizationRequest !== undefined ||
47
+ input.waitForHumanSeconds !== undefined)
48
+ return yield* runPollingPublishAuthorization(input);
38
49
  return yield* Effect.scoped(Effect.gen(function* () {
39
50
  const authClient = yield* AuthClient;
40
51
  const presenter = yield* AuthLoginPresenter;
@@ -42,12 +53,16 @@ export const runPublishAuthorization = Effect.fn("Auth.runPublishAuthorization")
42
53
  const verifier = makePkceVerifier();
43
54
  const challenge = makePkceChallenge(verifier);
44
55
  const state = makeOAuthState();
45
- const server = yield* startLoopbackServer(state).pipe(Effect.mapError(loopbackFailureToAuthFailure));
56
+ const server = yield* startLoopbackServer(state, "publish").pipe(Effect.mapError(loopbackFailureToAuthFailure));
46
57
  const request = yield* authClient.createPublishAuthorizationRequest({
47
58
  registryUrl: input.registryUrl,
48
- redirectUri: server.redirectUri,
49
- state,
50
- codeChallenge: challenge,
59
+ delivery: {
60
+ kind: "loopback",
61
+ redirect_uri: server.redirectUri,
62
+ state,
63
+ code_challenge: challenge,
64
+ code_challenge_method: "S256",
65
+ },
51
66
  publicationSet: input.publicationSet,
52
67
  });
53
68
  const openedBrowser = yield* interaction.openBrowser(request.authorizationUrl);
@@ -57,7 +72,9 @@ export const runPublishAuthorization = Effect.fn("Auth.runPublishAuthorization")
57
72
  authorizationUrl: request.authorizationUrl,
58
73
  });
59
74
  const callback = yield* server
60
- .awaitCallback(PUBLISH_AUTHORIZATION_TIMEOUT_MS)
75
+ .awaitCallback(Math.min(PUBLISH_AUTHORIZATION_TIMEOUT_MS, input.waitForHumanSeconds === undefined
76
+ ? PUBLISH_AUTHORIZATION_TIMEOUT_MS
77
+ : input.waitForHumanSeconds * 1000))
61
78
  .pipe(Effect.mapError(loopbackFailureToAuthFailure));
62
79
  const expectedIssuer = new URL(request.authorizationUrl).origin;
63
80
  if (callback.iss !== expectedIssuer) {
@@ -73,6 +90,8 @@ export const runPublishAuthorization = Effect.fn("Auth.runPublishAuthorization")
73
90
  verifier,
74
91
  redirectUri: server.redirectUri,
75
92
  });
93
+ if (capability.status === "admitted")
94
+ yield* server.complete;
76
95
  return capability;
77
96
  }));
78
97
  }, Effect.satisfiesSuccessType());
@@ -11,4 +11,5 @@ export { DeviceLoginInteractionTest, type DeviceLoginInteractionTestState, } fro
11
11
  export { AuthLoginInteractionTest, type AuthLoginInteractionTestState, } from "./login-interaction.js";
12
12
  export { AuthLoginPresenterTest, type AuthLoginPresenterTestState } from "./login-presenter.js";
13
13
  export { PendingDeviceLoginStoreTest } from "./pending-device-login-store.js";
14
+ export { PendingPublishAuthorizationStoreTest } from "./pending-publish-authorization-store.js";
14
15
  //# sourceMappingURL=testing.d.ts.map
@@ -11,4 +11,5 @@ export { DeviceLoginInteractionTest, } from "./device-login.js";
11
11
  export { AuthLoginInteractionTest, } from "./login-interaction.js";
12
12
  export { AuthLoginPresenterTest } from "./login-presenter.js";
13
13
  export { PendingDeviceLoginStoreTest } from "./pending-device-login-store.js";
14
+ export { PendingPublishAuthorizationStoreTest } from "./pending-publish-authorization-store.js";
14
15
  //# sourceMappingURL=testing.js.map
package/package.json CHANGED
@@ -4,9 +4,9 @@
4
4
  "url": "https://github.com/agentxm/axm/issues"
5
5
  },
6
6
  "dependencies": {
7
- "@agentxm/extension-model": "^0.28.10",
8
- "@agentxm/registry-client": "^0.28.10",
9
- "@agentxm/registry-protocol": "^0.28.10",
7
+ "@agentxm/extension-model": "^0.28.12",
8
+ "@agentxm/registry-client": "^0.28.12",
9
+ "@agentxm/registry-protocol": "^0.28.12",
10
10
  "@napi-rs/keyring": "^1.3.0",
11
11
  "effect": "4.0.0-rc.112",
12
12
  "proper-lockfile": "^4.1.2"
@@ -61,5 +61,5 @@
61
61
  },
62
62
  "sideEffects": false,
63
63
  "type": "module",
64
- "version": "0.28.10"
64
+ "version": "0.28.12"
65
65
  }