| @@ -27,6 +27,8 @@ import { cloudCliOAuthConfig, type CloudCliOAuthConfig } from "./publicConfig.ts · 26 unmodified lines |
| 27 | const CLOUD_CLI_OAUTH_TOKEN_SECRET = "cloud-cli-oauth-token"; | 27 | const CLOUD_CLI_OAUTH_TOKEN_SECRET = "cloud-cli-oauth-token"; |
| 28 | const CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT = Duration.minutes(10); | 28 | const CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT = Duration.minutes(10); |
| 29 | const CLOUD_CLI_OAUTH_REFRESH_EARLY_MS = Duration.toMillis(Duration.minutes(5)); | 29 | const CLOUD_CLI_OAUTH_REFRESH_EARLY_MS = Duration.toMillis(Duration.minutes(5)); |
| | 30 | const CLOUD_CLI_OAUTH_CALLBACK_HOST = "127.0.0.1"; |
| | 31 | const CLOUD_CLI_OAUTH_CALLBACK_PORT = 34338; |
| 30 | | 32 | |
| 31 | const PersistedToken = Schema.Struct({ | 33 | const 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 | |
| | 51 | type CredentialReadFailure = ServerSecretStore.SecretStoreReadError | Schema.SchemaError; |
| | 52 | |
| | 53 | type CredentialPersistFailure = |
| | 54 | | Schema.SchemaError |
| | 55 | | ServerSecretStore.SecretStoreTemporaryPathGenerationError |
| | 56 | | ServerSecretStore.SecretStorePersistError; |
| | 57 | |
| | 58 | const CloudCliCredentialRefreshStage = Schema.Literals([ |
| | 59 | "read-credential", |
| | 60 | "decode-credential", |
| | 61 | "load-oauth-config", |
| | 62 | "exchange-token", |
| | 63 | "encode-credential", |
| | 64 | "persist-credential", |
| | 65 | ]); |
| | 66 | |
| | 67 | const 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 | |
| 49 | export class CloudCliCredentialRemovalError extends Schema.TaggedErrorClass<CloudCliCredentialRemovalError>()( | 76 | export 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 | |
| 58 | export class CloudCliCredentialRefreshError extends Schema.TaggedErrorClass<CloudCliCredentialRefreshError>()( | 88 | export 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 | |
| 67 | export class CloudCliCredentialReadError extends Schema.TaggedErrorClass<CloudCliCredentialReadError>()( | 119 | export 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 | |
| 76 | export class CloudCliAuthorizationError extends Schema.TaggedErrorClass<CloudCliAuthorizationError>()( | 140 | export 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 | |
| 85 | export class CloudCliAuthorizationTimeoutError extends Schema.TaggedErrorClass<CloudCliAuthorizationTimeoutError>()( | 171 | export 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 |
| 103 | export class CloudCliTokenManager extends Context.Service< | 193 | export 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 | |
| 113 | const 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 | | | |
| 118 | function stringToBytes(value: string): Uint8Array { | 211 | function 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 = secrets | 230 | 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.value | 441 | ? 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 }); |