@agentxm/registry-auth 0.28.12 → 0.28.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/auth-client.d.ts +1 -1
- package/dist/src/auth-client.js +1 -1
- package/dist/src/credential-store.d.ts +39 -2
- package/dist/src/credential-store.js +50 -46
- package/dist/src/device-login.d.ts +2 -2
- package/dist/src/errors.d.ts +26 -2
- package/dist/src/errors.js +17 -0
- package/dist/src/identity.d.ts +46 -0
- package/dist/src/identity.js +49 -0
- package/dist/src/index.d.ts +10 -1
- package/dist/src/index.js +13 -1
- package/dist/src/internal/environment.d.ts +23 -5
- package/dist/src/internal/environment.js +59 -9
- package/dist/src/login-presenter.d.ts +55 -0
- package/dist/src/login-presenter.js +22 -0
- package/dist/src/login-suggestion.d.ts +18 -0
- package/dist/src/login-suggestion.js +34 -0
- package/dist/src/login.d.ts +80 -0
- package/dist/src/login.js +155 -0
- package/dist/src/logout.d.ts +35 -0
- package/dist/src/logout.js +37 -0
- package/dist/src/selected-registry.d.ts +25 -0
- package/dist/src/selected-registry.js +18 -0
- package/dist/src/step-up.d.ts +49 -0
- package/dist/src/step-up.js +174 -0
- package/dist/src/tokens.d.ts +57 -0
- package/dist/src/tokens.js +78 -0
- package/package.json +7 -6
|
@@ -13,6 +13,7 @@ import * as Effect from "effect/Effect";
|
|
|
13
13
|
import * as Layer from "effect/Layer";
|
|
14
14
|
import * as ServiceMap from "effect/Context";
|
|
15
15
|
import type { DeviceLoginPendingResult } from "./device-login.js";
|
|
16
|
+
import type { AuthInteractionAbandoned } from "./errors.js";
|
|
16
17
|
import type { LoginResult } from "./login-output.js";
|
|
17
18
|
export type AuthLoginProgress = {
|
|
18
19
|
readonly _tag: "StartingDeviceAuthorization";
|
|
@@ -30,7 +31,42 @@ export type AuthLoginProgress = {
|
|
|
30
31
|
} | {
|
|
31
32
|
readonly _tag: "CompletingSignIn";
|
|
32
33
|
readonly registryHost: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly _tag: "CheckingRegistrySession";
|
|
36
|
+
readonly registryHost: string;
|
|
37
|
+
} | {
|
|
38
|
+
readonly _tag: "RevokingRegistrySession";
|
|
39
|
+
readonly registryHost: string;
|
|
40
|
+
} | {
|
|
41
|
+
readonly _tag: "ListingRegistryTokens";
|
|
42
|
+
}
|
|
43
|
+
/** A Registry write that the Registry may challenge for human verification. */
|
|
44
|
+
| {
|
|
45
|
+
readonly _tag: "RunningVerifiedWrite";
|
|
46
|
+
readonly operation: string;
|
|
47
|
+
}
|
|
48
|
+
/** The bounded wait while a person completes verification. */
|
|
49
|
+
| {
|
|
50
|
+
readonly _tag: "WaitingForHumanVerification";
|
|
51
|
+
readonly operation: string;
|
|
52
|
+
}
|
|
53
|
+
/** The one retry of the challenged write, after verification. */
|
|
54
|
+
| {
|
|
55
|
+
readonly _tag: "RetryingVerifiedWrite";
|
|
56
|
+
readonly operation: string;
|
|
33
57
|
};
|
|
58
|
+
/** What a person decided about replacing a session that is still valid. */
|
|
59
|
+
export type SessionReplacementDecision = "replace" | "keep";
|
|
60
|
+
/** Why sign-in fell back to the device-code flow. */
|
|
61
|
+
export type DeviceCodeFallbackReason = "remote-or-headless" | "loopback-bind-failed";
|
|
62
|
+
/** One step-up challenge, as a person needs to see it. */
|
|
63
|
+
export interface StepUpChallengePresentation {
|
|
64
|
+
readonly action: string;
|
|
65
|
+
readonly target: string;
|
|
66
|
+
readonly verificationUrl: string;
|
|
67
|
+
readonly expiresAt: string;
|
|
68
|
+
readonly browserOpened: boolean;
|
|
69
|
+
}
|
|
34
70
|
export interface DeviceFlowPresentation {
|
|
35
71
|
readonly verificationUri: string;
|
|
36
72
|
readonly verificationUriComplete: string;
|
|
@@ -64,6 +100,19 @@ export interface AuthLoginPresenterService {
|
|
|
64
100
|
readonly candidateCount: number;
|
|
65
101
|
readonly authorizationUrl: string;
|
|
66
102
|
}) => Effect.Effect<void>;
|
|
103
|
+
/** A still-valid session was found for the selected Registry. */
|
|
104
|
+
readonly noteExistingSession: (handle: string) => Effect.Effect<void>;
|
|
105
|
+
/** Stored credentials were rejected, so a new sign-in starts. */
|
|
106
|
+
readonly noteRejectedStoredCredentials: Effect.Effect<void>;
|
|
107
|
+
/** Sign-in fell back to the device-code flow for the carried reason. */
|
|
108
|
+
readonly noteDeviceCodeFallback: (reason: DeviceCodeFallbackReason) => Effect.Effect<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Ask whether to replace a session that is still valid. Abandoning the
|
|
111
|
+
* question fails with `AuthInteractionAbandoned`; declining returns "keep".
|
|
112
|
+
*/
|
|
113
|
+
readonly confirmSessionReplacement: (message: string) => Effect.Effect<SessionReplacementDecision, AuthInteractionAbandoned>;
|
|
114
|
+
/** Guidance for one pending step-up verification. */
|
|
115
|
+
readonly presentStepUpChallenge: (challenge: StepUpChallengePresentation) => Effect.Effect<void>;
|
|
67
116
|
}
|
|
68
117
|
declare const AuthLoginPresenter_base: ServiceMap.ServiceClass<AuthLoginPresenter, "@agentxm/registry-auth/login-presenter/AuthLoginPresenter", AuthLoginPresenterService>;
|
|
69
118
|
export declare class AuthLoginPresenter extends AuthLoginPresenter_base {
|
|
@@ -84,9 +133,15 @@ export interface AuthLoginPresenterTestState {
|
|
|
84
133
|
readonly candidateCount: number;
|
|
85
134
|
readonly authorizationUrl: string;
|
|
86
135
|
}>;
|
|
136
|
+
readonly existingSessions: Array<string>;
|
|
137
|
+
readonly rejectedStoredCredentials: Array<true>;
|
|
138
|
+
readonly deviceCodeFallbacks: Array<DeviceCodeFallbackReason>;
|
|
139
|
+
readonly sessionReplacementPrompts: Array<string>;
|
|
140
|
+
readonly stepUpChallenges: Array<StepUpChallengePresentation>;
|
|
87
141
|
}
|
|
88
142
|
export declare const AuthLoginPresenterTest: (overrides?: {
|
|
89
143
|
readonly tryEmitPendingDeviceLogin?: (result: DeviceLoginPendingResult) => Effect.Effect<boolean>;
|
|
144
|
+
readonly confirmSessionReplacement?: (message: string) => Effect.Effect<SessionReplacementDecision, AuthInteractionAbandoned>;
|
|
90
145
|
}) => {
|
|
91
146
|
layer: Layer.Layer<AuthLoginPresenter, never, never>;
|
|
92
147
|
state: AuthLoginPresenterTestState;
|
|
@@ -24,6 +24,11 @@ export const AuthLoginPresenterTest = (overrides) => {
|
|
|
24
24
|
loopbackStarts: [],
|
|
25
25
|
loopbackBrowserOutcomes: [],
|
|
26
26
|
publishReviews: [],
|
|
27
|
+
existingSessions: [],
|
|
28
|
+
rejectedStoredCredentials: [],
|
|
29
|
+
deviceCodeFallbacks: [],
|
|
30
|
+
sessionReplacementPrompts: [],
|
|
31
|
+
stepUpChallenges: [],
|
|
27
32
|
};
|
|
28
33
|
const layer = Layer.succeed(AuthLoginPresenter, {
|
|
29
34
|
withProgress: (progress, run) => Effect.suspend(() => {
|
|
@@ -52,6 +57,23 @@ export const AuthLoginPresenterTest = (overrides) => {
|
|
|
52
57
|
notePublishReview: (review) => Effect.sync(() => {
|
|
53
58
|
state.publishReviews.push(review);
|
|
54
59
|
}),
|
|
60
|
+
noteExistingSession: (handle) => Effect.sync(() => {
|
|
61
|
+
state.existingSessions.push(handle);
|
|
62
|
+
}),
|
|
63
|
+
noteRejectedStoredCredentials: Effect.sync(() => {
|
|
64
|
+
state.rejectedStoredCredentials.push(true);
|
|
65
|
+
}),
|
|
66
|
+
noteDeviceCodeFallback: (reason) => Effect.sync(() => {
|
|
67
|
+
state.deviceCodeFallbacks.push(reason);
|
|
68
|
+
}),
|
|
69
|
+
confirmSessionReplacement: (message) => Effect.gen(function* () {
|
|
70
|
+
state.sessionReplacementPrompts.push(message);
|
|
71
|
+
return yield* overrides?.confirmSessionReplacement?.(message) ??
|
|
72
|
+
Effect.succeed("replace");
|
|
73
|
+
}),
|
|
74
|
+
presentStepUpChallenge: (challenge) => Effect.sync(() => {
|
|
75
|
+
state.stepUpChallenges.push(challenge);
|
|
76
|
+
}),
|
|
55
77
|
});
|
|
56
78
|
return { layer, state };
|
|
57
79
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline credential-presence probe.
|
|
3
|
+
*
|
|
4
|
+
* Answers "could this invocation be missing a private item because it is not
|
|
5
|
+
* signed in to that origin?" without contacting any Registry. A successful
|
|
6
|
+
* ambient or stored token suppresses the hint even when that identity cannot
|
|
7
|
+
* see the requested item.
|
|
8
|
+
*
|
|
9
|
+
* @experimental This API is unstable and may change without notice.
|
|
10
|
+
*/
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import { CredentialStore } from "./credential-store.js";
|
|
13
|
+
/**
|
|
14
|
+
* Whether every remote origin among `locations` already has a credential this
|
|
15
|
+
* invocation would present. False when at least one does not.
|
|
16
|
+
*/
|
|
17
|
+
export declare const hasCredentialsForAll: (locations: ReadonlyArray<string>, defaultRegistryUrl: string) => Effect.Effect<boolean, never, CredentialStore>;
|
|
18
|
+
//# sourceMappingURL=login-suggestion.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline credential-presence probe.
|
|
3
|
+
*
|
|
4
|
+
* Answers "could this invocation be missing a private item because it is not
|
|
5
|
+
* signed in to that origin?" without contacting any Registry. A successful
|
|
6
|
+
* ambient or stored token suppresses the hint even when that identity cannot
|
|
7
|
+
* see the requested item.
|
|
8
|
+
*
|
|
9
|
+
* @experimental This API is unstable and may change without notice.
|
|
10
|
+
*/
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Option from "effect/Option";
|
|
13
|
+
import { CredentialStore } from "./credential-store.js";
|
|
14
|
+
import { resolveRequestToken } from "./token-resolution.js";
|
|
15
|
+
const remoteOrigins = (locations) => {
|
|
16
|
+
const origins = new Set();
|
|
17
|
+
for (const location of locations) {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(location);
|
|
20
|
+
if (url.protocol === "http:" || url.protocol === "https:")
|
|
21
|
+
origins.add(url.origin);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Invalid source locations are reported by their owning resolution path.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...origins];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Whether every remote origin among `locations` already has a credential this
|
|
31
|
+
* invocation would present. False when at least one does not.
|
|
32
|
+
*/
|
|
33
|
+
export const hasCredentialsForAll = (locations, defaultRegistryUrl) => Effect.forEach(remoteOrigins(locations), (origin) => resolveRequestToken(origin, defaultRegistryUrl).pipe(Effect.map(Option.isSome), Effect.catch(() => Effect.succeed(false)))).pipe(Effect.map((present) => present.every((hasCredential) => hasCredential)));
|
|
34
|
+
//# sourceMappingURL=login-suggestion.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-in orchestration: flag coherence, persisted-credential policy, the
|
|
3
|
+
* existing-session decision, strategy selection, and the loopback →
|
|
4
|
+
* device-code fallback chain.
|
|
5
|
+
*
|
|
6
|
+
* The capability decides; the application supplies presentation through
|
|
7
|
+
* `AuthLoginPresenter` and platform integration through
|
|
8
|
+
* `AuthLoginInteraction`. No sign-in decision is taken outside this module.
|
|
9
|
+
*
|
|
10
|
+
* @experimental This API is unstable and may change without notice.
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import { AuthClient } from "./auth-client.js";
|
|
14
|
+
import { CredentialStore } from "./credential-store.js";
|
|
15
|
+
import { type RunDeviceLoginOptions } from "./device-login.js";
|
|
16
|
+
import { RegistryAuthFailed } from "./errors.js";
|
|
17
|
+
import type { LoopbackCallbackRejected, LoopbackLoginFallback } from "./loopback-server.js";
|
|
18
|
+
import { AuthLoginPresenter } from "./login-presenter.js";
|
|
19
|
+
/** The invocation's sign-in inputs, parsed from the command line. */
|
|
20
|
+
export interface LoginRequest {
|
|
21
|
+
/** Preapproval: start a new sign-in without asking about a valid session. */
|
|
22
|
+
readonly yes: boolean;
|
|
23
|
+
readonly deviceCode: boolean;
|
|
24
|
+
readonly restart: boolean;
|
|
25
|
+
/** Resume and wait for a pending device sign-in instead of starting one. */
|
|
26
|
+
readonly wait: boolean;
|
|
27
|
+
readonly timeoutSeconds?: number;
|
|
28
|
+
readonly scopes: ReadonlyArray<string>;
|
|
29
|
+
/** No terminal is available to run a sign-in flow interactively. */
|
|
30
|
+
readonly nonInteractive: boolean;
|
|
31
|
+
/** The invocation reports through a machine document rather than prose. */
|
|
32
|
+
readonly machineOutput: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** What one `login` invocation settled on. */
|
|
35
|
+
export type LoginOutcome =
|
|
36
|
+
/** A valid session was found and kept; nothing was written. */
|
|
37
|
+
{
|
|
38
|
+
readonly _tag: "SessionRetained";
|
|
39
|
+
readonly registryHost: string;
|
|
40
|
+
readonly handle: string;
|
|
41
|
+
}
|
|
42
|
+
/** A sign-in flow ran; the presenter reported its result. */
|
|
43
|
+
| {
|
|
44
|
+
readonly _tag: "SignInAttempted";
|
|
45
|
+
}
|
|
46
|
+
/** A pending device sign-in was resumed. */
|
|
47
|
+
| {
|
|
48
|
+
readonly _tag: "PendingSignInResumed";
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The device-login options one sign-in request means. Exported so the option
|
|
52
|
+
* shaping is provable without substituting the flow it is handed to.
|
|
53
|
+
*/
|
|
54
|
+
export declare const deviceLoginOptions: (request: Pick<LoginRequest, "restart" | "scopes">, openBrowser: boolean) => RunDeviceLoginOptions;
|
|
55
|
+
/** The resume options one `--wait` request means. */
|
|
56
|
+
export declare const resumeLoginOptions: (request: Pick<LoginRequest, "timeoutSeconds">) => {
|
|
57
|
+
readonly timeoutSeconds?: number;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* How a loopback sign-in that could not complete is classified.
|
|
61
|
+
*
|
|
62
|
+
* A failed bind is recoverable — device-code sign-in replaces it — while an
|
|
63
|
+
* expired wait or a rejected callback is terminal and leaves credentials
|
|
64
|
+
* untouched.
|
|
65
|
+
*/
|
|
66
|
+
export declare const classifyLoopbackFailure: (error: LoopbackLoginFallback | LoopbackCallbackRejected) => RegistryAuthFailed;
|
|
67
|
+
export declare const login: (request: LoginRequest, registryUrl: string) => Effect.Effect<{
|
|
68
|
+
readonly _tag: "PendingSignInResumed";
|
|
69
|
+
readonly registryHost?: never;
|
|
70
|
+
readonly handle?: never;
|
|
71
|
+
} | {
|
|
72
|
+
readonly _tag: "SessionRetained";
|
|
73
|
+
readonly registryHost: string;
|
|
74
|
+
readonly handle: string & import("effect/Brand").Brand<"Handle">;
|
|
75
|
+
} | {
|
|
76
|
+
readonly _tag: "SignInAttempted";
|
|
77
|
+
readonly registryHost?: never;
|
|
78
|
+
readonly handle?: never;
|
|
79
|
+
}, RegistryAuthFailed | import("./errors.js").AuthLoginRequired | import("./errors.js").AuthTokenPolicyRequired | import("./errors.js").DeviceLoginDenied | import("./errors.js").DeviceLoginCodeExpired | import("./errors.js").DeviceAuthorizationPending | import("./errors.js").StepUpRequired | import("@agentxm/registry-client").RegistryProblem | import("@agentxm/registry-client").RegistryRequestFailed | import("@agentxm/registry-client").RegistryOperationFailed | import("./errors.js").AuthExchangeFailed | import("./errors.js").PublishAuthorizationPending | import("./errors.js").StepUpVerificationPending | import("./errors.js").AuthInteractionAbandoned, AuthClient | CredentialStore | AuthLoginPresenter | import("./pending-device-login-store.ts").PendingDeviceLoginStore | import("./device-login.js").DeviceLoginInteraction>;
|
|
80
|
+
//# sourceMappingURL=login.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-in orchestration: flag coherence, persisted-credential policy, the
|
|
3
|
+
* existing-session decision, strategy selection, and the loopback →
|
|
4
|
+
* device-code fallback chain.
|
|
5
|
+
*
|
|
6
|
+
* The capability decides; the application supplies presentation through
|
|
7
|
+
* `AuthLoginPresenter` and platform integration through
|
|
8
|
+
* `AuthLoginInteraction`. No sign-in decision is taken outside this module.
|
|
9
|
+
*
|
|
10
|
+
* @experimental This API is unstable and may change without notice.
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Option from "effect/Option";
|
|
14
|
+
import { AuthClient } from "./auth-client.js";
|
|
15
|
+
import { CredentialStore, makePersistedCredentialsUnsupportedError } from "./credential-store.js";
|
|
16
|
+
import { initiateDeviceLogin, resumeDeviceLogin, runDeviceLogin, } from "./device-login.js";
|
|
17
|
+
import { RegistryAuthFailed } from "./errors.js";
|
|
18
|
+
import { AuthLoginPresenter } from "./login-presenter.js";
|
|
19
|
+
import { runLoopbackLogin } from "./loopback-login.js";
|
|
20
|
+
import { selectLoginStrategy } from "./login-strategy.js";
|
|
21
|
+
import { loginStrategyEnvironment } from "./internal/environment.js";
|
|
22
|
+
const usage = (detail) => new RegistryAuthFailed({ category: "usage", detail });
|
|
23
|
+
/**
|
|
24
|
+
* The device-login options one sign-in request means. Exported so the option
|
|
25
|
+
* shaping is provable without substituting the flow it is handed to.
|
|
26
|
+
*/
|
|
27
|
+
export const deviceLoginOptions = (request, openBrowser) => ({
|
|
28
|
+
openBrowser,
|
|
29
|
+
restart: request.restart,
|
|
30
|
+
...(request.scopes.length === 0 ? {} : { scopes: [...request.scopes] }),
|
|
31
|
+
});
|
|
32
|
+
/** The resume options one `--wait` request means. */
|
|
33
|
+
export const resumeLoginOptions = (request) => request.timeoutSeconds === undefined ? {} : { timeoutSeconds: request.timeoutSeconds };
|
|
34
|
+
/**
|
|
35
|
+
* How a loopback sign-in that could not complete is classified.
|
|
36
|
+
*
|
|
37
|
+
* A failed bind is recoverable — device-code sign-in replaces it — while an
|
|
38
|
+
* expired wait or a rejected callback is terminal and leaves credentials
|
|
39
|
+
* untouched.
|
|
40
|
+
*/
|
|
41
|
+
export const classifyLoopbackFailure = (error) => error._tag === "LoopbackLoginFallback"
|
|
42
|
+
? new RegistryAuthFailed({
|
|
43
|
+
category: "auth",
|
|
44
|
+
detail: "Browser sign-in expired after 5 minutes. No credentials were changed.",
|
|
45
|
+
suggestions: [
|
|
46
|
+
{ description: "Try browser sign-in again.", cmd: "axm login" },
|
|
47
|
+
{
|
|
48
|
+
description: "Use device-code sign-in on a remote or headless machine.",
|
|
49
|
+
cmd: "axm login --device-code",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
cause: error,
|
|
53
|
+
})
|
|
54
|
+
: new RegistryAuthFailed({
|
|
55
|
+
category: "auth",
|
|
56
|
+
detail: error.reason === "access_denied"
|
|
57
|
+
? "Sign-in was cancelled. No credentials were changed."
|
|
58
|
+
: "The authorization callback was invalid and sign-in could not be completed. Run `axm login` to try again.",
|
|
59
|
+
suggestions: [{ description: "Try signing in again.", cmd: "axm login" }],
|
|
60
|
+
cause: error,
|
|
61
|
+
});
|
|
62
|
+
const rejectIncoherentFlags = (request) => request.wait && request.deviceCode
|
|
63
|
+
? Option.some(usage("--wait resumes an existing device sign-in and cannot be combined with --device-code."))
|
|
64
|
+
: request.wait && request.restart
|
|
65
|
+
? Option.some(usage("--restart starts a replacement device sign-in and cannot be combined with --wait."))
|
|
66
|
+
: request.restart && !request.deviceCode
|
|
67
|
+
? Option.some(usage("--restart requires --device-code."))
|
|
68
|
+
: !request.wait && request.timeoutSeconds !== undefined
|
|
69
|
+
? Option.some(usage("--timeout requires --wait."))
|
|
70
|
+
: Option.none();
|
|
71
|
+
/**
|
|
72
|
+
* Decide whether a session that is still valid should be replaced.
|
|
73
|
+
*
|
|
74
|
+
* Preapproval answers the one question a valid session raises, so a new
|
|
75
|
+
* sign-in starts in every mode. Without it, a mode that cannot ask keeps the
|
|
76
|
+
* session; a mode that can, asks.
|
|
77
|
+
*/
|
|
78
|
+
const shouldReplaceValidSession = (request, handle) => Effect.gen(function* () {
|
|
79
|
+
const presenter = yield* AuthLoginPresenter;
|
|
80
|
+
if (request.yes) {
|
|
81
|
+
if (!request.machineOutput)
|
|
82
|
+
yield* presenter.noteExistingSession(handle);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (request.nonInteractive || request.machineOutput)
|
|
86
|
+
return false;
|
|
87
|
+
yield* presenter.noteExistingSession(handle);
|
|
88
|
+
const decision = yield* presenter.confirmSessionReplacement("Log in with a different account?");
|
|
89
|
+
return decision === "replace";
|
|
90
|
+
});
|
|
91
|
+
export const login = Effect.fn("Login.run")(function* (request, registryUrl) {
|
|
92
|
+
const credentials = yield* CredentialStore;
|
|
93
|
+
const presenter = yield* AuthLoginPresenter;
|
|
94
|
+
const registryHost = new URL(registryUrl).host;
|
|
95
|
+
const incoherent = rejectIncoherentFlags(request);
|
|
96
|
+
if (Option.isSome(incoherent))
|
|
97
|
+
return yield* Effect.fail(incoherent.value);
|
|
98
|
+
if (!credentials.allowsPersistedCredentials) {
|
|
99
|
+
return yield* makePersistedCredentialsUnsupportedError();
|
|
100
|
+
}
|
|
101
|
+
if (request.wait) {
|
|
102
|
+
yield* resumeDeviceLogin(registryUrl, resumeLoginOptions(request));
|
|
103
|
+
return { _tag: "PendingSignInResumed" };
|
|
104
|
+
}
|
|
105
|
+
const existing = yield* credentials.load(registryUrl);
|
|
106
|
+
if (Option.isSome(existing)) {
|
|
107
|
+
const authClient = yield* AuthClient;
|
|
108
|
+
const identity = yield* presenter.withProgress({ _tag: "CheckingRegistrySession", registryHost }, () => authClient.getMe(existing.value.access_token).pipe(Effect.option));
|
|
109
|
+
if (Option.isSome(identity)) {
|
|
110
|
+
if (!(yield* shouldReplaceValidSession(request, identity.value.userHandle))) {
|
|
111
|
+
return {
|
|
112
|
+
_tag: "SessionRetained",
|
|
113
|
+
registryHost,
|
|
114
|
+
handle: identity.value.userHandle,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (!request.machineOutput) {
|
|
119
|
+
yield* presenter.noteRejectedStoredCredentials;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const strategy = selectLoginStrategy({ deviceCode: request.deviceCode, nonInteractive: request.nonInteractive }, yield* loginStrategyEnvironment);
|
|
123
|
+
const scopeOptions = request.scopes.length === 0 ? {} : { scopes: [...request.scopes] };
|
|
124
|
+
const deviceOptions = (openBrowser) => deviceLoginOptions(request, openBrowser);
|
|
125
|
+
if (strategy === "device-code") {
|
|
126
|
+
if (!request.deviceCode)
|
|
127
|
+
yield* presenter.noteDeviceCodeFallback("remote-or-headless");
|
|
128
|
+
if (request.nonInteractive) {
|
|
129
|
+
yield* initiateDeviceLogin(registryUrl, deviceOptions(false));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
yield* runDeviceLogin(registryUrl, deviceOptions(false));
|
|
133
|
+
}
|
|
134
|
+
return { _tag: "SignInAttempted" };
|
|
135
|
+
}
|
|
136
|
+
if (request.nonInteractive) {
|
|
137
|
+
return yield* new RegistryAuthFailed({
|
|
138
|
+
category: "auth",
|
|
139
|
+
detail: "Loopback browser sign-in requires an interactive terminal. Use device-code sign-in instead.",
|
|
140
|
+
suggestions: [
|
|
141
|
+
{
|
|
142
|
+
description: "Use device-code sign-in on a remote or headless machine.",
|
|
143
|
+
cmd: "axm login --device-code",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
yield* Effect.suspend(() => runLoopbackLogin(registryUrl, { ...scopeOptions })).pipe(Effect.catchTag("LoopbackLoginFallback", (error) => error.reason === "bind_failed"
|
|
149
|
+
? presenter
|
|
150
|
+
.noteDeviceCodeFallback("loopback-bind-failed")
|
|
151
|
+
.pipe(Effect.andThen(runDeviceLogin(registryUrl, deviceOptions(true))))
|
|
152
|
+
: Effect.fail(classifyLoopbackFailure(error))), Effect.catchTag("LoopbackCallbackRejected", (error) => Effect.fail(classifyLoopbackFailure(error))));
|
|
153
|
+
return { _tag: "SignInAttempted" };
|
|
154
|
+
});
|
|
155
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-out: revoke the session at the Registry, then erase the selected
|
|
3
|
+
* Registry's stored credentials.
|
|
4
|
+
*
|
|
5
|
+
* The local erase is unconditional. A remote revoke that fails leaves a token
|
|
6
|
+
* that expires on its own, and the outcome says so rather than reporting a
|
|
7
|
+
* clean sign-out.
|
|
8
|
+
*
|
|
9
|
+
* @experimental This API is unstable and may change without notice.
|
|
10
|
+
*/
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import { AuthClient } from "./auth-client.js";
|
|
13
|
+
import { CredentialStore } from "./credential-store.js";
|
|
14
|
+
import { AuthLoginPresenter } from "./login-presenter.js";
|
|
15
|
+
/** What one `logout` invocation settled on. */
|
|
16
|
+
export type LogoutOutcome = {
|
|
17
|
+
readonly _tag: "NotSignedIn";
|
|
18
|
+
readonly registryHost: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly _tag: "SignedOut";
|
|
21
|
+
readonly registryHost: string;
|
|
22
|
+
readonly handle?: string;
|
|
23
|
+
/** False when the Registry did not confirm the revoke; the token expires on its own. */
|
|
24
|
+
readonly revokedRemotely: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare const logout: (registryUrl: string) => Effect.Effect<{
|
|
27
|
+
readonly _tag: "NotSignedIn";
|
|
28
|
+
readonly registryHost: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly revokedRemotely: boolean;
|
|
31
|
+
readonly handle?: string & import("effect/Brand").Brand<"Handle">;
|
|
32
|
+
readonly _tag: "SignedOut";
|
|
33
|
+
readonly registryHost: string;
|
|
34
|
+
}, import("./errors.ts").RegistryAuthFailed, AuthClient | CredentialStore | AuthLoginPresenter>;
|
|
35
|
+
//# sourceMappingURL=logout.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-out: revoke the session at the Registry, then erase the selected
|
|
3
|
+
* Registry's stored credentials.
|
|
4
|
+
*
|
|
5
|
+
* The local erase is unconditional. A remote revoke that fails leaves a token
|
|
6
|
+
* that expires on its own, and the outcome says so rather than reporting a
|
|
7
|
+
* clean sign-out.
|
|
8
|
+
*
|
|
9
|
+
* @experimental This API is unstable and may change without notice.
|
|
10
|
+
*/
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Option from "effect/Option";
|
|
13
|
+
import { AuthClient } from "./auth-client.js";
|
|
14
|
+
import { CredentialStore } from "./credential-store.js";
|
|
15
|
+
import { AuthLoginPresenter } from "./login-presenter.js";
|
|
16
|
+
/** `normalizeHandle("@unknown")` — the anonymous credential sentinel. */
|
|
17
|
+
const ANONYMOUS_HANDLE = "@unknown";
|
|
18
|
+
export const logout = Effect.fn("Logout.run")(function* (registryUrl) {
|
|
19
|
+
const authClient = yield* AuthClient;
|
|
20
|
+
const credentials = yield* CredentialStore;
|
|
21
|
+
const presenter = yield* AuthLoginPresenter;
|
|
22
|
+
const registryHost = new URL(registryUrl).host;
|
|
23
|
+
const existing = yield* credentials.load(registryUrl);
|
|
24
|
+
if (Option.isNone(existing)) {
|
|
25
|
+
return { _tag: "NotSignedIn", registryHost };
|
|
26
|
+
}
|
|
27
|
+
const revoked = yield* presenter.withProgress({ _tag: "RevokingRegistrySession", registryHost }, () => authClient.revokeToken(existing.value.refresh_token).pipe(Effect.option));
|
|
28
|
+
yield* credentials.clear(registryUrl);
|
|
29
|
+
const handle = existing.value.handle;
|
|
30
|
+
return {
|
|
31
|
+
_tag: "SignedOut",
|
|
32
|
+
registryHost,
|
|
33
|
+
...(handle === ANONYMOUS_HANDLE ? {} : { handle }),
|
|
34
|
+
revokedRemotely: Option.isSome(revoked),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=logout.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Registry this invocation authenticates against.
|
|
3
|
+
*
|
|
4
|
+
* Which Registry is selected is transport configuration, so the value itself
|
|
5
|
+
* belongs to the Registry client. What a caller needs from it is an
|
|
6
|
+
* authentication concern: the endpoint every use case in this package takes,
|
|
7
|
+
* and the host a person is told they are signing in to, signing out of, or
|
|
8
|
+
* identified on. Publishing both here is what lets a caller name the selected
|
|
9
|
+
* Registry without reaching into the transport package for its configuration
|
|
10
|
+
* tag.
|
|
11
|
+
*
|
|
12
|
+
* @experimental This API is unstable and may change without notice.
|
|
13
|
+
*/
|
|
14
|
+
import * as Effect from "effect/Effect";
|
|
15
|
+
import { RegistryUrl } from "@agentxm/registry-client";
|
|
16
|
+
/** The selected Registry, as an endpoint and as a host a person reads. */
|
|
17
|
+
export interface SelectedRegistry {
|
|
18
|
+
/** The endpoint every registry-auth use case takes. */
|
|
19
|
+
readonly url: string;
|
|
20
|
+
/** The host, for the operation names and result documents that show it. */
|
|
21
|
+
readonly host: string;
|
|
22
|
+
}
|
|
23
|
+
/** Read the selected Registry's endpoint and its human-readable host. */
|
|
24
|
+
export declare const selectedRegistry: Effect.Effect<SelectedRegistry, never, RegistryUrl>;
|
|
25
|
+
//# sourceMappingURL=selected-registry.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Registry this invocation authenticates against.
|
|
3
|
+
*
|
|
4
|
+
* Which Registry is selected is transport configuration, so the value itself
|
|
5
|
+
* belongs to the Registry client. What a caller needs from it is an
|
|
6
|
+
* authentication concern: the endpoint every use case in this package takes,
|
|
7
|
+
* and the host a person is told they are signing in to, signing out of, or
|
|
8
|
+
* identified on. Publishing both here is what lets a caller name the selected
|
|
9
|
+
* Registry without reaching into the transport package for its configuration
|
|
10
|
+
* tag.
|
|
11
|
+
*
|
|
12
|
+
* @experimental This API is unstable and may change without notice.
|
|
13
|
+
*/
|
|
14
|
+
import * as Effect from "effect/Effect";
|
|
15
|
+
import { RegistryUrl } from "@agentxm/registry-client";
|
|
16
|
+
/** Read the selected Registry's endpoint and its human-readable host. */
|
|
17
|
+
export const selectedRegistry = Effect.map(RegistryUrl, (url) => ({ url, host: new URL(url).host }));
|
|
18
|
+
//# sourceMappingURL=selected-registry.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step-up verification: the protocol that carries one challenged Registry
|
|
3
|
+
* write through human verification and retries it exactly once.
|
|
4
|
+
*
|
|
5
|
+
* The capability owns challenge detection, request-reference validation,
|
|
6
|
+
* resumption and terminal-status rules, expiry, the bounded wait, and the
|
|
7
|
+
* retry-once contract. The application supplies only presentation and browser
|
|
8
|
+
* launch through the login presenter and interaction ports.
|
|
9
|
+
*
|
|
10
|
+
* @experimental This API is unstable and may change without notice.
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import { AuthClient } from "./auth-client.js";
|
|
14
|
+
import { CredentialStore } from "./credential-store.js";
|
|
15
|
+
import { StepUpVerificationPending, type AuthError } from "./errors.js";
|
|
16
|
+
import { AuthLoginInteraction } from "./login-interaction.js";
|
|
17
|
+
import { AuthLoginPresenter } from "./login-presenter.js";
|
|
18
|
+
/** How one challenged write names itself to a person watching the terminal. */
|
|
19
|
+
export interface StepUpPresentation {
|
|
20
|
+
/** Lifecycle label for the challenged write itself. */
|
|
21
|
+
readonly operationLabel: string;
|
|
22
|
+
/** Lifecycle label for the bounded wait on human verification. */
|
|
23
|
+
readonly waitingLabel: string;
|
|
24
|
+
}
|
|
25
|
+
/** The invocation's verification inputs, parsed from the command line. */
|
|
26
|
+
export interface StepUpOptions {
|
|
27
|
+
/** Resume this exact pending request instead of issuing a fresh challenge. */
|
|
28
|
+
readonly resumeReference?: string;
|
|
29
|
+
/** Bounded wait, in whole seconds, for the person to verify. */
|
|
30
|
+
readonly waitForHumanSeconds?: number;
|
|
31
|
+
/** No terminal is available to guide a person through verification. */
|
|
32
|
+
readonly unattended: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** A challenged write that completed, and whether verification was needed. */
|
|
35
|
+
export interface VerifiedWrite<A> {
|
|
36
|
+
readonly value: A;
|
|
37
|
+
readonly stepUpCompleted: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Run a Registry write that may be challenged, carry the challenge through
|
|
41
|
+
* human verification, and retry the write exactly once with the verified
|
|
42
|
+
* request id.
|
|
43
|
+
*
|
|
44
|
+
* The write is never replayed without verification, and a wait that elapses
|
|
45
|
+
* while the request is still valid resolves to `StepUpVerificationPending`
|
|
46
|
+
* rather than a failure of the write.
|
|
47
|
+
*/
|
|
48
|
+
export declare const runWithStepUp: <A, E, R>(operation: (stepUpRequestId?: string) => Effect.Effect<A, E, R>, presentation: StepUpPresentation, options: StepUpOptions, registryUrl: string) => Effect.Effect<VerifiedWrite<A>, E | AuthError | StepUpVerificationPending, R | AuthClient | AuthLoginInteraction | AuthLoginPresenter | CredentialStore>;
|
|
49
|
+
//# sourceMappingURL=step-up.d.ts.map
|