tesla-fleet-api 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +284 -1
- package/dist/_virtual/_rolldown/runtime.cjs +10 -0
- package/dist/bluetooth/adapters/shared.cjs +38 -0
- package/dist/bluetooth/adapters/shared.mjs +36 -0
- package/dist/bluetooth/async.cjs +121 -0
- package/dist/bluetooth/async.d.cts +10 -0
- package/dist/bluetooth/async.d.mts +10 -0
- package/dist/bluetooth/async.mjs +116 -0
- package/dist/bluetooth/broadcast.cjs +153 -0
- package/dist/bluetooth/broadcast.d.cts +59 -0
- package/dist/bluetooth/broadcast.d.mts +59 -0
- package/dist/bluetooth/broadcast.mjs +151 -0
- package/dist/bluetooth/constants.cjs +99 -0
- package/dist/bluetooth/constants.d.cts +70 -0
- package/dist/bluetooth/constants.d.mts +70 -0
- package/dist/bluetooth/constants.mjs +72 -0
- package/dist/bluetooth/esphome.cjs +292 -0
- package/dist/bluetooth/esphome.d.cts +83 -0
- package/dist/bluetooth/esphome.d.mts +83 -0
- package/dist/bluetooth/esphome.mjs +291 -0
- package/dist/bluetooth/framing.cjs +125 -0
- package/dist/bluetooth/framing.d.cts +45 -0
- package/dist/bluetooth/framing.d.mts +45 -0
- package/dist/bluetooth/framing.mjs +122 -0
- package/dist/bluetooth/homey.cjs +155 -0
- package/dist/bluetooth/homey.d.cts +75 -0
- package/dist/bluetooth/homey.d.mts +75 -0
- package/dist/bluetooth/homey.mjs +150 -0
- package/dist/bluetooth/names.cjs +24 -0
- package/dist/bluetooth/names.d.cts +10 -0
- package/dist/bluetooth/names.d.mts +10 -0
- package/dist/bluetooth/names.mjs +20 -0
- package/dist/bluetooth/noble.cjs +178 -0
- package/dist/bluetooth/noble.d.cts +58 -0
- package/dist/bluetooth/noble.d.mts +58 -0
- package/dist/bluetooth/noble.mjs +177 -0
- package/dist/bluetooth/streamglue.cjs +157 -0
- package/dist/bluetooth/streamglue.d.cts +37 -0
- package/dist/bluetooth/streamglue.d.mts +37 -0
- package/dist/bluetooth/streamglue.mjs +154 -0
- package/dist/bluetooth/teslabluetooth.cjs +67 -0
- package/dist/bluetooth/teslabluetooth.d.cts +41 -0
- package/dist/bluetooth/teslabluetooth.d.mts +41 -0
- package/dist/bluetooth/teslabluetooth.mjs +66 -0
- package/dist/bluetooth/transport.cjs +10 -0
- package/dist/bluetooth/transport.d.cts +95 -0
- package/dist/bluetooth/transport.d.mts +95 -0
- package/dist/bluetooth/transport.mjs +10 -0
- package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
- package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
- package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
- package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
- package/dist/bluetooth/verify.cjs +93 -0
- package/dist/bluetooth/verify.d.cts +35 -0
- package/dist/bluetooth/verify.d.mts +35 -0
- package/dist/bluetooth/verify.mjs +91 -0
- package/dist/bluetooth.cjs +53 -0
- package/dist/bluetooth.d.cts +10 -0
- package/dist/bluetooth.d.mts +10 -0
- package/dist/bluetooth.mjs +9 -0
- package/dist/commands.cjs +1080 -85
- package/dist/commands.d.cts +585 -54
- package/dist/commands.d.mts +583 -51
- package/dist/commands.mjs +1084 -93
- package/dist/const.cjs +22 -0
- package/dist/const.d.cts +21 -0
- package/dist/const.d.mts +21 -0
- package/dist/const.mjs +20 -1
- package/dist/energy.cjs +24 -19
- package/dist/energy.d.cts +21 -15
- package/dist/energy.d.mts +21 -15
- package/dist/energy.mjs +24 -19
- package/dist/energyisland.cjs +38 -0
- package/dist/energyisland.mjs +37 -0
- package/dist/energyparsers.cjs +118 -0
- package/dist/energyparsers.d.cts +39 -0
- package/dist/energyparsers.d.mts +39 -0
- package/dist/energyparsers.mjs +116 -0
- package/dist/energyspecific.cjs +375 -17
- package/dist/energyspecific.d.cts +199 -19
- package/dist/energyspecific.d.mts +197 -16
- package/dist/energyspecific.mjs +370 -18
- package/dist/exceptions.cjs +3 -2
- package/dist/exceptions.d.cts +3 -2
- package/dist/exceptions.d.mts +3 -2
- package/dist/exceptions.mjs +3 -2
- package/dist/funnel.cjs +297 -0
- package/dist/funnel.d.cts +118 -0
- package/dist/funnel.d.mts +118 -0
- package/dist/funnel.mjs +292 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +22 -8
- package/dist/index.d.mts +22 -8
- package/dist/index.mjs +19 -6
- package/dist/log.cjs +75 -0
- package/dist/log.d.cts +4 -0
- package/dist/log.d.mts +4 -0
- package/dist/log.mjs +71 -0
- package/dist/oauth.cjs +143 -0
- package/dist/oauth.d.cts +69 -0
- package/dist/oauth.d.mts +69 -0
- package/dist/oauth.mjs +143 -0
- package/dist/powerwall/client.cjs +818 -0
- package/dist/powerwall/client.d.cts +297 -0
- package/dist/powerwall/client.d.mts +297 -0
- package/dist/powerwall/client.mjs +807 -0
- package/dist/powerwall/components.cjs +188 -0
- package/dist/powerwall/components.d.cts +45 -0
- package/dist/powerwall/components.d.mts +45 -0
- package/dist/powerwall/components.mjs +182 -0
- package/dist/powerwall/energysite.cjs +216 -0
- package/dist/powerwall/energysite.d.cts +104 -0
- package/dist/powerwall/energysite.d.mts +104 -0
- package/dist/powerwall/energysite.mjs +214 -0
- package/dist/powerwall/exceptions.cjs +59 -0
- package/dist/powerwall/exceptions.d.cts +38 -0
- package/dist/powerwall/exceptions.d.mts +38 -0
- package/dist/powerwall/exceptions.mjs +51 -0
- package/dist/powerwall/gateway.cjs +102 -0
- package/dist/powerwall/gateway.d.cts +57 -0
- package/dist/powerwall/gateway.d.mts +57 -0
- package/dist/powerwall/gateway.mjs +101 -0
- package/dist/powerwall/merge.cjs +163 -0
- package/dist/powerwall/merge.d.cts +63 -0
- package/dist/powerwall/merge.d.mts +63 -0
- package/dist/powerwall/merge.mjs +159 -0
- package/dist/powerwall/models.d.cts +95 -0
- package/dist/powerwall/models.d.mts +95 -0
- package/dist/powerwall/queries.cjs +17 -0
- package/dist/powerwall/queries.mjs +10 -0
- package/dist/powerwall/transport.cjs +232 -0
- package/dist/powerwall/transport.mjs +227 -0
- package/dist/powerwall.cjs +47 -0
- package/dist/powerwall.d.cts +9 -0
- package/dist/powerwall.d.mts +9 -0
- package/dist/powerwall.mjs +8 -0
- package/dist/router/base.cjs +203 -0
- package/dist/router/base.d.cts +112 -0
- package/dist/router/base.d.mts +112 -0
- package/dist/router/base.mjs +200 -0
- package/dist/router/energysite.cjs +65 -0
- package/dist/router/energysite.d.cts +47 -0
- package/dist/router/energysite.d.mts +47 -0
- package/dist/router/energysite.mjs +60 -0
- package/dist/router.cjs +13 -0
- package/dist/router.d.cts +3 -1
- package/dist/router.d.mts +3 -1
- package/dist/router.mjs +3 -1
- package/dist/signing/crypto.cjs +28 -0
- package/dist/signing/crypto.mjs +26 -1
- package/dist/signing/energy.cjs +123 -0
- package/dist/signing/energy.d.cts +45 -0
- package/dist/signing/energy.d.mts +45 -0
- package/dist/signing/energy.mjs +120 -0
- package/dist/signing/jws.cjs +229 -0
- package/dist/signing/jws.d.cts +45 -0
- package/dist/signing/jws.d.mts +45 -0
- package/dist/signing/jws.mjs +211 -0
- package/dist/signing/keys.cjs +191 -0
- package/dist/signing/keys.d.cts +21 -0
- package/dist/signing/keys.d.mts +21 -0
- package/dist/signing/keys.mjs +182 -0
- package/dist/signing/session.cjs +104 -30
- package/dist/signing/session.d.cts +60 -23
- package/dist/signing/session.d.mts +60 -23
- package/dist/signing/session.mjs +105 -31
- package/dist/tariff.cjs +27 -0
- package/dist/tariff.d.cts +11 -0
- package/dist/tariff.d.mts +11 -0
- package/dist/tariff.mjs +27 -1
- package/dist/teslafleetapi.cjs +256 -55
- package/dist/teslafleetapi.d.cts +110 -14
- package/dist/teslafleetapi.d.mts +110 -14
- package/dist/teslafleetapi.mjs +258 -57
- package/dist/teslemetry.cjs +152 -5
- package/dist/teslemetry.d.cts +95 -5
- package/dist/teslemetry.d.mts +93 -3
- package/dist/teslemetry.mjs +151 -6
- package/dist/teslemetryenergy.cjs +287 -0
- package/dist/teslemetryenergy.d.cts +154 -0
- package/dist/teslemetryenergy.d.mts +154 -0
- package/dist/teslemetryenergy.mjs +282 -0
- package/dist/teslemetryvehicle.cjs +407 -0
- package/dist/teslemetryvehicle.d.cts +238 -0
- package/dist/teslemetryvehicle.d.mts +238 -0
- package/dist/teslemetryvehicle.mjs +405 -0
- package/dist/tessie.cjs +42 -5
- package/dist/tessie.d.cts +51 -5
- package/dist/tessie.d.mts +49 -3
- package/dist/tessie.mjs +42 -5
- package/dist/tessievehicle.cjs +514 -0
- package/dist/tessievehicle.d.cts +333 -0
- package/dist/tessievehicle.d.mts +333 -0
- package/dist/tessievehicle.mjs +513 -0
- package/dist/types/live_status.d.cts +16 -16
- package/dist/types/live_status.d.mts +16 -16
- package/dist/types/responses.d.cts +5 -1
- package/dist/types/responses.d.mts +5 -1
- package/dist/types/vehicle.d.cts +7 -1
- package/dist/types/vehicle.d.mts +7 -1
- package/dist/types.d.cts +3 -3
- package/dist/types.d.mts +3 -3
- package/dist/util.cjs +41 -0
- package/dist/util.d.cts +18 -0
- package/dist/util.d.mts +18 -0
- package/dist/util.mjs +40 -0
- package/dist/vehicle.cjs +79 -36
- package/dist/vehicle.d.cts +42 -26
- package/dist/vehicle.d.mts +42 -26
- package/dist/vehicle.mjs +80 -37
- package/dist/vehiclesigned.cjs +20 -5
- package/dist/vehiclesigned.d.cts +14 -5
- package/dist/vehiclesigned.d.mts +14 -5
- package/dist/vehiclesigned.mjs +20 -5
- package/dist/vehiclespecific.cjs +126 -15
- package/dist/vehiclespecific.d.cts +82 -18
- package/dist/vehiclespecific.d.mts +82 -18
- package/dist/vehiclespecific.mjs +126 -15
- package/package.json +70 -3
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Charging from "./charging.mjs";
|
|
2
|
+
import Commands, { DAY_OF_WEEK_BITS, days_of_week_mask, infotainment_command_name, vcsec_command_name } from "./commands.mjs";
|
|
2
3
|
import Energy from "./energy.mjs";
|
|
3
|
-
import EnergySpecific from "./energyspecific.mjs";
|
|
4
|
+
import EnergySpecific, { MAX_DISPATCH_DURATION_SECONDS, UNSIGNED_DEVICE_COMMANDS, validate_dispatch_battery_power } from "./energyspecific.mjs";
|
|
5
|
+
import Partner from "./partner.mjs";
|
|
4
6
|
import TeslaFleetApi from "./teslafleetapi.mjs";
|
|
5
|
-
import
|
|
7
|
+
import TeslaFleetOAuth from "./oauth.mjs";
|
|
8
|
+
import Teslemetry, { REGISTER_URL, register_client } from "./teslemetry.mjs";
|
|
6
9
|
import Tessie from "./tessie.mjs";
|
|
10
|
+
import User from "./user.mjs";
|
|
7
11
|
import Vehicle from "./vehicle.mjs";
|
|
8
12
|
import VehicleSigned from "./vehiclesigned.mjs";
|
|
9
13
|
import VehicleSpecific from "./vehiclespecific.mjs";
|
|
14
|
+
import { TeslemetryVehicle, TeslemetryVehicleTelemetry, TeslemetryVehicles } from "./teslemetryvehicle.mjs";
|
|
15
|
+
import { TessieVehicle, TessieVehicles } from "./tessievehicle.mjs";
|
|
16
|
+
import { ENERGY_SIGNATURE_TTL_SECONDS, EnergyDeviceSigner } from "./signing/energy.mjs";
|
|
17
|
+
import { DEFAULT_PAIRING_POLL_INTERVAL, DEFAULT_PAIRING_TIMEOUT, TeslemetryEnergySite, TeslemetryEnergySites } from "./teslemetryenergy.mjs";
|
|
18
|
+
import { parse_authorized_clients, parse_gateway_address } from "./energyparsers.mjs";
|
|
19
|
+
import { CLOSURE_STATES, FieldPath, LOCK_STATES, ObservationFunnel, TeslemetryStreamPublisher, VehicleDataResultPublisher } from "./funnel.mjs";
|
|
10
20
|
import { Session } from "./signing/session.mjs";
|
|
21
|
+
import { jws_exports } from "./signing/jws.mjs";
|
|
22
|
+
import { ec_private_key_from_pem, generate_ec_private_key_pem, generate_rsa_private_key_pem, rsa_public_der_pkcs1 } from "./signing/keys.mjs";
|
|
11
23
|
import { AESDecryptAuthFault, AuthorizedClientPairingTimedOut, AuthorizedClientWaitExpired, BadGateway, BluetoothCloudUnavailableError, BluetoothCommandFailed, BluetoothTimeout, BluetoothTransportError, BluetoothUnconfirmedCommand, ClientClosedRequest, CouldNotHashMetadataFault, CouldNotRetrieveKeyFault, CouldNotRetrieveTokenFault, DeviceUnexpectedResponse, ECDSAInputFault, ECDSASignatureFault, EnergyGatewayUnreachable, Forbidden, GatewayTimeout, INFORMATION_FAULTS, IVIncorrectLengthFault, IVSmallerThanExpectedFault, IncorrectEpochFault, InsufficientCredits, InternalServerError, InvalidAuthCode, InvalidCommand, InvalidField, InvalidMethod, InvalidRedirectUrl, InvalidRegion, InvalidRequest, InvalidResource, InvalidResponse, InvalidScope, InvalidTeslemetryToken, InvalidTessieToken, InvalidToken, InvalidTokenFault, KEY_REJECTED_FAULTS, LibraryError, LocalEntityResultFault, LocalEntityStartFault, Locked, LoginRequired, MESSAGE_FAULTS, MissingTeslemetryToken, MissingTessieToken, MissingToken, MobileAccessDisabled, NotAcceptable, NotAllowed, NotFound, NotOnWhitelistFault, NotProvisionedWithIdentityFault, OAuthExpired, OAuthInvalid, PaymentRequired, PreconditionFailed, PrivateKeyError, RateLimited, ResourceUnavailableForLegalReasons, ResponseError, SIGNED_MESSAGE_INFORMATION_FAULTS, ServiceUnavailable, SessionInfoAuthenticationFault, SignatureTooShortFault, SignedCommandRequired, SignedCommandResponseReplayed, SignedMessageInformationFault, SignedMessageInformationFaultAESDecryptAuth, SignedMessageInformationFaultCouldNotHashMetadata, SignedMessageInformationFaultCouldNotRetrieveKey, SignedMessageInformationFaultCouldNotRetrieveToken, SignedMessageInformationFaultECDSAInput, SignedMessageInformationFaultECDSASignature, SignedMessageInformationFaultIVIncorrectLength, SignedMessageInformationFaultIVSmallerThanExpected, SignedMessageInformationFaultIncorrectEpoch, SignedMessageInformationFaultInvalidToken, SignedMessageInformationFaultLocalEntityResult, SignedMessageInformationFaultLocalEntityStart, SignedMessageInformationFaultNotOnWhitelist, SignedMessageInformationFaultNotProvisionedWithIdentity, SignedMessageInformationFaultSignatureTooShort, SignedMessageInformationFaultTimeExpired, SignedMessageInformationFaultTokenAndCounterInvalid, SignedMessageInformationFaultTokenIsIncorrectLength, SignedMessageInformationFaultUnknown, SigningDisabled, SubscriptionRequired, TeslaFleetConnectionError, TeslaFleetError, TeslaFleetInformationFault, TeslaFleetMessageFault, TeslaFleetMessageFaultBadParameter, TeslaFleetMessageFaultBusy, TeslaFleetMessageFaultCommandRequiresAccountCredentials, TeslaFleetMessageFaultCouldNotHashMetadata, TeslaFleetMessageFaultDecoding, TeslaFleetMessageFaultFieldExceedsMTU, TeslaFleetMessageFaultIVIncorrectLength, TeslaFleetMessageFaultInactiveKey, TeslaFleetMessageFaultIncorrectEpoch, TeslaFleetMessageFaultInsufficientPrivileges, TeslaFleetMessageFaultInternal, TeslaFleetMessageFaultInvalidCommand, TeslaFleetMessageFaultInvalidDomains, TeslaFleetMessageFaultInvalidKeyHandle, TeslaFleetMessageFaultInvalidSignature, TeslaFleetMessageFaultInvalidTokenOrCounter, TeslaFleetMessageFaultKeychainIsFull, TeslaFleetMessageFaultNotProvisionedWithIdentity, TeslaFleetMessageFaultRemoteAccessDisabled, TeslaFleetMessageFaultRemoteServiceAccessDisabled, TeslaFleetMessageFaultRepeatedCounter, TeslaFleetMessageFaultRequiresResponseEncryption, TeslaFleetMessageFaultResponseSizeExceedsMTU, TeslaFleetMessageFaultTimeExpired, TeslaFleetMessageFaultTimeToLiveTooLong, TeslaFleetMessageFaultTimeout, TeslaFleetMessageFaultUnknownKeyId, TeslaFleetMessageFaultWrongPersonalization, TeslemetryRegistrationError, TimeExpiredFault, TokenAndCounterInvalidFault, TokenIsIncorrectLengthFault, UnauthorizedClient, UnknownFault, UnsupportedVehicle, VehicleOffline, VehicleSubscriptionRequired, WHITELIST_OPERATION_STATUS, WhitelistOperationAttemptingToAddExistingKey, WhitelistOperationAttemptingToAddKeyWithServiceRole, WhitelistOperationAttemptingToAddKeyWithoutRole, WhitelistOperationAttemptingToDemoteSuperiorToOneself, WhitelistOperationAttemptingToElevateOthersAboveOneself, WhitelistOperationAttemptingToRemoveOwnPermissions, WhitelistOperationCouldNotStartLocalEntityAuth, WhitelistOperationFMAttemptingToAddPermanentKey, WhitelistOperationFMAttemptingToRemovePermanentKey, WhitelistOperationFMModifyingOutsideOfFMode, WhitelistOperationInvalidPublicKey, WhitelistOperationKeychainWhileFSFull, WhitelistOperationKeyfobSlotsFull, WhitelistOperationLocalEntityAuthFailedCancelled, WhitelistOperationLocalEntityAuthFailedTimedOutWaitingForTap, WhitelistOperationLocalEntityAuthFailedTimedOutWaitingForUIAck, WhitelistOperationLocalEntityAuthFailedUIDenied, WhitelistOperationLocalEntityAuthFailedValetMode, WhitelistOperationNoPermissionToAdd, WhitelistOperationNoPermissionToChangePermissions, WhitelistOperationNoPermissionToRemove, WhitelistOperationNoPermissionToRemoveOneself, WhitelistOperationNonServiceKeyAttemptingToAddServiceTech, WhitelistOperationNotAllowedToAddUnlessOnReader, WhitelistOperationPublicKeyNotOnWhitelist, WhitelistOperationServiceKeyAttemptingToAddServiceTechOutsideServiceMode, WhitelistOperationStatus, WhitelistOperationUndocumentedError, WhitelistOperationWhitelistFull, is_key_rejected, raise_for_status } from "./exceptions.mjs";
|
|
12
|
-
import { getTariffPeriods } from "./tariff.mjs";
|
|
13
|
-
import {
|
|
14
|
-
|
|
24
|
+
import { getTariffPeriods, unwrapTariffV2 } from "./tariff.mjs";
|
|
25
|
+
import { firmwareAtLeast, firmwareCompare } from "./util.mjs";
|
|
26
|
+
import { AuthorizationRole, AuthorizedClientKeyType, AuthorizedClientState, AuthorizedClientType, AuthorizedVerificationType, AutoSeat, BluetoothVehicleData, CabinOverheatProtectionTemp, ClimateKeeperMode, ClimateModes, ClosureState, DEFAULT_LOCALE, DeviceType, DistanceUnit, EnergyDeviceIdentifierType, EnergyDisplayFormat, EnergyExportMode, EnergyIslandMode, EnergyOperationMode, Level, Method, Models, PhoneFontSize, SERVERS, Scope, SeatHeaterLevel, Seats, SnapshotTopic, SunRoofCommand, TOKEN_URLS, TemperatureUnit, TeslaEnergyPeriod, TimeDisplayFormat, TirePressureUnit, Trunk, VehicleDataEndpoint, VehicleImageType, WaitTopic, WindowCommand, is_valid_region } from "./const.mjs";
|
|
27
|
+
export { AESDecryptAuthFault, AuthorizationRole, AuthorizedClientKeyType, AuthorizedClientPairingTimedOut, AuthorizedClientState, AuthorizedClientType, AuthorizedClientWaitExpired, AuthorizedVerificationType, AutoSeat, BadGateway, BluetoothCloudUnavailableError, BluetoothCommandFailed, BluetoothTimeout, BluetoothTransportError, BluetoothUnconfirmedCommand, BluetoothVehicleData, CLOSURE_STATES, CabinOverheatProtectionTemp, Charging, ClientClosedRequest, ClimateKeeperMode, ClimateModes, ClosureState, Commands, CouldNotHashMetadataFault, CouldNotRetrieveKeyFault, CouldNotRetrieveTokenFault, DAY_OF_WEEK_BITS, DEFAULT_LOCALE, DEFAULT_PAIRING_POLL_INTERVAL, DEFAULT_PAIRING_TIMEOUT, DeviceType, DeviceUnexpectedResponse, DistanceUnit, ECDSAInputFault, ECDSASignatureFault, ENERGY_SIGNATURE_TTL_SECONDS, Energy, EnergyDeviceIdentifierType, EnergyDeviceSigner, EnergyDisplayFormat, EnergyExportMode, EnergyGatewayUnreachable, EnergyIslandMode, EnergyOperationMode, EnergySpecific, FieldPath, Forbidden, GatewayTimeout, INFORMATION_FAULTS, IVIncorrectLengthFault, IVSmallerThanExpectedFault, IncorrectEpochFault, InsufficientCredits, InternalServerError, InvalidAuthCode, InvalidCommand, InvalidField, InvalidMethod, InvalidRedirectUrl, InvalidRegion, InvalidRequest, InvalidResource, InvalidResponse, InvalidScope, InvalidTeslemetryToken, InvalidTessieToken, InvalidToken, InvalidTokenFault, KEY_REJECTED_FAULTS, LOCK_STATES, Level, LibraryError, LocalEntityResultFault, LocalEntityStartFault, Locked, LoginRequired, MAX_DISPATCH_DURATION_SECONDS, MESSAGE_FAULTS, Method, MissingTeslemetryToken, MissingTessieToken, MissingToken, MobileAccessDisabled, Models, NotAcceptable, NotAllowed, NotFound, NotOnWhitelistFault, NotProvisionedWithIdentityFault, OAuthExpired, OAuthInvalid, ObservationFunnel, Partner, PaymentRequired, PhoneFontSize, PreconditionFailed, PrivateKeyError, REGISTER_URL, RateLimited, ResourceUnavailableForLegalReasons, ResponseError, SERVERS, SIGNED_MESSAGE_INFORMATION_FAULTS, Scope, SeatHeaterLevel, Seats, ServiceUnavailable, Session, SessionInfoAuthenticationFault, SignatureTooShortFault, SignedCommandRequired, SignedCommandResponseReplayed, SignedMessageInformationFault, SignedMessageInformationFaultAESDecryptAuth, SignedMessageInformationFaultCouldNotHashMetadata, SignedMessageInformationFaultCouldNotRetrieveKey, SignedMessageInformationFaultCouldNotRetrieveToken, SignedMessageInformationFaultECDSAInput, SignedMessageInformationFaultECDSASignature, SignedMessageInformationFaultIVIncorrectLength, SignedMessageInformationFaultIVSmallerThanExpected, SignedMessageInformationFaultIncorrectEpoch, SignedMessageInformationFaultInvalidToken, SignedMessageInformationFaultLocalEntityResult, SignedMessageInformationFaultLocalEntityStart, SignedMessageInformationFaultNotOnWhitelist, SignedMessageInformationFaultNotProvisionedWithIdentity, SignedMessageInformationFaultSignatureTooShort, SignedMessageInformationFaultTimeExpired, SignedMessageInformationFaultTokenAndCounterInvalid, SignedMessageInformationFaultTokenIsIncorrectLength, SignedMessageInformationFaultUnknown, SigningDisabled, SnapshotTopic, SubscriptionRequired, SunRoofCommand, TOKEN_URLS, TemperatureUnit, TeslaEnergyPeriod, TeslaFleetApi, TeslaFleetConnectionError, TeslaFleetError, TeslaFleetInformationFault, TeslaFleetMessageFault, TeslaFleetMessageFaultBadParameter, TeslaFleetMessageFaultBusy, TeslaFleetMessageFaultCommandRequiresAccountCredentials, TeslaFleetMessageFaultCouldNotHashMetadata, TeslaFleetMessageFaultDecoding, TeslaFleetMessageFaultFieldExceedsMTU, TeslaFleetMessageFaultIVIncorrectLength, TeslaFleetMessageFaultInactiveKey, TeslaFleetMessageFaultIncorrectEpoch, TeslaFleetMessageFaultInsufficientPrivileges, TeslaFleetMessageFaultInternal, TeslaFleetMessageFaultInvalidCommand, TeslaFleetMessageFaultInvalidDomains, TeslaFleetMessageFaultInvalidKeyHandle, TeslaFleetMessageFaultInvalidSignature, TeslaFleetMessageFaultInvalidTokenOrCounter, TeslaFleetMessageFaultKeychainIsFull, TeslaFleetMessageFaultNotProvisionedWithIdentity, TeslaFleetMessageFaultRemoteAccessDisabled, TeslaFleetMessageFaultRemoteServiceAccessDisabled, TeslaFleetMessageFaultRepeatedCounter, TeslaFleetMessageFaultRequiresResponseEncryption, TeslaFleetMessageFaultResponseSizeExceedsMTU, TeslaFleetMessageFaultTimeExpired, TeslaFleetMessageFaultTimeToLiveTooLong, TeslaFleetMessageFaultTimeout, TeslaFleetMessageFaultUnknownKeyId, TeslaFleetMessageFaultWrongPersonalization, TeslaFleetOAuth, Teslemetry, TeslemetryEnergySite, TeslemetryEnergySites, TeslemetryRegistrationError, TeslemetryStreamPublisher, TeslemetryVehicle, TeslemetryVehicleTelemetry, TeslemetryVehicles, Tessie, TessieVehicle, TessieVehicles, TimeDisplayFormat, TimeExpiredFault, TirePressureUnit, TokenAndCounterInvalidFault, TokenIsIncorrectLengthFault, Trunk, UNSIGNED_DEVICE_COMMANDS, UnauthorizedClient, UnknownFault, UnsupportedVehicle, User, Vehicle, VehicleDataEndpoint, VehicleDataResultPublisher, VehicleImageType, VehicleOffline, VehicleSigned, VehicleSpecific, VehicleSubscriptionRequired, WHITELIST_OPERATION_STATUS, WaitTopic, WhitelistOperationAttemptingToAddExistingKey, WhitelistOperationAttemptingToAddKeyWithServiceRole, WhitelistOperationAttemptingToAddKeyWithoutRole, WhitelistOperationAttemptingToDemoteSuperiorToOneself, WhitelistOperationAttemptingToElevateOthersAboveOneself, WhitelistOperationAttemptingToRemoveOwnPermissions, WhitelistOperationCouldNotStartLocalEntityAuth, WhitelistOperationFMAttemptingToAddPermanentKey, WhitelistOperationFMAttemptingToRemovePermanentKey, WhitelistOperationFMModifyingOutsideOfFMode, WhitelistOperationInvalidPublicKey, WhitelistOperationKeychainWhileFSFull, WhitelistOperationKeyfobSlotsFull, WhitelistOperationLocalEntityAuthFailedCancelled, WhitelistOperationLocalEntityAuthFailedTimedOutWaitingForTap, WhitelistOperationLocalEntityAuthFailedTimedOutWaitingForUIAck, WhitelistOperationLocalEntityAuthFailedUIDenied, WhitelistOperationLocalEntityAuthFailedValetMode, WhitelistOperationNoPermissionToAdd, WhitelistOperationNoPermissionToChangePermissions, WhitelistOperationNoPermissionToRemove, WhitelistOperationNoPermissionToRemoveOneself, WhitelistOperationNonServiceKeyAttemptingToAddServiceTech, WhitelistOperationNotAllowedToAddUnlessOnReader, WhitelistOperationPublicKeyNotOnWhitelist, WhitelistOperationServiceKeyAttemptingToAddServiceTechOutsideServiceMode, WhitelistOperationStatus, WhitelistOperationUndocumentedError, WhitelistOperationWhitelistFull, WindowCommand, days_of_week_mask, ec_private_key_from_pem, firmwareAtLeast, firmwareCompare, generate_ec_private_key_pem, generate_rsa_private_key_pem, getTariffPeriods, infotainment_command_name, is_key_rejected, is_valid_region, jws_exports as jws, parse_authorized_clients, parse_gateway_address, raise_for_status, register_client, rsa_public_der_pkcs1, unwrapTariffV2, validate_dispatch_battery_power, vcsec_command_name };
|
package/dist/log.cjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const require_exceptions = require("./exceptions.cjs");
|
|
2
|
+
// src/log.ts
|
|
3
|
+
/**
|
|
4
|
+
* Parameter and body keys whose values never reach a log, lower-cased with
|
|
5
|
+
* separators removed. `migrate_to_oauth` sends and receives tokens through
|
|
6
|
+
* `_request`; PINs travel as `pin` or `password` body keys (Fleet, Teslemetry)
|
|
7
|
+
* or query parameters (Tessie); `trigger_homelink` and the telemetry JWS send
|
|
8
|
+
* `token`; a vehicle reply lists `tokens`; a signed `routable_message` executes
|
|
9
|
+
* again if replayed before it expires, up to 30 seconds later.
|
|
10
|
+
*/
|
|
11
|
+
const SECRET_KEYS = /* @__PURE__ */ new Set([
|
|
12
|
+
"accesstoken",
|
|
13
|
+
"clientsecret",
|
|
14
|
+
"idtoken",
|
|
15
|
+
"password",
|
|
16
|
+
"pin",
|
|
17
|
+
"privatekey",
|
|
18
|
+
"refreshtoken",
|
|
19
|
+
"routablemessage",
|
|
20
|
+
"token",
|
|
21
|
+
"tokens"
|
|
22
|
+
]);
|
|
23
|
+
const REDACTED = "[redacted]";
|
|
24
|
+
function isSecretKey(key) {
|
|
25
|
+
return SECRET_KEYS.has(key.replace(/[_-]/g, "").toLowerCase());
|
|
26
|
+
}
|
|
27
|
+
/** `JSON.stringify` for a log line, with secret values replaced at any depth. */
|
|
28
|
+
function redactedJson(value) {
|
|
29
|
+
return JSON.stringify(value, (key, val) => isSecretKey(key) ? REDACTED : val);
|
|
30
|
+
}
|
|
31
|
+
/** A query string for a log line, with secret values replaced. */
|
|
32
|
+
function redactedQuery(query) {
|
|
33
|
+
const redacted = new URLSearchParams(query);
|
|
34
|
+
for (const key of new Set(query.keys())) if (isSecretKey(key)) redacted.set(key, REDACTED);
|
|
35
|
+
return redacted.toString();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A `TeslaFleetError`'s message is its class's, with per-instance detail in
|
|
39
|
+
* `data`, so it is safe to log. Any other message, or thrown value, comes from
|
|
40
|
+
* outside (a token function, a transport) and may hold a secret, so only its
|
|
41
|
+
* type is logged.
|
|
42
|
+
*/
|
|
43
|
+
function errorText(error) {
|
|
44
|
+
if (error instanceof require_exceptions.TeslaFleetError) return `${error.name}: ${error.message}`;
|
|
45
|
+
return error instanceof Error ? error.name : typeof error;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Log the outcome of a completed request: a command's `result` and `reason`
|
|
49
|
+
* (inside `response`, or at the top level as Tessie replies) when present,
|
|
50
|
+
* otherwise plain success. Mirrors Python's `_log_request_result`.
|
|
51
|
+
*/
|
|
52
|
+
function logRequestResult(log, command, transport, data) {
|
|
53
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
54
|
+
log(`command=${command} transport=${transport} result=success`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const response = data.response;
|
|
58
|
+
let result;
|
|
59
|
+
if (response !== null && typeof response === "object" && "result" in response) result = response;
|
|
60
|
+
else if ("result" in data) result = data;
|
|
61
|
+
else {
|
|
62
|
+
log(`command=${command} transport=${transport} result=success`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
log(`command=${command} transport=${transport} result=${result.result} reason=${result.reason ?? ""}`);
|
|
66
|
+
}
|
|
67
|
+
/** Log a failed request or command. The error class name carries the failure kind. */
|
|
68
|
+
function logRequestError(log, command, transport, error) {
|
|
69
|
+
log(`command=${command} transport=${transport} result=error error=${errorText(error)}`);
|
|
70
|
+
}
|
|
71
|
+
exports.errorText = errorText;
|
|
72
|
+
exports.logRequestError = logRequestError;
|
|
73
|
+
exports.logRequestResult = logRequestResult;
|
|
74
|
+
exports.redactedJson = redactedJson;
|
|
75
|
+
exports.redactedQuery = redactedQuery;
|
package/dist/log.d.cts
ADDED
package/dist/log.d.mts
ADDED
package/dist/log.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { TeslaFleetError } from "./exceptions.mjs";
|
|
2
|
+
// src/log.ts
|
|
3
|
+
/**
|
|
4
|
+
* Parameter and body keys whose values never reach a log, lower-cased with
|
|
5
|
+
* separators removed. `migrate_to_oauth` sends and receives tokens through
|
|
6
|
+
* `_request`; PINs travel as `pin` or `password` body keys (Fleet, Teslemetry)
|
|
7
|
+
* or query parameters (Tessie); `trigger_homelink` and the telemetry JWS send
|
|
8
|
+
* `token`; a vehicle reply lists `tokens`; a signed `routable_message` executes
|
|
9
|
+
* again if replayed before it expires, up to 30 seconds later.
|
|
10
|
+
*/
|
|
11
|
+
const SECRET_KEYS = /* @__PURE__ */ new Set([
|
|
12
|
+
"accesstoken",
|
|
13
|
+
"clientsecret",
|
|
14
|
+
"idtoken",
|
|
15
|
+
"password",
|
|
16
|
+
"pin",
|
|
17
|
+
"privatekey",
|
|
18
|
+
"refreshtoken",
|
|
19
|
+
"routablemessage",
|
|
20
|
+
"token",
|
|
21
|
+
"tokens"
|
|
22
|
+
]);
|
|
23
|
+
const REDACTED = "[redacted]";
|
|
24
|
+
function isSecretKey(key) {
|
|
25
|
+
return SECRET_KEYS.has(key.replace(/[_-]/g, "").toLowerCase());
|
|
26
|
+
}
|
|
27
|
+
/** `JSON.stringify` for a log line, with secret values replaced at any depth. */
|
|
28
|
+
function redactedJson(value) {
|
|
29
|
+
return JSON.stringify(value, (key, val) => isSecretKey(key) ? REDACTED : val);
|
|
30
|
+
}
|
|
31
|
+
/** A query string for a log line, with secret values replaced. */
|
|
32
|
+
function redactedQuery(query) {
|
|
33
|
+
const redacted = new URLSearchParams(query);
|
|
34
|
+
for (const key of new Set(query.keys())) if (isSecretKey(key)) redacted.set(key, REDACTED);
|
|
35
|
+
return redacted.toString();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A `TeslaFleetError`'s message is its class's, with per-instance detail in
|
|
39
|
+
* `data`, so it is safe to log. Any other message, or thrown value, comes from
|
|
40
|
+
* outside (a token function, a transport) and may hold a secret, so only its
|
|
41
|
+
* type is logged.
|
|
42
|
+
*/
|
|
43
|
+
function errorText(error) {
|
|
44
|
+
if (error instanceof TeslaFleetError) return `${error.name}: ${error.message}`;
|
|
45
|
+
return error instanceof Error ? error.name : typeof error;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Log the outcome of a completed request: a command's `result` and `reason`
|
|
49
|
+
* (inside `response`, or at the top level as Tessie replies) when present,
|
|
50
|
+
* otherwise plain success. Mirrors Python's `_log_request_result`.
|
|
51
|
+
*/
|
|
52
|
+
function logRequestResult(log, command, transport, data) {
|
|
53
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
54
|
+
log(`command=${command} transport=${transport} result=success`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const response = data.response;
|
|
58
|
+
let result;
|
|
59
|
+
if (response !== null && typeof response === "object" && "result" in response) result = response;
|
|
60
|
+
else if ("result" in data) result = data;
|
|
61
|
+
else {
|
|
62
|
+
log(`command=${command} transport=${transport} result=success`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
log(`command=${command} transport=${transport} result=${result.result} reason=${result.reason ?? ""}`);
|
|
66
|
+
}
|
|
67
|
+
/** Log a failed request or command. The error class name carries the failure kind. */
|
|
68
|
+
function logRequestError(log, command, transport, error) {
|
|
69
|
+
log(`command=${command} transport=${transport} result=error error=${errorText(error)}`);
|
|
70
|
+
}
|
|
71
|
+
export { errorText, logRequestError, logRequestResult, redactedJson, redactedQuery };
|
package/dist/oauth.cjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const require_const = require("./const.cjs");
|
|
2
|
+
const require_exceptions = require("./exceptions.cjs");
|
|
3
|
+
const require_teslafleetapi = require("./teslafleetapi.cjs");
|
|
4
|
+
// src/oauth.ts
|
|
5
|
+
function nowSeconds() {
|
|
6
|
+
return Date.now() / 1e3;
|
|
7
|
+
}
|
|
8
|
+
/** The fields every token reply needs; a reply without them must not replace a working token. */
|
|
9
|
+
function tokenFrom(data) {
|
|
10
|
+
if (typeof data?.access_token !== "string" || typeof data.expires_in !== "number") throw new require_exceptions.InvalidResponse();
|
|
11
|
+
return {
|
|
12
|
+
access_token: data.access_token,
|
|
13
|
+
expires: Math.floor(nowSeconds()) + data.expires_in
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Tesla Fleet API client that signs a user in with OAuth and keeps its access
|
|
18
|
+
* token fresh. `expires` is in epoch seconds, as in Python, so a stored
|
|
19
|
+
* token works with either library.
|
|
20
|
+
*/
|
|
21
|
+
var TeslaFleetOAuth = class extends require_teslafleetapi.default {
|
|
22
|
+
client_id;
|
|
23
|
+
redirect_uri;
|
|
24
|
+
refresh_token;
|
|
25
|
+
/** Expiry of the access token, in epoch seconds. */
|
|
26
|
+
expires;
|
|
27
|
+
#client_secret;
|
|
28
|
+
#refreshing = null;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
if (!require_const.is_valid_region(options.region)) throw new Error("Invalid region");
|
|
31
|
+
const { region, client_id, client_secret, redirect_uri, access_token, refresh_token, expires, ...rest } = options;
|
|
32
|
+
super({
|
|
33
|
+
...rest,
|
|
34
|
+
region,
|
|
35
|
+
accessToken: access_token
|
|
36
|
+
});
|
|
37
|
+
this.client_id = client_id;
|
|
38
|
+
this.#client_secret = client_secret ?? null;
|
|
39
|
+
this.redirect_uri = redirect_uri ?? null;
|
|
40
|
+
this.refresh_token = refresh_token ?? null;
|
|
41
|
+
this.expires = expires ?? 0;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The URL to send the user to, built exactly as Python builds it: values are
|
|
45
|
+
* not URL-encoded.
|
|
46
|
+
* @param scopes Scopes to request.
|
|
47
|
+
* @param state Value Tesla echoes back on the redirect.
|
|
48
|
+
*/
|
|
49
|
+
get_login_url(scopes, state = "login") {
|
|
50
|
+
if (this.redirect_uri == null) throw new Error("Redirect URI is missing");
|
|
51
|
+
return `https://${this.region === "cn" ? "auth.tesla.cn" : "auth.tesla.com"}/oauth2/v3/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&scope=${scopes.join("+")}&state=${state}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Exchange an authorization code for tokens. A code prefixed with a region
|
|
55
|
+
* (e.g. `NA_...`) also selects that region's server.
|
|
56
|
+
* @param code The code from the login redirect.
|
|
57
|
+
*/
|
|
58
|
+
async get_refresh_token(code) {
|
|
59
|
+
if (this.#client_secret == null) throw new Error("Client secret is missing");
|
|
60
|
+
if (this.redirect_uri == null) throw new Error("Redirect URI is missing");
|
|
61
|
+
this.server ??= require_const.SERVERS[this.region ?? "na"];
|
|
62
|
+
const data = await this.#token({
|
|
63
|
+
grant_type: "authorization_code",
|
|
64
|
+
client_id: this.client_id,
|
|
65
|
+
client_secret: this.#client_secret,
|
|
66
|
+
code,
|
|
67
|
+
audience: this.server,
|
|
68
|
+
redirect_uri: this.redirect_uri
|
|
69
|
+
});
|
|
70
|
+
const { access_token, expires } = tokenFrom(data);
|
|
71
|
+
this.refresh_token = data.refresh_token ?? null;
|
|
72
|
+
this.accessToken = access_token;
|
|
73
|
+
this.expires = expires;
|
|
74
|
+
const region = code.split("_")[0].toLowerCase();
|
|
75
|
+
if (require_const.is_valid_region(region)) this.server = require_const.SERVERS[region];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The current access token, refreshed first when `expires` has passed.
|
|
79
|
+
* @throws MissingToken when there is no token and none can be refreshed.
|
|
80
|
+
*/
|
|
81
|
+
async access_token() {
|
|
82
|
+
if (this.expires < nowSeconds()) await this.refresh_access_token();
|
|
83
|
+
if (this.accessToken && typeof this.accessToken === "string") return this.accessToken;
|
|
84
|
+
throw new require_exceptions.MissingToken("Access token is missing");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Refresh the access token if it is missing or expired.
|
|
88
|
+
* @returns The refreshed token to persist, or `null` when none was needed.
|
|
89
|
+
*/
|
|
90
|
+
async check_access_token() {
|
|
91
|
+
if (this.accessToken && this.expires > nowSeconds()) return null;
|
|
92
|
+
return this.refresh_access_token();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Refresh the access token. Tesla rotates the refresh token, so persist what
|
|
96
|
+
* this returns.
|
|
97
|
+
* @throws MissingToken without a refresh token, or the `TeslaFleetError`
|
|
98
|
+
* matching the failure, e.g. `LoginRequired`.
|
|
99
|
+
*/
|
|
100
|
+
refresh_access_token() {
|
|
101
|
+
this.#refreshing ??= this.#refresh().finally(() => {
|
|
102
|
+
this.#refreshing = null;
|
|
103
|
+
});
|
|
104
|
+
return this.#refreshing;
|
|
105
|
+
}
|
|
106
|
+
async #refresh() {
|
|
107
|
+
if (!this.refresh_token) throw new require_exceptions.MissingToken("Refresh token is missing");
|
|
108
|
+
const data = await this.#token({
|
|
109
|
+
grant_type: "refresh_token",
|
|
110
|
+
client_id: this.client_id,
|
|
111
|
+
refresh_token: this.refresh_token
|
|
112
|
+
});
|
|
113
|
+
const { access_token, expires } = tokenFrom(data);
|
|
114
|
+
if (typeof data.refresh_token !== "string") throw new require_exceptions.InvalidResponse();
|
|
115
|
+
this.accessToken = access_token;
|
|
116
|
+
this.refresh_token = data.refresh_token;
|
|
117
|
+
this.expires = expires;
|
|
118
|
+
return {
|
|
119
|
+
refresh_token: data.refresh_token,
|
|
120
|
+
expires
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** POST a form to the region's token endpoint. Bypasses `_request`, which would log the reply. */
|
|
124
|
+
async #token(form) {
|
|
125
|
+
try {
|
|
126
|
+
const resp = await this._fetch(require_const.TOKEN_URLS[this.region ?? "na"], {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
129
|
+
body: new URLSearchParams(form),
|
|
130
|
+
signal: this.timeout != null ? AbortSignal.timeout(this.timeout) : null
|
|
131
|
+
});
|
|
132
|
+
return await this._response(resp);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof require_exceptions.TeslaFleetError) throw error;
|
|
135
|
+
throw new require_exceptions.TeslaFleetConnectionError(null, null, { cause: error });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async _request(method, path, params = null, json = null) {
|
|
139
|
+
await this.check_access_token();
|
|
140
|
+
return super._request(method, path, params, json);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
exports.default = TeslaFleetOAuth;
|
package/dist/oauth.d.cts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Method, Region, Scope } from "./const.cjs";
|
|
2
|
+
import { Logger } from "./log.cjs";
|
|
3
|
+
import TeslaFleetApi from "./teslafleetapi.cjs";
|
|
4
|
+
export type TeslaFleetOAuthOptions = {
|
|
5
|
+
region: Region;
|
|
6
|
+
client_id: string;
|
|
7
|
+
/** Needed only to exchange an authorization code. */
|
|
8
|
+
client_secret?: string;
|
|
9
|
+
redirect_uri?: string;
|
|
10
|
+
access_token?: string;
|
|
11
|
+
refresh_token?: string;
|
|
12
|
+
/** Expiry of `access_token`, in epoch seconds as `TeslaFleetOAuth.expires`. */
|
|
13
|
+
expires?: number;
|
|
14
|
+
debug?: boolean;
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
};
|
|
19
|
+
/** What a refresh hands back for the caller to persist. */
|
|
20
|
+
export type RefreshedToken = {
|
|
21
|
+
refresh_token: string;
|
|
22
|
+
/** Epoch seconds. */
|
|
23
|
+
expires: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Tesla Fleet API client that signs a user in with OAuth and keeps its access
|
|
27
|
+
* token fresh. `expires` is in epoch seconds, as in Python, so a stored
|
|
28
|
+
* token works with either library.
|
|
29
|
+
*/
|
|
30
|
+
export default class TeslaFleetOAuth extends TeslaFleetApi {
|
|
31
|
+
#private;
|
|
32
|
+
readonly client_id: string;
|
|
33
|
+
redirect_uri: string | null;
|
|
34
|
+
refresh_token: string | null;
|
|
35
|
+
/** Expiry of the access token, in epoch seconds. */
|
|
36
|
+
expires: number;
|
|
37
|
+
constructor(options: TeslaFleetOAuthOptions);
|
|
38
|
+
/**
|
|
39
|
+
* The URL to send the user to, built exactly as Python builds it: values are
|
|
40
|
+
* not URL-encoded.
|
|
41
|
+
* @param scopes Scopes to request.
|
|
42
|
+
* @param state Value Tesla echoes back on the redirect.
|
|
43
|
+
*/
|
|
44
|
+
get_login_url(scopes: Scope[], state?: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Exchange an authorization code for tokens. A code prefixed with a region
|
|
47
|
+
* (e.g. `NA_...`) also selects that region's server.
|
|
48
|
+
* @param code The code from the login redirect.
|
|
49
|
+
*/
|
|
50
|
+
get_refresh_token(code: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* The current access token, refreshed first when `expires` has passed.
|
|
53
|
+
* @throws MissingToken when there is no token and none can be refreshed.
|
|
54
|
+
*/
|
|
55
|
+
access_token(): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Refresh the access token if it is missing or expired.
|
|
58
|
+
* @returns The refreshed token to persist, or `null` when none was needed.
|
|
59
|
+
*/
|
|
60
|
+
check_access_token(): Promise<RefreshedToken | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Refresh the access token. Tesla rotates the refresh token, so persist what
|
|
63
|
+
* this returns.
|
|
64
|
+
* @throws MissingToken without a refresh token, or the `TeslaFleetError`
|
|
65
|
+
* matching the failure, e.g. `LoginRequired`.
|
|
66
|
+
*/
|
|
67
|
+
refresh_access_token(): Promise<RefreshedToken>;
|
|
68
|
+
_request(method: Method, path: string, params?: Record<string, any> | null, json?: Record<string, any> | null): Promise<any>;
|
|
69
|
+
}
|
package/dist/oauth.d.mts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Method, Region, Scope } from "./const.mjs";
|
|
2
|
+
import { Logger } from "./log.mjs";
|
|
3
|
+
import TeslaFleetApi from "./teslafleetapi.mjs";
|
|
4
|
+
export type TeslaFleetOAuthOptions = {
|
|
5
|
+
region: Region;
|
|
6
|
+
client_id: string;
|
|
7
|
+
/** Needed only to exchange an authorization code. */
|
|
8
|
+
client_secret?: string;
|
|
9
|
+
redirect_uri?: string;
|
|
10
|
+
access_token?: string;
|
|
11
|
+
refresh_token?: string;
|
|
12
|
+
/** Expiry of `access_token`, in epoch seconds as `TeslaFleetOAuth.expires`. */
|
|
13
|
+
expires?: number;
|
|
14
|
+
debug?: boolean;
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
};
|
|
19
|
+
/** What a refresh hands back for the caller to persist. */
|
|
20
|
+
export type RefreshedToken = {
|
|
21
|
+
refresh_token: string;
|
|
22
|
+
/** Epoch seconds. */
|
|
23
|
+
expires: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Tesla Fleet API client that signs a user in with OAuth and keeps its access
|
|
27
|
+
* token fresh. `expires` is in epoch seconds, as in Python, so a stored
|
|
28
|
+
* token works with either library.
|
|
29
|
+
*/
|
|
30
|
+
export default class TeslaFleetOAuth extends TeslaFleetApi {
|
|
31
|
+
#private;
|
|
32
|
+
readonly client_id: string;
|
|
33
|
+
redirect_uri: string | null;
|
|
34
|
+
refresh_token: string | null;
|
|
35
|
+
/** Expiry of the access token, in epoch seconds. */
|
|
36
|
+
expires: number;
|
|
37
|
+
constructor(options: TeslaFleetOAuthOptions);
|
|
38
|
+
/**
|
|
39
|
+
* The URL to send the user to, built exactly as Python builds it: values are
|
|
40
|
+
* not URL-encoded.
|
|
41
|
+
* @param scopes Scopes to request.
|
|
42
|
+
* @param state Value Tesla echoes back on the redirect.
|
|
43
|
+
*/
|
|
44
|
+
get_login_url(scopes: Scope[], state?: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Exchange an authorization code for tokens. A code prefixed with a region
|
|
47
|
+
* (e.g. `NA_...`) also selects that region's server.
|
|
48
|
+
* @param code The code from the login redirect.
|
|
49
|
+
*/
|
|
50
|
+
get_refresh_token(code: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* The current access token, refreshed first when `expires` has passed.
|
|
53
|
+
* @throws MissingToken when there is no token and none can be refreshed.
|
|
54
|
+
*/
|
|
55
|
+
access_token(): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Refresh the access token if it is missing or expired.
|
|
58
|
+
* @returns The refreshed token to persist, or `null` when none was needed.
|
|
59
|
+
*/
|
|
60
|
+
check_access_token(): Promise<RefreshedToken | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Refresh the access token. Tesla rotates the refresh token, so persist what
|
|
63
|
+
* this returns.
|
|
64
|
+
* @throws MissingToken without a refresh token, or the `TeslaFleetError`
|
|
65
|
+
* matching the failure, e.g. `LoginRequired`.
|
|
66
|
+
*/
|
|
67
|
+
refresh_access_token(): Promise<RefreshedToken>;
|
|
68
|
+
_request(method: Method, path: string, params?: Record<string, any> | null, json?: Record<string, any> | null): Promise<any>;
|
|
69
|
+
}
|
package/dist/oauth.mjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { SERVERS, TOKEN_URLS, is_valid_region } from "./const.mjs";
|
|
2
|
+
import { InvalidResponse, MissingToken, TeslaFleetConnectionError, TeslaFleetError } from "./exceptions.mjs";
|
|
3
|
+
import TeslaFleetApi from "./teslafleetapi.mjs";
|
|
4
|
+
// src/oauth.ts
|
|
5
|
+
function nowSeconds() {
|
|
6
|
+
return Date.now() / 1e3;
|
|
7
|
+
}
|
|
8
|
+
/** The fields every token reply needs; a reply without them must not replace a working token. */
|
|
9
|
+
function tokenFrom(data) {
|
|
10
|
+
if (typeof data?.access_token !== "string" || typeof data.expires_in !== "number") throw new InvalidResponse();
|
|
11
|
+
return {
|
|
12
|
+
access_token: data.access_token,
|
|
13
|
+
expires: Math.floor(nowSeconds()) + data.expires_in
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Tesla Fleet API client that signs a user in with OAuth and keeps its access
|
|
18
|
+
* token fresh. `expires` is in epoch seconds, as in Python, so a stored
|
|
19
|
+
* token works with either library.
|
|
20
|
+
*/
|
|
21
|
+
var TeslaFleetOAuth = class extends TeslaFleetApi {
|
|
22
|
+
client_id;
|
|
23
|
+
redirect_uri;
|
|
24
|
+
refresh_token;
|
|
25
|
+
/** Expiry of the access token, in epoch seconds. */
|
|
26
|
+
expires;
|
|
27
|
+
#client_secret;
|
|
28
|
+
#refreshing = null;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
if (!is_valid_region(options.region)) throw new Error("Invalid region");
|
|
31
|
+
const { region, client_id, client_secret, redirect_uri, access_token, refresh_token, expires, ...rest } = options;
|
|
32
|
+
super({
|
|
33
|
+
...rest,
|
|
34
|
+
region,
|
|
35
|
+
accessToken: access_token
|
|
36
|
+
});
|
|
37
|
+
this.client_id = client_id;
|
|
38
|
+
this.#client_secret = client_secret ?? null;
|
|
39
|
+
this.redirect_uri = redirect_uri ?? null;
|
|
40
|
+
this.refresh_token = refresh_token ?? null;
|
|
41
|
+
this.expires = expires ?? 0;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The URL to send the user to, built exactly as Python builds it: values are
|
|
45
|
+
* not URL-encoded.
|
|
46
|
+
* @param scopes Scopes to request.
|
|
47
|
+
* @param state Value Tesla echoes back on the redirect.
|
|
48
|
+
*/
|
|
49
|
+
get_login_url(scopes, state = "login") {
|
|
50
|
+
if (this.redirect_uri == null) throw new Error("Redirect URI is missing");
|
|
51
|
+
return `https://${this.region === "cn" ? "auth.tesla.cn" : "auth.tesla.com"}/oauth2/v3/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&scope=${scopes.join("+")}&state=${state}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Exchange an authorization code for tokens. A code prefixed with a region
|
|
55
|
+
* (e.g. `NA_...`) also selects that region's server.
|
|
56
|
+
* @param code The code from the login redirect.
|
|
57
|
+
*/
|
|
58
|
+
async get_refresh_token(code) {
|
|
59
|
+
if (this.#client_secret == null) throw new Error("Client secret is missing");
|
|
60
|
+
if (this.redirect_uri == null) throw new Error("Redirect URI is missing");
|
|
61
|
+
this.server ??= SERVERS[this.region ?? "na"];
|
|
62
|
+
const data = await this.#token({
|
|
63
|
+
grant_type: "authorization_code",
|
|
64
|
+
client_id: this.client_id,
|
|
65
|
+
client_secret: this.#client_secret,
|
|
66
|
+
code,
|
|
67
|
+
audience: this.server,
|
|
68
|
+
redirect_uri: this.redirect_uri
|
|
69
|
+
});
|
|
70
|
+
const { access_token, expires } = tokenFrom(data);
|
|
71
|
+
this.refresh_token = data.refresh_token ?? null;
|
|
72
|
+
this.accessToken = access_token;
|
|
73
|
+
this.expires = expires;
|
|
74
|
+
const region = code.split("_")[0].toLowerCase();
|
|
75
|
+
if (is_valid_region(region)) this.server = SERVERS[region];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The current access token, refreshed first when `expires` has passed.
|
|
79
|
+
* @throws MissingToken when there is no token and none can be refreshed.
|
|
80
|
+
*/
|
|
81
|
+
async access_token() {
|
|
82
|
+
if (this.expires < nowSeconds()) await this.refresh_access_token();
|
|
83
|
+
if (this.accessToken && typeof this.accessToken === "string") return this.accessToken;
|
|
84
|
+
throw new MissingToken("Access token is missing");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Refresh the access token if it is missing or expired.
|
|
88
|
+
* @returns The refreshed token to persist, or `null` when none was needed.
|
|
89
|
+
*/
|
|
90
|
+
async check_access_token() {
|
|
91
|
+
if (this.accessToken && this.expires > nowSeconds()) return null;
|
|
92
|
+
return this.refresh_access_token();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Refresh the access token. Tesla rotates the refresh token, so persist what
|
|
96
|
+
* this returns.
|
|
97
|
+
* @throws MissingToken without a refresh token, or the `TeslaFleetError`
|
|
98
|
+
* matching the failure, e.g. `LoginRequired`.
|
|
99
|
+
*/
|
|
100
|
+
refresh_access_token() {
|
|
101
|
+
this.#refreshing ??= this.#refresh().finally(() => {
|
|
102
|
+
this.#refreshing = null;
|
|
103
|
+
});
|
|
104
|
+
return this.#refreshing;
|
|
105
|
+
}
|
|
106
|
+
async #refresh() {
|
|
107
|
+
if (!this.refresh_token) throw new MissingToken("Refresh token is missing");
|
|
108
|
+
const data = await this.#token({
|
|
109
|
+
grant_type: "refresh_token",
|
|
110
|
+
client_id: this.client_id,
|
|
111
|
+
refresh_token: this.refresh_token
|
|
112
|
+
});
|
|
113
|
+
const { access_token, expires } = tokenFrom(data);
|
|
114
|
+
if (typeof data.refresh_token !== "string") throw new InvalidResponse();
|
|
115
|
+
this.accessToken = access_token;
|
|
116
|
+
this.refresh_token = data.refresh_token;
|
|
117
|
+
this.expires = expires;
|
|
118
|
+
return {
|
|
119
|
+
refresh_token: data.refresh_token,
|
|
120
|
+
expires
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** POST a form to the region's token endpoint. Bypasses `_request`, which would log the reply. */
|
|
124
|
+
async #token(form) {
|
|
125
|
+
try {
|
|
126
|
+
const resp = await this._fetch(TOKEN_URLS[this.region ?? "na"], {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
129
|
+
body: new URLSearchParams(form),
|
|
130
|
+
signal: this.timeout != null ? AbortSignal.timeout(this.timeout) : null
|
|
131
|
+
});
|
|
132
|
+
return await this._response(resp);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof TeslaFleetError) throw error;
|
|
135
|
+
throw new TeslaFleetConnectionError(null, null, { cause: error });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async _request(method, path, params = null, json = null) {
|
|
139
|
+
await this.check_access_token();
|
|
140
|
+
return super._request(method, path, params, json);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
export { TeslaFleetOAuth as default };
|