@agentxm/registry-auth 0.28.11 → 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.
- package/dist/src/auth-client.d.ts +12 -4
- package/dist/src/auth-client.js +79 -38
- package/dist/src/device-login.d.ts +20 -8
- package/dist/src/device-login.js +9 -4
- package/dist/src/errors.d.ts +12 -11
- package/dist/src/errors.js +3 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +2 -1
- package/dist/src/live.d.ts +1 -0
- package/dist/src/live.js +1 -0
- package/dist/src/loopback-login.js +2 -1
- package/dist/src/loopback-server.d.ts +3 -1
- package/dist/src/loopback-server.js +51 -7
- package/dist/src/pending-publish-authorization-store.d.ts +33 -0
- package/dist/src/pending-publish-authorization-store.js +90 -0
- package/dist/src/publish-authorization-polling.d.ts +13 -0
- package/dist/src/publish-authorization-polling.js +176 -0
- package/dist/src/publish-authorization.d.ts +4 -1
- package/dist/src/publish-authorization.js +24 -5
- package/dist/src/testing.d.ts +1 -0
- package/dist/src/testing.js +1 -0
- package/package.json +4 -4
|
@@ -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
|
|
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
|
}
|
package/dist/src/auth-client.js
CHANGED
|
@@ -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,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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/src/device-login.js
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
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,
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -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
|
package/dist/src/errors.js
CHANGED
|
@@ -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);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
package/dist/src/live.d.ts
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.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
|
|
16
|
-
const
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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());
|
package/dist/src/testing.d.ts
CHANGED
|
@@ -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
|
package/dist/src/testing.js
CHANGED
|
@@ -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.
|
|
8
|
-
"@agentxm/registry-client": "^0.28.
|
|
9
|
-
"@agentxm/registry-protocol": "^0.28.
|
|
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.
|
|
64
|
+
"version": "0.28.12"
|
|
65
65
|
}
|