History

Structure cloud CLI token errors

Julius Marminge committed 4 months ago

Commit dfc32cd1 parent 6c12fe13 files · +347 −53Browse files

Unified · Split

3 files
  1. apps/server/src/cloud/CliTokenManager.test.ts +121 −0
  2. apps/server/src/cloud/CliTokenManager.ts +226 −47
  3. apps/server/src/cloud/http.ts +0 −6
apps/server/src/cloud/CliTokenManager.test.ts+121−0
@@ -0,0 +1,121 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as PlatformError from "effect/PlatformError";
7import * as HttpClient from "effect/unstable/http/HttpClient";
8
9import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
10import * as CliTokenManager from "./CliTokenManager.ts";
11
12const unusedSecretStoreOperation = () => Effect.die("unused secret-store operation");
13
14function makeSecretStore(
15 overrides: Partial<ServerSecretStore.ServerSecretStore["Service"]>,
16): ServerSecretStore.ServerSecretStore["Service"] {
17 return {
18 get: unusedSecretStoreOperation,
19 set: unusedSecretStoreOperation,
20 create: unusedSecretStoreOperation,
21 getOrCreateRandom: unusedSecretStoreOperation,
22 remove: unusedSecretStoreOperation,
23 ...overrides,
24 };
25}
26
27function makeTokenManager(secretStore: ServerSecretStore.ServerSecretStore["Service"]) {
28 return CliTokenManager.make.pipe(
29 Effect.provide(
30 Layer.mergeAll(
31 NodeServices.layer,
32 Layer.succeed(ServerSecretStore.ServerSecretStore, secretStore),
33 Layer.succeed(
34 HttpClient.HttpClient,
35 HttpClient.make(() => Effect.die("unused HTTP client")),
36 ),
37 ),
38 ),
39 );
40}
41
42describe("CloudCliTokenManager", () => {
43 it.effect("retains secret context and cause when credential removal fails", () => {
44 const failure = new ServerSecretStore.SecretStoreRemoveError({
45 secretName: "cloud-cli-oauth-token",
46 secretPath: "/tmp/secrets/cloud-cli-oauth-token.bin",
47 cause: PlatformError.systemError({
48 _tag: "PermissionDenied",
49 module: "FileSystem",
50 method: "remove",
51 pathOrDescriptor: "/tmp/secrets/cloud-cli-oauth-token.bin",
52 }),
53 });
54
55 return Effect.gen(function* () {
56 const tokens = yield* makeTokenManager(
57 makeSecretStore({ remove: () => Effect.fail(failure) }),
58 );
59 const error = yield* Effect.flip(tokens.clear);
60
61 expect(error).toMatchObject({
62 _tag: "CloudCliCredentialRemovalError",
63 secretName: "cloud-cli-oauth-token",
64 cause: failure,
65 });
66 expect(error.message).toBe(
67 "Could not remove the stored T3 Connect CLI credential cloud-cli-oauth-token.",
68 );
69 });
70 });
71
72 it.effect("classifies credential read failures without replacing the cause", () => {
73 const failure = new ServerSecretStore.SecretStoreReadError({
74 secretName: "cloud-cli-oauth-token",
75 secretPath: "/tmp/secrets/cloud-cli-oauth-token.bin",
76 cause: PlatformError.systemError({
77 _tag: "PermissionDenied",
78 module: "FileSystem",
79 method: "readFile",
80 pathOrDescriptor: "/tmp/secrets/cloud-cli-oauth-token.bin",
81 }),
82 });
83
84 return Effect.gen(function* () {
85 const tokens = yield* makeTokenManager(makeSecretStore({ get: () => Effect.fail(failure) }));
86 const error = yield* Effect.flip(tokens.hasCredential);
87
88 expect(error).toMatchObject({
89 _tag: "CloudCliCredentialReadError",
90 stage: "read-credential",
91 secretName: "cloud-cli-oauth-token",
92 cause: failure,
93 });
94 expect(error.message).toBe(
95 "Could not inspect the stored T3 Connect CLI credential cloud-cli-oauth-token during read-credential.",
96 );
97 });
98 });
99
100 it.effect("classifies malformed persisted credentials as refresh decode failures", () =>
101 Effect.gen(function* () {
102 const tokens = yield* makeTokenManager(
103 makeSecretStore({
104 get: () =>
105 Effect.succeed(Option.some(new TextEncoder().encode("not valid credential JSON"))),
106 }),
107 );
108 const error = yield* Effect.flip(tokens.getExisting);
109
110 expect(error).toMatchObject({
111 _tag: "CloudCliCredentialRefreshError",
112 stage: "decode-credential",
113 secretName: "cloud-cli-oauth-token",
114 cause: { _tag: "SchemaError" },
115 });
116 expect(error.message).toBe(
117 "Could not refresh the T3 Connect CLI credential cloud-cli-oauth-token during decode-credential.",
118 );
119 }),
120 );
121});
apps/server/src/cloud/CliTokenManager.ts+226−47
@@ -27,6 +27,8 @@ import { cloudCliOAuthConfig, type CloudCliOAuthConfig } from "./publicConfig.ts · 26 unmodified lines
27const CLOUD_CLI_OAUTH_TOKEN_SECRET = "cloud-cli-oauth-token";27const CLOUD_CLI_OAUTH_TOKEN_SECRET = "cloud-cli-oauth-token";
28const CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT = Duration.minutes(10);28const CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT = Duration.minutes(10);
29const CLOUD_CLI_OAUTH_REFRESH_EARLY_MS = Duration.toMillis(Duration.minutes(5));29const CLOUD_CLI_OAUTH_REFRESH_EARLY_MS = Duration.toMillis(Duration.minutes(5));
30const CLOUD_CLI_OAUTH_CALLBACK_HOST = "127.0.0.1";
31const CLOUD_CLI_OAUTH_CALLBACK_PORT = 34338;
30 32
31const PersistedToken = Schema.Struct({33const PersistedToken = Schema.Struct({
32 accessToken: Schema.String,34 accessToken: Schema.String,
@@ -46,48 +48,136 @@ const OAuthTokenResponse = Schema.Struct({ · 13 unmodified lines
46 token_type: Schema.String,48 token_type: Schema.String,
47});49});
48 50
51type CredentialReadFailure = ServerSecretStore.SecretStoreReadError | Schema.SchemaError;
52
53type CredentialPersistFailure =
54 | Schema.SchemaError
55 | ServerSecretStore.SecretStoreTemporaryPathGenerationError
56 | ServerSecretStore.SecretStorePersistError;
57
58const CloudCliCredentialRefreshStage = Schema.Literals([
59 "read-credential",
60 "decode-credential",
61 "load-oauth-config",
62 "exchange-token",
63 "encode-credential",
64 "persist-credential",
65]);
66
67const CloudCliAuthorizationStage = Schema.Literals([
68 "load-oauth-config",
69 "prepare-pkce",
70 "start-callback-server",
71 "exchange-token",
72 "encode-credential",
73 "persist-credential",
74]);
75
49export class CloudCliCredentialRemovalError extends Schema.TaggedErrorClass<CloudCliCredentialRemovalError>()(76export class CloudCliCredentialRemovalError extends Schema.TaggedErrorClass<CloudCliCredentialRemovalError>()(
50 "CloudCliCredentialRemovalError",77 "CloudCliCredentialRemovalError",
51 { cause: Schema.Defect() },78 {
79 secretName: Schema.Literal(CLOUD_CLI_OAUTH_TOKEN_SECRET),
80 cause: Schema.Defect(),
81 },
52) {82) {
53 override get message(): string {83 override get message(): string {
54 return "Could not remove the stored T3 Connect CLI credential.";84 return `Could not remove the stored T3 Connect CLI credential ${this.secretName}.`;
55 }85 }
56}86}
57 87
58export class CloudCliCredentialRefreshError extends Schema.TaggedErrorClass<CloudCliCredentialRefreshError>()(88export class CloudCliCredentialRefreshError extends Schema.TaggedErrorClass<CloudCliCredentialRefreshError>()(
59 "CloudCliCredentialRefreshError",89 "CloudCliCredentialRefreshError",
60 { cause: Schema.Defect() },90 {
91 stage: CloudCliCredentialRefreshStage,
92 secretName: Schema.Literal(CLOUD_CLI_OAUTH_TOKEN_SECRET),
93 tokenEndpoint: Schema.optional(Schema.String),
94 cause: Schema.Defect(),
95 },
61) {96) {
97 static fromCredentialRead(cause: CredentialReadFailure): CloudCliCredentialRefreshError {
98 return new CloudCliCredentialRefreshError({
99 stage: cause._tag === "SecretStoreReadError" ? "read-credential" : "decode-credential",
100 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
101 cause,
102 });
103 }
104
105 static fromCredentialPersist(cause: CredentialPersistFailure): CloudCliCredentialRefreshError {
106 return new CloudCliCredentialRefreshError({
107 stage: cause._tag === "SchemaError" ? "encode-credential" : "persist-credential",
108 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
109 cause,
110 });
111 }
112
62 override get message(): string {113 override get message(): string {
63 return "Could not refresh the T3 Connect CLI credential.";114 const tokenEndpoint = this.tokenEndpoint ? ` using ${this.tokenEndpoint}` : "";
115 return `Could not refresh the T3 Connect CLI credential ${this.secretName} during ${this.stage}${tokenEndpoint}.`;
64 }116 }
65}117}
66 118
67export class CloudCliCredentialReadError extends Schema.TaggedErrorClass<CloudCliCredentialReadError>()(119export class CloudCliCredentialReadError extends Schema.TaggedErrorClass<CloudCliCredentialReadError>()(
68 "CloudCliCredentialReadError",120 "CloudCliCredentialReadError",
69 { cause: Schema.Defect() },121 {
122 stage: Schema.Literals(["read-credential", "decode-credential"]),
123 secretName: Schema.Literal(CLOUD_CLI_OAUTH_TOKEN_SECRET),
124 cause: Schema.Defect(),
125 },
70) {126) {
127 static fromCredentialRead(cause: CredentialReadFailure): CloudCliCredentialReadError {
128 return new CloudCliCredentialReadError({
129 stage: cause._tag === "SecretStoreReadError" ? "read-credential" : "decode-credential",
130 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
131 cause,
132 });
133 }
134
71 override get message(): string {135 override get message(): string {
72 return "Could not read the stored T3 Connect CLI credential.";136 return `Could not inspect the stored T3 Connect CLI credential ${this.secretName} during ${this.stage}.`;
73 }137 }
74}138}
75 139
76export class CloudCliAuthorizationError extends Schema.TaggedErrorClass<CloudCliAuthorizationError>()(140export class CloudCliAuthorizationError extends Schema.TaggedErrorClass<CloudCliAuthorizationError>()(
77 "CloudCliAuthorizationError",141 "CloudCliAuthorizationError",
78 { cause: Schema.Defect() },142 {
143 stage: CloudCliAuthorizationStage,
144 secretName: Schema.Literal(CLOUD_CLI_OAUTH_TOKEN_SECRET),
145 tokenEndpoint: Schema.optional(Schema.String),
146 redirectUri: Schema.optional(Schema.String),
147 callbackHost: Schema.optional(Schema.String),
148 callbackPort: Schema.optional(Schema.Number),
149 cause: Schema.Defect(),
150 },
79) {151) {
152 static fromCredentialPersist(cause: CredentialPersistFailure): CloudCliAuthorizationError {
153 return new CloudCliAuthorizationError({
154 stage: cause._tag === "SchemaError" ? "encode-credential" : "persist-credential",
155 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
156 cause,
157 });
158 }
159
80 override get message(): string {160 override get message(): string {
81 return "Could not authorize the T3 Connect CLI.";161 const tokenEndpoint = this.tokenEndpoint ? ` using ${this.tokenEndpoint}` : "";
162 const redirectUri = this.redirectUri ? ` with callback ${this.redirectUri}` : "";
163 const callbackAddress =
164 this.callbackHost && this.callbackPort !== undefined
165 ? ` on ${this.callbackHost}:${this.callbackPort}`
166 : "";
167 return `Could not authorize the T3 Connect CLI credential ${this.secretName} during ${this.stage}${tokenEndpoint}${redirectUri}${callbackAddress}.`;
82 }168 }
83}169}
84 170
85export class CloudCliAuthorizationTimeoutError extends Schema.TaggedErrorClass<CloudCliAuthorizationTimeoutError>()(171export class CloudCliAuthorizationTimeoutError extends Schema.TaggedErrorClass<CloudCliAuthorizationTimeoutError>()(
86 "CloudCliAuthorizationTimeoutError",172 "CloudCliAuthorizationTimeoutError",
87 { cause: Schema.Defect() },173 {
174 redirectUri: Schema.String,
175 timeoutMillis: Schema.Number,
176 cause: Schema.Defect(),
177 },
88) {178) {
89 override get message(): string {179 override get message(): string {
90 return "Timed out waiting for T3 Connect authorization.";180 return `Timed out after ${this.timeoutMillis}ms waiting for T3 Connect authorization at ${this.redirectUri}.`;
91 }181 }
92}182}
93 183
@@ -103,18 +193,21 @@ export type CloudCliTokenManagerError = typeof CloudCliTokenManagerError.Type; · 9 unmodified lines
103export class CloudCliTokenManager extends Context.Service<193export class CloudCliTokenManager extends Context.Service<
104 CloudCliTokenManager,194 CloudCliTokenManager,
105 {195 {
106 readonly get: Effect.Effect<PersistedToken, CloudCliTokenManagerError>;196 readonly get: Effect.Effect<
107 readonly getExisting: Effect.Effect<Option.Option<PersistedToken>, CloudCliTokenManagerError>;197 PersistedToken,
108 readonly hasCredential: Effect.Effect<boolean, CloudCliTokenManagerError>;198 | CloudCliCredentialRefreshError
109 readonly clear: Effect.Effect<void, CloudCliTokenManagerError>;199 | CloudCliAuthorizationError
200 | CloudCliAuthorizationTimeoutError
201 >;
202 readonly getExisting: Effect.Effect<
203 Option.Option<PersistedToken>,
204 CloudCliCredentialRefreshError
205 >;
206 readonly hasCredential: Effect.Effect<boolean, CloudCliCredentialReadError>;
207 readonly clear: Effect.Effect<void, CloudCliCredentialRemovalError>;
110 }208 }
111>()("t3/cloud/CliTokenManager/CloudCliTokenManager") {}209>()("t3/cloud/CliTokenManager/CloudCliTokenManager") {}
112 210
113const wrapError =
114 <WrappedError extends CloudCliTokenManagerError>(makeError: (cause: unknown) => WrappedError) =>
115 <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, WrappedError, R> =>
116 effect.pipe(Effect.mapError(makeError));
117
118function stringToBytes(value: string): Uint8Array {211function stringToBytes(value: string): Uint8Array {
119 return new TextEncoder().encode(value);212 return new TextEncoder().encode(value);
120}213}
@@ -134,9 +227,15 @@ export const make = Effect.gen(function* () { · 13 unmodified lines
134 return token;227 return token;
135 });228 });
136 229
137 const clear = secrets230 const clear = secrets.remove(CLOUD_CLI_OAUTH_TOKEN_SECRET).pipe(
138 .remove(CLOUD_CLI_OAUTH_TOKEN_SECRET)231 Effect.mapError(
139 .pipe(wrapError((cause) => new CloudCliCredentialRemovalError({ cause })));232 (cause) =>
233 new CloudCliCredentialRemovalError({
234 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
235 cause,
236 }),
237 ),
238 );
140 239
141 const read = Effect.fn("cloud.cli_token.read")(function* () {240 const read = Effect.fn("cloud.cli_token.read")(function* () {
142 const encoded = yield* secrets.get(CLOUD_CLI_OAUTH_TOKEN_SECRET);241 const encoded = yield* secrets.get(CLOUD_CLI_OAUTH_TOKEN_SECRET);
@@ -162,21 +261,62 @@ export const make = Effect.gen(function* () { · 19 unmodified lines
162 });261 });
163 262
164 const refresh = Effect.fn("cloud.cli_token.refresh")(function* (token: PersistedToken) {263 const refresh = Effect.fn("cloud.cli_token.refresh")(function* (token: PersistedToken) {
165 const metadata = yield* cloudCliOAuthConfig;264 const metadata = yield* cloudCliOAuthConfig.pipe(
265 Effect.mapError(
266 (cause) =>
267 new CloudCliCredentialRefreshError({
268 stage: "load-oauth-config",
269 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
270 cause,
271 }),
272 ),
273 );
166 return yield* exchangeToken(metadata, {274 return yield* exchangeToken(metadata, {
167 grant_type: "refresh_token",275 grant_type: "refresh_token",
168 refresh_token: token.refreshToken,276 refresh_token: token.refreshToken,
169 client_id: metadata.clientId,277 client_id: metadata.clientId,
170 });278 }).pipe(
279 Effect.mapError(
280 (cause) =>
281 new CloudCliCredentialRefreshError({
282 stage: "exchange-token",
283 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
284 tokenEndpoint: metadata.tokenEndpoint,
285 cause,
286 }),
287 ),
288 );
171 });289 });
172 290
173 const login = Effect.fn("cloud.cli_token.login")(function* () {291 const login = Effect.fn("cloud.cli_token.login")(function* () {
174 const metadata = yield* cloudCliOAuthConfig;292 const metadata = yield* cloudCliOAuthConfig.pipe(
175 const verifier = Encoding.encodeBase64Url(yield* crypto.randomBytes(32));293 Effect.mapError(
176 const challenge = Encoding.encodeBase64Url(294 (cause) =>
177 yield* crypto.digest("SHA-256", new TextEncoder().encode(verifier)),295 new CloudCliAuthorizationError({
296 stage: "load-oauth-config",
297 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
298 cause,
299 }),
300 ),
301 );
302 const { challenge, state, verifier } = yield* Effect.gen(function* () {
303 const verifier = Encoding.encodeBase64Url(yield* crypto.randomBytes(32));
304 const challenge = Encoding.encodeBase64Url(
305 yield* crypto.digest("SHA-256", new TextEncoder().encode(verifier)),
306 );
307 const state = yield* crypto.randomUUIDv4;
308 return { challenge, state, verifier };
309 }).pipe(
310 Effect.mapError(
311 (cause) =>
312 new CloudCliAuthorizationError({
313 stage: "prepare-pkce",
314 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
315 redirectUri: metadata.redirectUri,
316 cause,
317 }),
318 ),
178 );319 );
179 const state = yield* crypto.randomUUIDv4;
180 const callback = yield* Deferred.make<string>();320 const callback = yield* Deferred.make<string>();
181 const callbackRoute = HttpRouter.add(321 const callbackRoute = HttpRouter.add(
182 "GET",322 "GET",
@@ -207,12 +347,23 @@ export const make = Effect.gen(function* () { · 24 unmodified lines
207 }).pipe(347 }).pipe(
208 Layer.provide(348 Layer.provide(
209 NodeHttpServer.layer(NodeHttp.createServer, {349 NodeHttpServer.layer(NodeHttp.createServer, {
210 host: "127.0.0.1",350 host: CLOUD_CLI_OAUTH_CALLBACK_HOST,
211 port: 34338,351 port: CLOUD_CLI_OAUTH_CALLBACK_PORT,
212 disablePreemptiveShutdown: true,352 disablePreemptiveShutdown: true,
213 }),353 }),
214 ),354 ),
215 Layer.build,355 Layer.build,
356 Effect.mapError(
357 (cause) =>
358 new CloudCliAuthorizationError({
359 stage: "start-callback-server",
360 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
361 redirectUri: metadata.redirectUri,
362 callbackHost: CLOUD_CLI_OAUTH_CALLBACK_HOST,
363 callbackPort: CLOUD_CLI_OAUTH_CALLBACK_PORT,
364 cause,
365 }),
366 ),
216 );367 );
217 const authorizationUrl = new URL(metadata.authorizationEndpoint);368 const authorizationUrl = new URL(metadata.authorizationEndpoint);
218 authorizationUrl.searchParams.set("client_id", metadata.clientId);369 authorizationUrl.searchParams.set("client_id", metadata.clientId);
@@ -225,13 +376,16 @@ export const make = Effect.gen(function* () { · 6 unmodified lines
225 yield* Console.log(`Open this URL to authorize T3 Connect:\n${authorizationUrl.toString()}\n`);376 yield* Console.log(`Open this URL to authorize T3 Connect:\n${authorizationUrl.toString()}\n`);
226 const code = yield* Deferred.await(callback).pipe(377 const code = yield* Deferred.await(callback).pipe(
227 Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT),378 Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT),
228 Effect.catchTag("TimeoutError", (cause) =>379 Effect.catchTags({
229 Effect.fail(380 TimeoutError: (cause) =>
230 new CloudCliAuthorizationTimeoutError({381 Effect.fail(
231 cause,382 new CloudCliAuthorizationTimeoutError({
232 }),383 redirectUri: metadata.redirectUri,
233 ),384 timeoutMillis: Duration.toMillis(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT),
234 ),385 cause,
386 }),
387 ),
388 }),
235 );389 );
236 return yield* exchangeToken(metadata, {390 return yield* exchangeToken(metadata, {
237 grant_type: "authorization_code",391 grant_type: "authorization_code",
@@ -239,26 +393,45 @@ export const make = Effect.gen(function* () { · 1 unmodified line
239 redirect_uri: metadata.redirectUri,393 redirect_uri: metadata.redirectUri,
240 client_id: metadata.clientId,394 client_id: metadata.clientId,
241 code_verifier: verifier,395 code_verifier: verifier,
242 });396 }).pipe(
397 Effect.mapError(
398 (cause) =>
399 new CloudCliAuthorizationError({
400 stage: "exchange-token",
401 secretName: CLOUD_CLI_OAUTH_TOKEN_SECRET,
402 tokenEndpoint: metadata.tokenEndpoint,
403 redirectUri: metadata.redirectUri,
404 cause,
405 }),
406 ),
407 );
243 });408 });
244 409
245 const getExistingNoLock = Effect.fn("cloud.cli_token.get_existing_no_lock")(function* () {410 const getExistingNoLock = Effect.fn("cloud.cli_token.get_existing_no_lock")(function* () {
246 const token = yield* read();411 const token = yield* read().pipe(
412 Effect.mapError(CloudCliCredentialRefreshError.fromCredentialRead),
413 );
247 if (Option.isNone(token)) return token;414 if (Option.isNone(token)) return token;
248 const now = yield* Clock.currentTimeMillis;415 const now = yield* Clock.currentTimeMillis;
249 if (token.value.expiresAtEpochMs - CLOUD_CLI_OAUTH_REFRESH_EARLY_MS > now) {416 if (token.value.expiresAtEpochMs - CLOUD_CLI_OAUTH_REFRESH_EARLY_MS > now) {
250 return token;417 return token;
251 }418 }
252 return Option.some(yield* refresh(token.value).pipe(Effect.flatMap(persist)));419 return Option.some(
420 yield* refresh(token.value).pipe(
421 Effect.flatMap((refreshed) =>
422 persist(refreshed).pipe(
423 Effect.mapError(CloudCliCredentialRefreshError.fromCredentialPersist),
424 ),
425 ),
426 ),
427 );
253 });428 });
254 429
255 const getExisting = semaphore.withPermits(1)(430 const getExisting = semaphore.withPermits(1)(getExistingNoLock());
256 getExistingNoLock().pipe(wrapError((cause) => new CloudCliCredentialRefreshError({ cause }))),
257 );
258 const hasCredential = semaphore.withPermits(1)(431 const hasCredential = semaphore.withPermits(1)(
259 read().pipe(432 read().pipe(
260 Effect.map(Option.isSome),433 Effect.map(Option.isSome),
261 wrapError((cause) => new CloudCliCredentialReadError({ cause })),434 Effect.mapError(CloudCliCredentialReadError.fromCredentialRead),
262 ),435 ),
263 );436 );
264 const get = semaphore.withPermits(1)(437 const get = semaphore.withPermits(1)(
@@ -266,8 +439,14 @@ export const make = Effect.gen(function* () { · 1 unmodified line
266 const token = yield* getExistingNoLock();439 const token = yield* getExistingNoLock();
267 return Option.isSome(token)440 return Option.isSome(token)
268 ? token.value441 ? token.value
269 : yield* Effect.scoped(login()).pipe(Effect.flatMap(persist));442 : yield* Effect.scoped(login()).pipe(
270 }).pipe(wrapError((cause) => new CloudCliAuthorizationError({ cause }))),443 Effect.flatMap((authorized) =>
444 persist(authorized).pipe(
445 Effect.mapError(CloudCliAuthorizationError.fromCredentialPersist),
446 ),
447 ),
448 );
449 }),
271 );450 );
272 451
273 return CloudCliTokenManager.of({ get, getExisting, hasCredential, clear });452 return CloudCliTokenManager.of({ get, getExisting, hasCredential, clear });
apps/server/src/cloud/http.ts+0−6
@@ -770,14 +770,8 @@ const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesi · 769 unmodified lines
770 CloudRelayConfigurationError: (error) =>770 CloudRelayConfigurationError: (error) =>
771 failEnvironmentCloudInternalError(error.message)(error),771 failEnvironmentCloudInternalError(error.message)(error),
772 CloudRelayRequestError: (error) => failEnvironmentCloudInternalError(error.message)(error),772 CloudRelayRequestError: (error) => failEnvironmentCloudInternalError(error.message)(error),
773 CloudCliCredentialRemovalError: (error) =>
774 failEnvironmentCloudInternalError(error.message)(error),
775 CloudCliCredentialRefreshError: (error) =>773 CloudCliCredentialRefreshError: (error) =>
776 failEnvironmentCloudInternalError(error.message)(error),774 failEnvironmentCloudInternalError(error.message)(error),
777 CloudCliCredentialReadError: (error) => failEnvironmentCloudInternalError(error.message)(error),
778 CloudCliAuthorizationError: (error) => failEnvironmentCloudInternalError(error.message)(error),
779 CloudCliAuthorizationTimeoutError: (error) =>
780 failEnvironmentCloudInternalError(error.message)(error),
781 }),775 }),
782);776);
783 777

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t