hstratus-auth 0.1.0.1 → 0.2.0.0
raw patch · 7 files changed
+132/−208 lines, 7 filesdep −optparse-applicativePVP ok
version bump matches the API change (PVP)
Dependencies removed: optparse-applicative
API changes (from Hackage documentation)
- Network.HStratus.Http.Cli: commonOptsParser :: Parser CommonOpts
+ Network.HStratus.Http: AccountLocked :: AuthError
+ Network.HStratus.Http: ArtifactDirCreationFailed :: !FilePath -> AuthError
+ Network.HStratus.Http: Authenticated :: Session -> AccountData -> AuthState
+ Network.HStratus.Http: CredentialsMissing :: AuthError
+ Network.HStratus.Http: InvalidCredentials :: AuthError
+ Network.HStratus.Http: PrivacyAgreementRequired :: AuthError
+ Network.HStratus.Http: Requires2FA :: Session -> AuthState
+ Network.HStratus.Http: Requires2SA :: Session -> NonEmpty Setup2SADevice -> AuthState
+ Network.HStratus.Http: ServiceError :: !Text -> !Maybe Text -> AuthError
+ Network.HStratus.Http: SrpProtocolError :: AuthError
+ Network.HStratus.Http: TwoFactorLocked :: AuthError
+ Network.HStratus.Http: TwoFactorStillRequired :: AuthError
+ Network.HStratus.Http: UnexpectedResponse :: !Text -> AuthError
+ Network.HStratus.Http: WebserviceNotFound :: !Text -> AuthError
+ Network.HStratus.Http: class Exception e => HStratusError e
+ Network.HStratus.Http: complete2SA :: Api -> NonEmpty Setup2SADevice -> IO AuthState
+ Network.HStratus.Http: complete2SAWith :: (NonEmpty Setup2SADevice -> IO Setup2SADevice) -> IO AuthCode -> Api -> NonEmpty Setup2SADevice -> IO AuthState
+ Network.HStratus.Http: completeTwoFactor :: Api -> IO AuthState
+ Network.HStratus.Http: completeTwoFactorWith :: (Word8 -> IO AuthCode) -> (TrustData -> IO (Maybe TrustedPhone)) -> Api -> IO AuthState
+ Network.HStratus.Http: data Api
+ Network.HStratus.Http: data ApiLogger
+ Network.HStratus.Http: data AuthError
+ Network.HStratus.Http: data AuthState
+ Network.HStratus.Http: fetchTrustData :: Api -> IO TrustData
+ Network.HStratus.Http: fileLogger :: Handle -> ApiLogger
+ Network.HStratus.Http: login :: Api -> IO AuthState
+ Network.HStratus.Http: loginWith :: (Word8 -> IO AuthCode) -> (TrustData -> IO (Maybe TrustedPhone)) -> (NonEmpty Setup2SADevice -> IO Setup2SADevice) -> Api -> IO AuthState
+ Network.HStratus.Http: mkApi :: Realm -> IO Api
+ Network.HStratus.Http: mkApiWith :: Session -> Endpoints -> Manager -> IO Api
+ Network.HStratus.Http: rawRequest :: Api -> Request -> IO (Response ByteString)
+ Network.HStratus.Http: redactingLogger :: Handle -> ApiLogger
+ Network.HStratus.Http: requestSmsCode :: Api -> TrustedPhone -> IO ()
+ Network.HStratus.Http: verboseLogger :: Handle -> ApiLogger
+ Network.HStratus.Http: verifySmsCode :: Api -> TrustedPhone -> AuthCode -> IO Bool
+ Network.HStratus.Http: withLogger :: ApiLogger -> Api -> Api
+ Network.HStratus.Http.Common: icloudBrowserHeaders :: RequestHeaders
+ Network.HStratus.Http.Common: icloudHome :: ByteString
+ Network.HStratus.Http.Common: lookupWebservice :: Text -> Map Text Webservice -> IO Request
+ Network.HStratus.Http.Common: stripTrailingSlash :: ByteString -> ByteString
+ Network.HStratus.Http.Common: withHeaders :: RequestHeaders -> Request -> Request
+ Network.HStratus.Http.Endpoints: China :: Realm
+ Network.HStratus.Http.Endpoints: Endpoints :: !ByteString -> !Request -> !Request -> !ByteString -> Endpoints
+ Network.HStratus.Http.Endpoints: Usual :: Realm
+ Network.HStratus.Http.Endpoints: [epAuth] :: Endpoints -> !Request
+ Network.HStratus.Http.Endpoints: [epHome] :: Endpoints -> !ByteString
+ Network.HStratus.Http.Endpoints: [epSetup] :: Endpoints -> !Request
+ Network.HStratus.Http.Endpoints: [epWidgetKey] :: Endpoints -> !ByteString
+ Network.HStratus.Http.Endpoints: data Endpoints
+ Network.HStratus.Http.Endpoints: data Realm
+ Network.HStratus.Http.Endpoints: realmEndpoints :: Realm -> Endpoints
+ Network.HStratus.Session: AccountData :: !Int -> !Bool -> !Maybe Bool -> !Map Text Webservice -> !Value -> AccountData
+ Network.HStratus.Session: Credentials :: !Text -> !Text -> Credentials
+ Network.HStratus.Session: Session :: !Credentials -> !FilePath -> !Text -> Session
+ Network.HStratus.Session: Webservice :: !Text -> !Maybe Text -> Webservice
+ Network.HStratus.Session: [adHsaChallengeRequired] :: AccountData -> !Bool
+ Network.HStratus.Session: [adHsaTrustedBrowser] :: AccountData -> !Maybe Bool
+ Network.HStratus.Session: [adHsaVersion] :: AccountData -> !Int
+ Network.HStratus.Session: [adRaw] :: AccountData -> !Value
+ Network.HStratus.Session: [adWebservices] :: AccountData -> !Map Text Webservice
+ Network.HStratus.Session: [credAccountName] :: Credentials -> !Text
+ Network.HStratus.Session: [credPassword] :: Credentials -> !Text
+ Network.HStratus.Session: [sessionClientId] :: Session -> !Text
+ Network.HStratus.Session: [sessionCreds] :: Session -> !Credentials
+ Network.HStratus.Session: [sessionTopDir] :: Session -> !FilePath
+ Network.HStratus.Session: [wsStatus] :: Webservice -> !Maybe Text
+ Network.HStratus.Session: [wsUrl] :: Webservice -> !Text
+ Network.HStratus.Session: data AccountData
+ Network.HStratus.Session: data Credentials
+ Network.HStratus.Session: data Session
+ Network.HStratus.Session: data Webservice
+ Network.HStratus.Session: loadSession :: IO Session
+ Network.HStratus.Session: saveCredentials :: Credentials -> IO ()
+ Network.HStratus.Trust: Setup2SADevice :: Object -> Setup2SADevice
+ Network.HStratus.Trust: TrustData :: !TrustedList -> !CodeStatus -> !Bool -> TrustData
+ Network.HStratus.Trust: TrustedPhone :: !Word8 -> !Text -> !Maybe Text -> TrustedPhone
+ Network.HStratus.Trust: [setup2SAFields] :: Setup2SADevice -> Object
+ Network.HStratus.Trust: [tdList] :: TrustData -> !TrustedList
+ Network.HStratus.Trust: [tdNoTrustedDevices] :: TrustData -> !Bool
+ Network.HStratus.Trust: [tdSecurityCode] :: TrustData -> !CodeStatus
+ Network.HStratus.Trust: [tpnId] :: TrustedPhone -> !Word8
+ Network.HStratus.Trust: [tpnNumberWithDialCode] :: TrustedPhone -> !Text
+ Network.HStratus.Trust: [tpnPushMode] :: TrustedPhone -> !Maybe Text
+ Network.HStratus.Trust: data TrustData
+ Network.HStratus.Trust: data TrustedPhone
+ Network.HStratus.Trust: newtype Setup2SADevice
+ Network.HStratus.Trust: pleaseReadCode :: Word8 -> IO Text
+ Network.HStratus.Trust: selectSetupDevice :: NonEmpty Setup2SADevice -> IO Setup2SADevice
+ Network.HStratus.Trust: selectTwoFaPhone :: TrustData -> IO (Maybe TrustedPhone)
+ Network.HStratus.Trust: setup2SADeviceLabel :: Setup2SADevice -> Text
Files
- ChangeLog.md +4/−0
- README.md +1/−2
- hstratus-auth.cabal +2/−19
- src-cli/Network/HStratus/Http/Cli.hs +0/−131
- src/Network/HStratus/Http/Cli.hs +125/−0
- test/HStratus/Http/CliSpec.hs +0/−54
- test/Spec.hs +0/−2
ChangeLog.md view
@@ -2,6 +2,10 @@ `icloud-auth` uses [PVP Versioning][1]. +## 0.2.0.0 -- 2026-07-30++* Remove `hstratus-auth-cli` sublibrary; move `Network.HStratus.Http.Cli` into the main library.+ ## 0.1.0.1 -- 2026-07-28 * Update package metadata.
README.md view
@@ -3,7 +3,7 @@ `hstratus-auth` authenticates with iCloud using Apple ID credentials stored on disk. The full sign-in flow — SRP credential exchange followed by any required two-factor (2FA) or legacy two-step (2SA) challenge — runs automatically,-prompting the terminal for verification codes when needed. On success it caches+prompting the user for verification codes when needed. On success, it caches a session token for use with other iCloud services. @@ -83,7 +83,6 @@ A command-line interface using this behaviour is provided by the [`hstratus`](https://github.com/adetokunbo/hstratus/tree/main/hstratus/#readme) package. Use [`hstratus auth init`](https://github.com/adetokunbo/hstratus/tree/main/hstratus/#hstratus-auth-init) and [`hstratus auth login`](https://github.com/adetokunbo/hstratus/tree/main/hstratus/#hstratus-auth-login) to save credentials and authenticate.- ---
hstratus-auth.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: hstratus-auth-version: 0.1.0.1+version: 0.2.0.0 synopsis: Authenticate with iCloud description: Authenticate with iCloud using Apple ID credentials stored on disk.@@ -36,23 +36,9 @@ location: https://github.com/adetokunbo/hstratus.git subdir: hstratus-auth -library hstratus-auth-cli- visibility: public- exposed-modules: Network.HStratus.Http.Cli- hs-source-dirs: src-cli- build-depends:- , base >=4.12 && <5- , directory >=1.3 && <1.4- , filepath >=1.4 && <1.6- , http-client-tls >=0.3 && <0.4- , hstratus-auth- , optparse-applicative >=0.18 && <0.19- , xdg-basedir >=0.2 && <0.3- default-language: Haskell2010- ghc-options: -Wall -Wincomplete-uni-patterns -Wpartial-fields -fwarn-tabs- library exposed-modules: Network.HStratus.Http+ Network.HStratus.Http.Cli Network.HStratus.Http.Endpoints Network.HStratus.Http.Common Network.HStratus.Session@@ -131,7 +117,6 @@ autogen-modules: Paths_hstratus_auth other-modules: HStratus.ApiLoggerSpec HStratus.Examples- HStratus.Http.CliSpec HStratus.HttpMockSpec HStratus.HttpSpec HStratus.Http.EndpointsSpec@@ -164,8 +149,6 @@ , QuickCheck >= 2.13 && < 2.16 , temporary >= 1.2 && < 1.4 , hstratus-auth- , hstratus-auth:hstratus-auth-cli- , optparse-applicative >=0.18 && <0.19 , silently >= 1.2 && < 1.3 , string-conv , text
− src-cli/Network/HStratus/Http/Cli.hs
@@ -1,131 +0,0 @@-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeApplications #-}--module Network.HStratus.Http.Cli- ( -- * Common CLI options- CommonOpts (..)- , commonOptsParser-- -- * Log target resolution- , resolveLogTarget- , defaultLogFile-- -- * Logger selection- , mkLoggerFor-- -- * Authenticated API runner- , runWithApi-- -- * Error handler- , onServiceError- )-where--import Control.Exception (catch, displayException)-import Network.HStratus.Http- ( Api- , ApiLogger- , AuthError- , AuthState (..)- , HStratusError- , fileLogger- , login- , mkApiWith- , redactingLogger- , verboseLogger- , withLogger- )-import Network.HStratus.Http.Endpoints (Realm (..), realmEndpoints)-import Network.HStratus.Session (AccountData, Session, loadSession)-import Network.HTTP.Client.TLS (newTlsManager)-import Options.Applicative-import System.Directory (createDirectoryIfMissing)-import System.Environment.XDG.BaseDir (getUserCacheDir)-import System.Exit (exitFailure)-import System.FilePath ((</>))-import System.IO (Handle, IOMode (..), stdout, withFile)----- | Options shared by all icloud CLI commands.-data CommonOpts = CommonOpts- { optChina :: Bool- -- ^ Use mainland China endpoints instead of the worldwide endpoints.- , optLog :: Bool- -- ^ Append HTTP exchanges to the default log file.- , optLogFile :: Maybe FilePath- -- ^ Append HTTP exchanges to this file instead of the default.- , optLogBodies :: Bool- -- ^ Include request bodies in the HTTP exchange log.- , optRedact :: Bool- -- ^ Redact sensitive headers (tokens, cookies) in the log.- }- deriving (Eq, Show)----- | Parser for 'CommonOpts'.-commonOptsParser :: Parser CommonOpts-commonOptsParser =- CommonOpts- <$> switch (long "china" <> help "Use mainland China endpoints")- <*> switch (long "log" <> help "Append HTTP exchanges to the default log file")- <*> optional- (strOption (long "log-file" <> metavar "FILE" <> help "Append HTTP exchanges to FILE"))- <*> switch (long "log-bodies" <> help "Include request bodies in the HTTP exchange log")- <*> switch (long "redact" <> help "Redact sensitive headers (tokens, cookies) in the log")----- | Resolve the log file path from 'CommonOpts', or 'Nothing' if logging is disabled.-resolveLogTarget :: CommonOpts -> IO (Maybe FilePath)-resolveLogTarget CommonOpts{optLogFile = Just fp} = pure (Just fp)-resolveLogTarget CommonOpts{optLog = True} = Just <$> defaultLogFile-resolveLogTarget _ = pure Nothing----- | Default log file path: @~\/.cache\/hs-icloud\/requests.log@.-defaultLogFile :: IO FilePath-defaultLogFile = do- dir <- getUserCacheDir "hs-icloud"- createDirectoryIfMissing True dir- pure (dir </> "requests.log")----- | Select the appropriate logger constructor from 'CommonOpts'.-mkLoggerFor :: CommonOpts -> Handle -> ApiLogger-mkLoggerFor CommonOpts{optRedact = True} = redactingLogger-mkLoggerFor CommonOpts{optLogBodies = True} = verboseLogger-mkLoggerFor _ = fileLogger---{- | Authenticate and run an action with the resulting 'Api'.--Handles session loading, TLS manager creation, logger wiring, and catches-'AuthError'. Additional error types should be caught by the caller.--}-runWithApi- :: CommonOpts- -> (AccountData -> Session -> Api -> IO ())- -> IO ()-runWithApi opts runAction = do- session <- loadSession- mgr <- newTlsManager- let realm = if optChina opts then China else Usual- api0 <- mkApiWith session (realmEndpoints realm) mgr- mbLogPath <- resolveLogTarget opts- let mkLogger' = mkLoggerFor opts- run api = do- result <- login api- case result of- Authenticated sess ad -> runAction ad sess api- _ -> putStrLn "Not authenticated — run 'hstratus-auth login' first." >> exitFailure- go = case mbLogPath of- Just fp -> withFile fp AppendMode $ \h -> run (withLogger (mkLogger' h) api0)- Nothing- | optLogBodies opts && not (optRedact opts) -> run (withLogger (mkLogger' stdout) api0)- | otherwise -> run api0- go `catch` onServiceError @AuthError----- | Print a service error and exit. Use as the catch handler in CLI wrappers.-onServiceError :: (HStratusError e) => e -> IO a-onServiceError e = putStrLn ("Error: " <> displayException e) >> exitFailure
+ src/Network/HStratus/Http/Cli.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE TypeApplications #-}++{- |+Module : Network.HStratus.Http.Cli+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD-3-Clause++CLI helpers shared by iCloud service commands: common options, log target+resolution, logger construction, and the authenticated API runner.+-}+module Network.HStratus.Http.Cli+ ( -- * Common CLI options+ CommonOpts (..)++ -- * Log target resolution+ , resolveLogTarget+ , defaultLogFile++ -- * Logger selection+ , mkLoggerFor++ -- * Authenticated API runner+ , runWithApi++ -- * Error handler+ , onServiceError+ )+where++import Control.Exception (catch, displayException)+import Network.HStratus.Http+ ( Api+ , ApiLogger+ , AuthError+ , AuthState (..)+ , HStratusError+ , fileLogger+ , login+ , mkApiWith+ , redactingLogger+ , verboseLogger+ , withLogger+ )+import Network.HStratus.Http.Endpoints (Realm (..), realmEndpoints)+import Network.HStratus.Session (AccountData, Session, loadSession)+import Network.HTTP.Client.TLS (newTlsManager)+import System.Directory (createDirectoryIfMissing)+import System.Environment.XDG.BaseDir (getUserCacheDir)+import System.Exit (exitFailure)+import System.FilePath ((</>))+import System.IO (Handle, IOMode (..), stdout, withFile)+++-- | Options shared by all icloud CLI commands.+data CommonOpts = CommonOpts+ { optChina :: Bool+ -- ^ Use mainland China endpoints instead of the worldwide endpoints.+ , optLog :: Bool+ -- ^ Append HTTP exchanges to the default log file.+ , optLogFile :: Maybe FilePath+ -- ^ Append HTTP exchanges to this file instead of the default.+ , optLogBodies :: Bool+ -- ^ Include request bodies in the HTTP exchange log.+ , optRedact :: Bool+ -- ^ Redact sensitive headers (tokens, cookies) in the log.+ }+ deriving (Eq, Show)+++-- | Resolve the log file path from 'CommonOpts', or 'Nothing' if logging is disabled.+resolveLogTarget :: CommonOpts -> IO (Maybe FilePath)+resolveLogTarget CommonOpts{optLogFile = Just fp} = pure (Just fp)+resolveLogTarget CommonOpts{optLog = True} = Just <$> defaultLogFile+resolveLogTarget _ = pure Nothing+++-- | Default log file path: @~\/.cache\/hs-icloud\/requests.log@.+defaultLogFile :: IO FilePath+defaultLogFile = do+ dir <- getUserCacheDir "hs-icloud"+ createDirectoryIfMissing True dir+ pure (dir </> "requests.log")+++-- | Select the appropriate logger constructor from 'CommonOpts'.+mkLoggerFor :: CommonOpts -> Handle -> ApiLogger+mkLoggerFor CommonOpts{optRedact = True} = redactingLogger+mkLoggerFor CommonOpts{optLogBodies = True} = verboseLogger+mkLoggerFor _ = fileLogger+++{- | Authenticate and run an action with the resulting 'Api'.++Handles session loading, TLS manager creation, logger wiring, and catches+'AuthError'. Additional error types should be caught by the caller.+-}+runWithApi+ :: CommonOpts+ -> (AccountData -> Session -> Api -> IO ())+ -> IO ()+runWithApi opts runAction = do+ session <- loadSession+ mgr <- newTlsManager+ let realm = if optChina opts then China else Usual+ api0 <- mkApiWith session (realmEndpoints realm) mgr+ mbLogPath <- resolveLogTarget opts+ let mkLogger' = mkLoggerFor opts+ run api = do+ result <- login api+ case result of+ Authenticated sess ad -> runAction ad sess api+ _ -> putStrLn "Not authenticated — run 'hstratus-auth login' first." >> exitFailure+ go = case mbLogPath of+ Just fp -> withFile fp AppendMode $ \h -> run (withLogger (mkLogger' h) api0)+ Nothing+ | optLogBodies opts && not (optRedact opts) -> run (withLogger (mkLogger' stdout) api0)+ | otherwise -> run api0+ go `catch` onServiceError @AuthError+++-- | Print a service error and exit. Use as the catch handler in CLI wrappers.+onServiceError :: (HStratusError e) => e -> IO a+onServiceError e = putStrLn ("Error: " <> displayException e) >> exitFailure
− test/HStratus/Http/CliSpec.hs
@@ -1,54 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--{- |-Module : HStratus.Http.CliSpec-Copyright : (c) 2026 Tim Emiola-Maintainer : Tim Emiola <adetokunbo@emio.la>-SPDX-License-Identifier: BSD-3-Clause--Tests for the CLI options parser in 'Network.HStratus.Http.Cli'.--}-module HStratus.Http.CliSpec (spec) where--import Network.HStratus.Http.Cli (CommonOpts (..), commonOptsParser)-import Options.Applicative (ParserResult (..), defaultPrefs, execParserPure, fullDesc, info, renderFailure)-import Test.Hspec-import Test.Hspec.Benri (endsRight)---defaultOpts :: CommonOpts-defaultOpts = CommonOpts False False Nothing False False---spec :: Spec-spec = describe "Network.HStratus.Http.Cli.commonOptsParser" $ do- it "defaults all flags to False with no log file" $- pure (parseOpts []) `endsRight` defaultOpts-- it "sets optChina when --china is given" $- fmap optChina (parseOpts ["--china"]) `shouldBe` Right True-- it "sets optLog when --log is given" $- fmap optLog (parseOpts ["--log"]) `shouldBe` Right True-- it "sets optLogFile when --log-file is given" $- fmap optLogFile (parseOpts ["--log-file", "/tmp/test.log"])- `shouldBe` Right (Just "/tmp/test.log")-- it "sets optLogBodies when --log-bodies is given" $- fmap optLogBodies (parseOpts ["--log-bodies"]) `shouldBe` Right True-- it "sets optRedact when --redact is given" $- fmap optRedact (parseOpts ["--redact"]) `shouldBe` Right True-- it "accepts multiple flags together" $- pure (parseOpts ["--log", "--redact", "--china"])- `endsRight` defaultOpts{optChina = True, optLog = True, optRedact = True}---parseOpts :: [String] -> Either String CommonOpts-parseOpts args =- case execParserPure defaultPrefs (info commonOptsParser fullDesc) args of- Success opts -> Right opts- Failure failure -> Left (fst (renderFailure failure "test"))- CompletionInvoked _ -> Left "completion invoked"
test/Spec.hs view
@@ -11,7 +11,6 @@ module Main where import qualified HStratus.ApiLoggerSpec as ApiLogger-import qualified HStratus.Http.CliSpec as HttpCli import qualified HStratus.Http.EndpointsSpec as HttpEndpoints import qualified HStratus.Http.ErrorsSpec as HttpErrors import qualified HStratus.Http.HeadersSpec as HttpHeaders@@ -38,7 +37,6 @@ Session.spec Http.spec ApiLogger.spec- HttpCli.spec HttpEndpoints.spec HttpErrors.spec HttpHeaders.spec