[codex] Structure cloud CLI token errors#3249
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
ApprovabilityVerdict: Needs human review Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
6c12fe1 to
1c4bd6e
Compare
dfc32cd to
76635e5
Compare
Dismissing prior approval to re-evaluate 76635e5
7fa3d68 to
565971f
Compare
Dismissing prior approval to re-evaluate 565971f
Dismissing prior approval to re-evaluate 565971f
565971f to
f0d9fb9
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Asset probe errors abort search
- Restored
Effect.orElseSucceed(() => false)onfileSystem.existscalls inresolveResourcePath,resolveIconPath, andresolveUserDataPathso I/O or permission errors on individual candidates are treated as 'not found' and the search continues to remaining alternatives.
- Restored
Or push these changes by commenting:
@cursor push 8c9f81cff2
Preview (8c9f81cff2)
diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts
--- a/apps/desktop/src/app/DesktopAppIdentity.test.ts
+++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts
@@ -153,7 +153,7 @@
),
);
- it.effect("preserves failures while inspecting the legacy userData path", () => {
+ it.effect("falls back to the default userData path when legacy path probe fails", () => {
const legacyPath = "/Users/alice/Library/Application Support/T3 Code (Alpha)";
const cause = PlatformError.systemError({
_tag: "PermissionDenied",
@@ -166,15 +166,9 @@
return withIdentity(
Effect.gen(function* () {
const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
- const error = yield* identity.resolveUserDataPath.pipe(Effect.flip);
+ const userDataPath = yield* identity.resolveUserDataPath;
- assert.instanceOf(error, DesktopAppIdentity.DesktopUserDataPathResolutionError);
- assert.equal(error.legacyPath, legacyPath);
- assert.strictEqual(error.cause, cause);
- assert.equal(
- error.message,
- `Failed to inspect legacy desktop user-data path at "${legacyPath}".`,
- );
+ assert.equal(userDataPath, "/Users/alice/Library/Application Support/t3code");
}),
{ legacyPathProbeError: cause },
);
diff --git a/apps/desktop/src/app/DesktopAppIdentity.ts b/apps/desktop/src/app/DesktopAppIdentity.ts
--- a/apps/desktop/src/app/DesktopAppIdentity.ts
+++ b/apps/desktop/src/app/DesktopAppIdentity.ts
@@ -33,7 +33,7 @@
export class DesktopAppIdentity extends Context.Service<
DesktopAppIdentity,
{
- readonly resolveUserDataPath: Effect.Effect<string, DesktopUserDataPathResolutionError>;
+ readonly resolveUserDataPath: Effect.Effect<string>;
readonly configure: Effect.Effect<void>;
}
>()("@t3tools/desktop/app/DesktopAppIdentity") {}
@@ -95,15 +95,9 @@
environment.appDataDirectory,
environment.legacyUserDataDirName,
);
- const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe(
- Effect.mapError(
- (cause) =>
- new DesktopUserDataPathResolutionError({
- legacyPath,
- cause,
- }),
- ),
- );
+ const legacyPathExists = yield* fileSystem
+ .exists(legacyPath)
+ .pipe(Effect.orElseSucceed(() => false));
return legacyPathExists
? legacyPath
: environment.path.join(environment.appDataDirectory, environment.userDataDirName);
diff --git a/apps/desktop/src/app/DesktopAssets.test.ts b/apps/desktop/src/app/DesktopAssets.test.ts
--- a/apps/desktop/src/app/DesktopAssets.test.ts
+++ b/apps/desktop/src/app/DesktopAssets.test.ts
@@ -3,6 +3,7 @@
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
import * as PlatformError from "effect/PlatformError";
import * as DesktopAssets from "./DesktopAssets.ts";
@@ -22,7 +23,7 @@
}).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))));
describe("DesktopAssets", () => {
- it.effect("preserves the failed asset candidate and filesystem cause", () =>
+ it.effect("treats a failed asset probe as not found and continues searching", () =>
Effect.gen(function* () {
const fileName = "custom.bin";
const candidatePath = "/repo/apps/desktop/resources/custom.bin";
@@ -41,17 +42,9 @@
);
const assets = yield* DesktopAssets.DesktopAssets.pipe(Effect.provide(assetsLayer));
- const error = yield* assets.resolveResourcePath(fileName).pipe(Effect.flip);
+ const result = yield* assets.resolveResourcePath(fileName);
- assert.instanceOf(error, DesktopAssets.DesktopAssetProbeError);
- assert.equal(error.fileName, fileName);
- assert.equal(error.candidatePath, candidatePath);
- assert.strictEqual(error.cause, cause);
- assert.equal(
- error.message,
- `Failed to probe desktop asset "${fileName}" at ${candidatePath}.`,
- );
- assert.notInclude(error.message, "private filesystem diagnostic");
+ assert.isTrue(Option.isNone(result));
}),
);
});
diff --git a/apps/desktop/src/app/DesktopAssets.ts b/apps/desktop/src/app/DesktopAssets.ts
--- a/apps/desktop/src/app/DesktopAssets.ts
+++ b/apps/desktop/src/app/DesktopAssets.ts
@@ -30,9 +30,7 @@
DesktopAssets,
{
readonly iconPaths: Effect.Effect<DesktopIconPaths>;
- readonly resolveResourcePath: (
- fileName: string,
- ) => Effect.Effect<Option.Option<string>, DesktopAssetProbeError>;
+ readonly resolveResourcePath: (fileName: string) => Effect.Effect<Option.Option<string>>;
}
>()("@t3tools/desktop/app/DesktopAssets") {}
@@ -40,20 +38,14 @@
fileName: string,
): Effect.fn.Return<
Option.Option<string>,
- DesktopAssetProbeError,
+ never,
FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
> {
const fileSystem = yield* FileSystem.FileSystem;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const candidates = environment.resolveResourcePathCandidates(fileName);
for (const candidate of candidates) {
- const exists = yield* fileSystem
- .exists(candidate)
- .pipe(
- Effect.mapError(
- (cause) => new DesktopAssetProbeError({ fileName, candidatePath: candidate, cause }),
- ),
- );
+ const exists = yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false));
if (exists) {
return Option.some(candidate);
}
@@ -65,23 +57,16 @@
ext: keyof DesktopIconPaths,
): Effect.fn.Return<
Option.Option<string>,
- DesktopAssetProbeError,
+ never,
FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
> {
const fileSystem = yield* FileSystem.FileSystem;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
if (environment.isDevelopment && environment.platform === "darwin" && ext === "png") {
const developmentDockIconPath = environment.developmentDockIconPath;
- const developmentDockIconExists = yield* fileSystem.exists(developmentDockIconPath).pipe(
- Effect.mapError(
- (cause) =>
- new DesktopAssetProbeError({
- fileName: "icon.png",
- candidatePath: developmentDockIconPath,
- cause,
- }),
- ),
- );
+ const developmentDockIconExists = yield* fileSystem
+ .exists(developmentDockIconPath)
+ .pipe(Effect.orElseSucceed(() => false));
if (developmentDockIconExists) {
return Option.some(developmentDockIconPath);
}
diff --git a/apps/server/src/auth/EnvironmentAuth.test.ts b/apps/server/src/auth/EnvironmentAuth.test.ts
--- a/apps/server/src/auth/EnvironmentAuth.test.ts
+++ b/apps/server/src/auth/EnvironmentAuth.test.ts
@@ -114,6 +114,16 @@
.pipe(Effect.flip);
expect(error._tag).toBe("ServerAuthScopeNotGrantedError");
+ if (error._tag === "ServerAuthScopeNotGrantedError") {
+ expect(error.requestedScopes).toEqual(["orchestration:read", "access:write"]);
+ expect(error.grantedScopes).toEqual([
+ "orchestration:read",
+ "orchestration:operate",
+ "terminal:operate",
+ "review:write",
+ "relay:read",
+ ]);
+ }
}).pipe(Effect.provide(makeEnvironmentAuthLayer())),
);
@@ -252,4 +262,20 @@
),
),
);
+
+ it.effect("retains both session ids when rejecting self-revocation", () =>
+ Effect.gen(function* () {
+ const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
+ const issued = yield* serverAuth.issueSession();
+ const error = yield* serverAuth
+ .revokeClientSession(issued.sessionId, issued.sessionId)
+ .pipe(Effect.flip);
+
+ expect(error._tag).toBe("ServerAuthForbiddenOperationError");
+ if (error._tag === "ServerAuthForbiddenOperationError") {
+ expect(error.currentSessionId).toBe(issued.sessionId);
+ expect(error.targetSessionId).toBe(issued.sessionId);
+ }
+ }).pipe(Effect.provide(makeEnvironmentAuthLayer())),
+ );
});
diff --git a/apps/server/src/auth/EnvironmentAuth.ts b/apps/server/src/auth/EnvironmentAuth.ts
--- a/apps/server/src/auth/EnvironmentAuth.ts
+++ b/apps/server/src/auth/EnvironmentAuth.ts
@@ -85,44 +85,50 @@
export class ServerAuthSessionCredentialValidationError extends Schema.TaggedErrorClass<ServerAuthSessionCredentialValidationError>()(
"ServerAuthSessionCredentialValidationError",
{
+ credentialKind: Schema.Literals(["session", "websocket-ticket"]),
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to validate session credential.";
+ return `Failed to validate ${this.credentialKind} credential.`;
}
}
export class ServerAuthAuthenticatedSessionIssueError extends Schema.TaggedErrorClass<ServerAuthAuthenticatedSessionIssueError>()(
"ServerAuthAuthenticatedSessionIssueError",
{
+ subject: Schema.String,
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to issue authenticated session.";
+ return `Failed to issue authenticated session for ${this.subject}.`;
}
}
export class ServerAuthAuthenticatedAccessTokenIssueError extends Schema.TaggedErrorClass<ServerAuthAuthenticatedAccessTokenIssueError>()(
"ServerAuthAuthenticatedAccessTokenIssueError",
{
+ subject: Schema.String,
+ scopes: Schema.Array(Schema.String),
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to issue authenticated access token.";
+ return `Failed to issue authenticated access token for ${this.subject} with scopes [${this.scopes.join(", ")}].`;
}
}
export class ServerAuthPairingLinkCreationError extends Schema.TaggedErrorClass<ServerAuthPairingLinkCreationError>()(
"ServerAuthPairingLinkCreationError",
{
+ subject: Schema.String,
+ scopes: Schema.Array(Schema.String),
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to create pairing link.";
+ return `Failed to create pairing link for ${this.subject} with scopes [${this.scopes.join(", ")}].`;
}
}
@@ -140,22 +146,25 @@
export class ServerAuthPairingLinkRevocationError extends Schema.TaggedErrorClass<ServerAuthPairingLinkRevocationError>()(
"ServerAuthPairingLinkRevocationError",
{
+ pairingLinkId: Schema.String,
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to revoke pairing link.";
+ return `Failed to revoke pairing link ${this.pairingLinkId}.`;
}
}
export class ServerAuthSessionTokenIssueError extends Schema.TaggedErrorClass<ServerAuthSessionTokenIssueError>()(
"ServerAuthSessionTokenIssueError",
{
+ subject: Schema.String,
+ scopes: Schema.Array(Schema.String),
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to issue session token.";
+ return `Failed to issue session token for ${this.subject} with scopes [${this.scopes.join(", ")}].`;
}
}
@@ -173,55 +182,63 @@
export class ServerAuthSessionRevocationError extends Schema.TaggedErrorClass<ServerAuthSessionRevocationError>()(
"ServerAuthSessionRevocationError",
{
+ sessionId: Schema.String,
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to revoke session.";
+ return `Failed to revoke session ${this.sessionId}.`;
}
}
export class ServerAuthOtherSessionsRevocationError extends Schema.TaggedErrorClass<ServerAuthOtherSessionsRevocationError>()(
"ServerAuthOtherSessionsRevocationError",
{
+ excludedSessionId: Schema.String,
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to revoke other sessions.";
+ return `Failed to revoke sessions other than ${this.excludedSessionId}.`;
}
}
export class ServerAuthWebSocketTokenIssueError extends Schema.TaggedErrorClass<ServerAuthWebSocketTokenIssueError>()(
"ServerAuthWebSocketTokenIssueError",
{
+ sessionId: Schema.String,
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to issue websocket token.";
+ return `Failed to issue websocket token for session ${this.sessionId}.`;
}
}
export class ServerAuthDpopReplayStateRecordError extends Schema.TaggedErrorClass<ServerAuthDpopReplayStateRecordError>()(
"ServerAuthDpopReplayStateRecordError",
{
+ proofKeyThumbprint: Schema.String,
+ proofId: Schema.String,
+ replayKey: Schema.String,
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to record DPoP proof replay state.";
+ return `Failed to record replay state for DPoP proof ${this.proofId} (${this.proofKeyThumbprint}).`;
}
}
export class ServerAuthDpopReplayKeyCalculationError extends Schema.TaggedErrorClass<ServerAuthDpopReplayKeyCalculationError>()(
"ServerAuthDpopReplayKeyCalculationError",
{
+ proofKeyThumbprint: Schema.String,
+ proofId: Schema.String,
...serverAuthInternalErrorContext,
},
) {
override get message(): string {
- return "Failed to calculate DPoP replay key.";
+ return `Failed to calculate replay key for DPoP proof ${this.proofId} (${this.proofKeyThumbprint}).`;
}
}
@@ -360,11 +377,6 @@
ServerAuthInvalidCredentialError,
]);
export type ServerAuthCredentialError = typeof ServerAuthCredentialError.Type;
-export const isServerAuthCredentialError = Schema.is(ServerAuthCredentialError);
-export const serverAuthCredentialReason = (
- error: ServerAuthCredentialError,
-): "missing_credential" | "invalid_credential" =>
- error._tag === "ServerAuthMissingCredentialError" ? "missing_credential" : "invalid_credential";
export class ServerAuthInvalidScopeError extends Schema.TaggedErrorClass<ServerAuthInvalidScopeError>()(
"ServerAuthInvalidScopeError",
@@ -377,10 +389,13 @@
export class ServerAuthScopeNotGrantedError extends Schema.TaggedErrorClass<ServerAuthScopeNotGrantedError>()(
"ServerAuthScopeNotGrantedError",
- {},
+ {
+ requestedScopes: Schema.Array(Schema.String),
+ grantedScopes: Schema.Array(Schema.String),
+ },
) {
override get message(): string {
- return "The requested authentication scope was not granted.";
+ return `Requested scopes [${this.requestedScopes.join(", ")}] exceed granted scopes [${this.grantedScopes.join(", ")}].`;
}
}
@@ -389,28 +404,35 @@
ServerAuthScopeNotGrantedError,
]);
export type ServerAuthInvalidRequestError = typeof ServerAuthInvalidRequestError.Type;
-export const isServerAuthInvalidRequestError = Schema.is(ServerAuthInvalidRequestError);
-export const serverAuthInvalidRequestReason = (
- error: ServerAuthInvalidRequestError,
-): "invalid_scope" | "scope_not_granted" =>
- error._tag === "ServerAuthInvalidScopeError" ? "invalid_scope" : "scope_not_granted";
export class ServerAuthForbiddenOperationError extends Schema.TaggedErrorClass<ServerAuthForbiddenOperationError>()(
"ServerAuthForbiddenOperationError",
- {},
+ {
+ currentSessionId: Schema.String,
+ targetSessionId: Schema.String,
+ },
) {
override get message(): string {
- return "The current authentication session cannot revoke itself.";
+ return `Authentication session ${this.currentSessionId} cannot revoke itself.`;
}
}
+export type ServerAuthAuthenticationInternalError =
+ | ServerAuthSessionCredentialValidationError
+ | ServerAuthDpopReplayStateRecordError
+ | ServerAuthDpopReplayKeyCalculationError;
+
+export type ServerAuthAuthenticationError =
+ | ServerAuthCredentialError
+ | ServerAuthAuthenticationInternalError;
+
export class EnvironmentAuth extends Context.Service<
EnvironmentAuth,
{
readonly getDescriptor: () => Effect.Effect<ServerAuthDescriptor>;
readonly getSessionState: (
request: HttpServerRequest.HttpServerRequest,
- ) => Effect.Effect<AuthSessionState, ServerAuthInternalError>;
+ ) => Effect.Effect<AuthSessionState, ServerAuthAuthenticationInternalError>;
readonly createBrowserSession: (
credential: string,
requestMetadata: AuthClientMetadata,
@@ -419,7 +441,9 @@
readonly response: AuthBrowserSessionResult;
readonly sessionToken: string;
},
- ServerAuthInvalidCredentialError | ServerAuthInternalError
+ | ServerAuthInvalidCredentialError
+ | ServerAuthBootstrapCredentialValidationError
+ | ServerAuthAuthenticatedSessionIssueError
>;
readonly exchangeBootstrapCredentialForAccessToken: (
credential: string,
@@ -430,7 +454,10 @@
},
) => Effect.Effect<
AuthAccessTokenResult,
- ServerAuthInvalidCredentialError | ServerAuthInvalidRequestError | ServerAuthInternalError
+ | ServerAuthInvalidCredentialError
+ | ServerAuthScopeNotGrantedError
+ | ServerAuthBootstrapCredentialValidationError
+ | ServerAuthAuthenticatedAccessTokenIssueError
>;
readonly createPairingLink: (input?: {
readonly ttl?: Duration.Duration;
@@ -438,56 +465,61 @@
readonly scopes?: ReadonlyArray<AuthEnvironmentScope>;
readonly subject?: string;
readonly proofKeyThumbprint?: string;
- }) => Effect.Effect<IssuedPairingLink, ServerAuthInternalError>;
+ }) => Effect.Effect<IssuedPairingLink, ServerAuthPairingLinkCreationError>;
readonly issuePairingCredential: (
input?: AuthCreatePairingCredentialInput,
- ) => Effect.Effect<AuthPairingCredentialResult, ServerAuthInternalError>;
+ ) => Effect.Effect<AuthPairingCredentialResult, ServerAuthPairingLinkCreationError>;
readonly issueStartupPairingCredential: () => Effect.Effect<
AuthPairingCredentialResult,
- ServerAuthInternalError
+ ServerAuthPairingLinkCreationError
>;
readonly listPairingLinks: (input?: {
readonly excludeSubjects?: ReadonlyArray<string>;
- }) => Effect.Effect<ReadonlyArray<AuthPairingLink>, ServerAuthInternalError>;
- readonly revokePairingLink: (id: string) => Effect.Effect<boolean, ServerAuthInternalError>;
+ }) => Effect.Effect<ReadonlyArray<AuthPairingLink>, ServerAuthPairingLinksListError>;
+ readonly revokePairingLink: (
+ id: string,
+ ) => Effect.Effect<boolean, ServerAuthPairingLinkRevocationError>;
readonly issueSession: (input?: {
readonly ttl?: Duration.Duration;
readonly subject?: string;
readonly scopes?: ReadonlyArray<AuthEnvironmentScope>;
readonly label?: string;
- }) => Effect.Effect<IssuedBearerSession, ServerAuthInternalError>;
+ }) => Effect.Effect<IssuedBearerSession, ServerAuthSessionTokenIssueError>;
readonly listSessions: () => Effect.Effect<
ReadonlyArray<AuthClientSession>,
- ServerAuthInternalError
+ ServerAuthSessionsListError
>;
readonly revokeSession: (
sessionId: AuthSessionId,
- ) => Effect.Effect<boolean, ServerAuthInternalError>;
+ ) => Effect.Effect<boolean, ServerAuthSessionRevocationError>;
readonly revokeOtherSessionsExcept: (
sessionId: AuthSessionId,
- ) => Effect.Effect<number, ServerAuthInternalError>;
+ ) => Effect.Effect<number, ServerAuthOtherSessionsRevocationError>;
readonly listClientSessions: (
currentSessionId: AuthSessionId,
- ) => Effect.Effect<ReadonlyArray<AuthClientSession>, ServerAuthInternalError>;
+ ) => Effect.Effect<ReadonlyArray<AuthClientSession>, ServerAuthSessionsListError>;
readonly revokeClientSession: (
currentSessionId: AuthSessionId,
targetSessionId: AuthSessionId,
- ) => Effect.Effect<boolean, ServerAuthForbiddenOperationError | ServerAuthInternalError>;
+ ) => Effect.Effect<
+ boolean,
+ ServerAuthForbiddenOperationError | ServerAuthSessionRevocationError
+ >;
readonly revokeOtherClientSessions: (
currentSessionId: AuthSessionId,
- ) => Effect.Effect<number, ServerAuthInternalError>;
+ ) => Effect.Effect<number, ServerAuthOtherSessionsRevocationError>;
readonly authenticateHttpRequest: (
request: HttpServerRequest.HttpServerRequest,
- ) => Effect.Effect<AuthenticatedSession, ServerAuthCredentialError | ServerAuthInternalError>;
+ ) => Effect.Effect<AuthenticatedSession, ServerAuthAuthenticationError>;
readonly authenticateWebSocketUpgrade: (
request: HttpServerRequest.HttpServerRequest,
- ) => Effect.Effect<AuthenticatedSession, ServerAuthCredentialError | ServerAuthInternalError>;
+ ) => Effect.Effect<AuthenticatedSession, ServerAuthAuthenticationError>;
readonly issueWebSocketTicket: (
session: Pick<AuthenticatedSession, "sessionId">,
- ) => Effect.Effect<AuthWebSocketTicketResult, ServerAuthInternalError>;
+ ) => Effect.Effect<AuthWebSocketTicketResult, ServerAuthWebSocketTokenIssueError>;
readonly issueStartupPairingUrl: (
baseUrl: string,
- ) => Effect.Effect<string, ServerAuthInternalError>;
+ ) => Effect.Effect<string, ServerAuthPairingLinkCreationError>;
}
>()("t3/auth/EnvironmentAuth") {}
@@ -514,7 +546,7 @@
export function toBootstrapExchangeError(
cause: PairingGrantStore.BootstrapCredentialError,
-): ServerAuthInvalidCredentialError | ServerAuthInternalError {
+): ServerAuthInvalidCredentialError | ServerAuthBootstrapCredentialValidationError {
if (PairingGrantStore.isBootstrapCredentialInternalError(cause)) {
return new ServerAuthBootstrapCredentialValidationError({ cause });
}
@@ -526,12 +558,17 @@
const mapSessionVerificationErrors = <A, R>(
effect: Effect.Effect<A, SessionStore.SessionCredentialError, R>,
-): Effect.Effect<A, ServerAuthInvalidCredentialError | ServerAuthInternalError, R> =>
+ credentialKind: ServerAuthSessionCredentialValidationError["credentialKind"],
+): Effect.Effect<
+ A,
+ ServerAuthInvalidCredentialError | ServerAuthSessionCredentialValidationError,
+ R
+> =>
effect.pipe(
Effect.mapError((cause) =>
SessionStore.isSessionCredentialInvalidError(cause)
? new ServerAuthInvalidCredentialError({ cause })
- : new ServerAuthSessionCredentialValidationError({ cause }),
+ : new ServerAuthSessionCredentialValidationError({ credentialKind, cause }),
),
);
@@ -565,7 +602,7 @@
token: string,
): Effect.Effect<
AuthenticatedSession,
- ServerAuthInvalidCredentialError | ServerAuthInternalError
+ ServerAuthInvalidCredentialError | ServerAuthSessionCredentialValidationError
> =>
sessions.verify(token).pipe(
Effect.tapError((cause) =>
@@ -585,12 +622,12 @@
...(session.proofKeyThumbprint ? { proofKeyThumbprint: session.proofKeyThumbprint } : {}),
...(session.expiresAt ? { expiresAt: session.expiresAt } : {}),
})),
- mapSessionVerificationErrors,
+ (effect) => mapSessionVerificationErrors(effect, "session"),
);
const authenticateRequest = (
request: HttpServerRequest.HttpServerRequest,
- ): Effect.Effect<AuthenticatedSession, ServerAuthCredentialError | ServerAuthInternalError> => {
+ ): Effect.Effect<AuthenticatedSession, ServerAuthAuthenticationError> => {
const cookieToken = request.cookies[sessions.cookieName];
const bearerToken = parseBearerToken(request);
const dpopToken = parseDpopToken(request);
@@ -642,12 +679,18 @@
...(session.expiresAt ? { expiresAt: DateTime.toUtc(session.expiresAt) } : {}),
}) satisfies AuthSessionState,
),
- Effect.catchIf(isServerAuthCredentialError, () =>
- Effect.succeed({
- authenticated: false,
- auth: descriptor,
- } satisfies AuthSessionState),
- ),
+ Effect.catchTags({
+ ServerAuthMissingCredentialError: () =>
+ Effect.succeed({
+ authenticated: false,
+ auth: descriptor,
+ } satisfies AuthSessionState),
+ ServerAuthInvalidCredentialError: () =>
+ Effect.succeed({
+ authenticated: false,
+ auth: descriptor,
+ } satisfies AuthSessionState),
+ }),
Effect.withSpan("EnvironmentAuth.getSessionState"),
);
@@ -669,7 +712,13 @@
},
})
.pipe(
- Effect.mapError((cause) => new ServerAuthAuthenticatedSessionIssueError({ cause })),
+ Effect.mapError(
+ (cause) =>
+ new ServerAuthAuthenticatedSessionIssueError({
+ subject: grant.subject,
+ cause,
+ }),
+ ),
),
),
Effect.map(
@@ -695,7 +744,10 @@
Effect.gen(function* () {
const grantedScopes = requestedScopes ?? grant.scopes;
if (!grantedScopes.every((scope) => grant.scopes.includes(scope))) {
- return yield* new ServerAuthScopeNotGrantedError({});
+ return yield* new ServerAuthScopeNotGrantedError({
+ requestedScopes: grantedScopes,
+ grantedScopes: grant.scopes,
+ });
}
return yield* sessions
.issue({
@@ -715,7 +767,12 @@
})
.pipe(
Effect.mapError(
- (cause) => new ServerAuthAuthenticatedAccessTokenIssueError({ cause }),
+ (cause) =>
+ new ServerAuthAuthenticatedAccessTokenIssueError({
+ subject: grant.subject,
+ scopes: grantedScopes,
+ cause,
+ }),
),
);
}),
@@ -765,28 +822,33 @@
const createPairingLink: EnvironmentAuth["Service"]["createPairingLink"] = Effect.fn(
"EnvironmentAuth.createPairingLink",
- )(
- function* (input) {
- const createdAt = yield* DateTime.now;
- const issued = yield* bootstrapCredentials.issueOneTimeToken({
- scopes: input?.scopes ?? AuthStandardClientScopes,
- subject: input?.subject ?? "one-time-token",
+ )(function* (input) {
+ const scopes = input?.scopes ?? AuthStandardClientScopes;
+ const subject = input?.subject ?? "one-time-token";
+ const createdAt = yield* DateTime.now;
+ const issued = yield* bootstrapCredentials
+ .issueOneTimeToken({
+ scopes,
+ subject,
...(input?.ttl ? { ttl: input.ttl } : {}),
...(input?.label ? { label: input.label } : {}),
...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint } : {}),
- });
- return {
- id: issued.id,
- credential: issued.credential,
- scopes: input?.scopes ?? AuthStandardClientScopes,
- subject: input?.subject ?? "one-time-token",
- ...(issued.label ? { label: issued.label } : {}),
- createdAt: DateTime.toUtc(createdAt),
- expiresAt: DateTime.toUtc(issued.expiresAt),
- } satisfies IssuedPairingLink;
- },
- Effect.mapError((cause) => new ServerAuthPairingLinkCreationError({ cause })),
- );
+ })
+ .pipe(
+ Effect.mapError(
+ (cause) => new ServerAuthPairingLinkCreationError({ subject, scopes, cause }),
+ ),
+ );
+ return {
+ id: issued.id,
+ credential: issued.credential,
+ scopes,
+ subject,
+ ...(issued.label ? { label: issued.label } : {}),
+ createdAt: DateTime.toUtc(createdAt),
+ expiresAt: DateTime.toUtc(issued.expiresAt),
+ } satisfies IssuedPairingLink;
+ });
const listPairingLinks: EnvironmentAuth["Service"]["listPairingLinks"] = (input) =>
bootstrapCredentials.listActive().pipe(
@@ -806,16 +868,20 @@
const revokePairingLink: EnvironmentAuth["Service"]["revokePairingLink"] = (id) =>
bootstrapCredentials.revoke(id).pipe(
- Effect.mapError((cause) => new ServerAuthPairingLinkRevocationError({ cause })),
+ Effect.mapError(
+ (cause) => new ServerAuthPairingLinkRevocationError({ pairingLinkId: id, cause }),
+ ),
Effect.withSpan("EnvironmentAuth.revokePairingLink"),
);
- const issueSession: EnvironmentAuth["Service"]["issueSession"] = (input) =>
- sessions
+ const issueSession: EnvironmentAuth["Service"]["issueSession"] = (input) => {
+ const subject = input?.subject ?? DEFAULT_SESSION_SUBJECT;
+ const scopes = input?.scopes ?? AuthAdministrativeScopes;
+ return sessions
.issue({
- subject: input?.subject ?? DEFAULT_SESSION_SUBJECT,
+ subject,
method: "bearer-access-token",
- scopes: input?.scopes ?? AuthAdministrativeScopes,
+ scopes,
client: {
...(input?.label ? { label: input.label } : {}),
deviceType: "bot",
@@ -830,14 +896,17 @@
token: issued.token,
method: "bearer-access-token",
scopes: issued.scopes,
- subject: input?.subject ?? DEFAULT_SESSION_SUBJECT,
+ subject,
client: issued.client,
expiresAt: DateTime.toUtc(issued.expiresAt),
}) satisfies IssuedBearerSession,
),
- Effect.mapError((cause) => new ServerAuthSessionTokenIssueError({ cause })),
+ Effect.mapError(
+ (cause) => new ServerAuthSessionTokenIssueError({ subject, scopes, cause }),
+ ),
Effect.withSpan("EnvironmentAuth.issueSession"),
);
+ };
const listSessions: EnvironmentAuth["Service"]["listSessions"] = () =>
sessions.listActive().pipe(
@@ -848,7 +917,7 @@
const revokeSession: EnvironmentAuth["Service"]["revokeSession"] = (sessionId) =>
sessions.revoke(sessionId).pipe(
- Effect.mapError((cause) => new ServerAuthSessionRevocationError({ cause })),
+ Effect.mapError((cause) => new ServerAuthSessionRevocationError({ sessionId, cause })),
Effect.withSpan("EnvironmentAuth.revokeSession"),
);
@@ -856,7 +925,10 @@
sessionId,
) =>
sessions.revokeAllExcept(sessionId).pipe(
- Effect.mapError((cause) => new ServerAuthOtherSessionsRevocationError({ cause })),
+ Effect.mapError(
+ (cause) =>
+ new ServerAuthOtherSessionsRevocationError({ excludedSessionId: sessionId, cause }),
+ ),
Effect.withSpan("EnvironmentAuth.revokeOtherSessionsExcept"),
);
@@ -891,7 +963,10 @@
"EnvironmentAuth.revokeClientSession",
)(function* (currentSessionId, targetSessionId) {
if (currentSessionId === targetSessionId) {
- return yield* new ServerAuthForbiddenOperationError({});
+ return yield* new ServerAuthForbiddenOperationError({
+ currentSessionId,
+ targetSessionId,
+ });
}
return yield* revokeSession(targetSessionId);
});
@@ -917,7 +992,9 @@
const issueWebSocketTicket: EnvironmentAuth["Service"]["issueWebSocketTicket"] = (session) =>
sessions.issueWebSocketToken(session.sessionId).pipe(
- Effect.mapError((cause) => new ServerAuthWebSocketTokenIssueError({ cause })),
+ Effect.mapError(
+ (cause) => new ServerAuthWebSocketTokenIssueError({ sessionId: session.sessionId, cause }),
+ ),
Effect.map(
... diff truncated: showing 800 of 3579 linesYou can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit f0d9fb9. Configure here.
| Effect.mapError( | ||
| (cause) => new DesktopAssetProbeError({ fileName, candidatePath: candidate, cause }), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
Asset probe errors abort search
High Severity
Changes to fileSystem.exists now propagate I/O or permission errors as fatal, rather than treating them as if the file or path doesn't exist. This prevents the application from finding valid resource paths (like icons) or user data directories, potentially blocking desktop startup even when alternatives are available.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f0d9fb9. Configure here.
3e06261 to
98a7351
Compare
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
98a7351 to
95a9380
Compare
0f8b837
into
codex/server-auth-error-boundaries
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>



Summary
Validation
vp test apps/server/src/cloud(33 tests)vp check(passes with 20 pre-existing warnings)vp run typecheckStacked on #3246 so the cloud HTTP error boundary lands first.
Note
Medium Risk
Touches OAuth token refresh/login and credential secret-store I/O; error tags and HTTP boundary handling changed, though behavior is mostly classification and safer logging rather than new auth logic.
Overview
Cloud CLI token manager errors are no longer wrapped by a generic
wrapErrorhelper. Each OAuth/credential step maps failures to tagged errors withstage,secretName, preservedcause, and redacted URL diagnostics (length, protocol, hostname only—no raw token/callback URLs in messages or JSON).CloudCliTokenManagerservice methods now expose narrower error channels (clear→ removal only,hasCredential→ read,getExisting→ refresh,get→ refresh/authorization/timeout). Login and refresh paths tag failures at stages likeload-oauth-config,prepare-pkce,start-callback-server, andexchange-token.Cloud HTTP
reconcileDesiredCloudLinkdrops several CLI errorcatchTaghandlers; it only mapsCloudCliCredentialRefreshErrorfromgetExisting(other CLI errors are no longer in that reconcile path’s catch set).Adds
CliTokenManager.test.tscovering URL redaction, removal/read/decode failure classification, and stable messages.Reviewed by Cursor Bugbot for commit 95a9380. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Structure CLI token errors with stage, URL diagnostics, and secret context
CloudCliCredentialRefreshError,CloudCliAuthorizationError,CloudCliAuthorizationTimeoutError,CloudCliCredentialReadError,CloudCliCredentialRemovalError) with structured fields: stage,secretName, token endpoint/redirect URI diagnostics, and callback host/port.wrapErrorhelper with explicit static constructors (fromStage,fromCredentialRead,fromCredentialPersist,fromRedirectUri) on each error class, so error messages now include stage and URL diagnostic context.getUrlDiagnosticsto strip sensitive credentials fromtokenEndpointandredirectUribefore embedding them in errors.reconcileDesiredLinkWithhandler in http.ts; onlyCloudCliCredentialRefreshErroris still translated there.Macroscope summarized 95a9380.