homebridge-kia 1.0.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 ADDED
@@ -0,0 +1,108 @@
1
+ # homebridge-kia-connect
2
+
3
+ Homebridge plugin for Kia Connect vehicles.
4
+
5
+ This plugin exposes a Kia vehicle to Apple Home through Homebridge, including vehicle status sensors and a small set of remote controls.
6
+
7
+ ## Features
8
+
9
+ - Vehicle status in HomeKit
10
+ - Door lock / unlock
11
+ - Remote climate start / stop
12
+ - Fuel level
13
+ - 12V battery level
14
+ - Outside temperature
15
+ - Engine running state
16
+ - Door, window, hood, and trunk sensors
17
+ - Tire pressure warning
18
+ - OTP-assisted Kia Connect authentication through the Homebridge UI
19
+
20
+ ## Requirements
21
+
22
+ - Node.js `20.18.0` or newer
23
+ - Homebridge `1.8.0` or newer
24
+ - A Kia Connect account for a supported US Kia vehicle
25
+
26
+ ## Installation
27
+
28
+ Install through the Homebridge UI, or with npm:
29
+
30
+ ```bash
31
+ npm install -g homebridge-kia
32
+ ```
33
+
34
+ Then restart Homebridge.
35
+
36
+ ## Configuration
37
+
38
+ The plugin is a dynamic platform and uses the following settings:
39
+
40
+ - `username`: Kia Connect email
41
+ - `password`: Kia Connect password
42
+ - `vehicleIndex`: Which vehicle to use if your account has multiple vehicles
43
+ - `pollIntervalMinutes`: Refresh interval, minimum `5`
44
+ - `enableDoorLock`: Show the HomeKit lock service
45
+ - `enableClimateControl`: Show the HomeKit climate switch
46
+
47
+ Example:
48
+
49
+ ```json
50
+ {
51
+ "platform": "KiaConnect",
52
+ "name": "Kia Connect",
53
+ "username": "you@example.com",
54
+ "password": "your-password",
55
+ "vehicleIndex": 0,
56
+ "pollIntervalMinutes": 30,
57
+ "enableDoorLock": true,
58
+ "enableClimateControl": true
59
+ }
60
+ ```
61
+
62
+ ## Authentication / OTP
63
+
64
+ If Kia Connect requires a one-time password:
65
+
66
+ 1. Open the plugin settings in Homebridge.
67
+ 2. Enter your Kia Connect email and password if needed.
68
+ 3. Click `Login`.
69
+ 4. Choose `Email` or `SMS` for the OTP.
70
+ 5. Enter the code and verify it.
71
+ 6. Restart Homebridge after authentication succeeds.
72
+
73
+ ## HomeKit Services
74
+
75
+ This plugin creates one accessory for the selected vehicle and can expose:
76
+
77
+ - `LockMechanism` for door lock control
78
+ - `Switch` for climate control
79
+ - `Battery` for 12V battery level
80
+ - `HumiditySensor` for fuel level
81
+ - `TemperatureSensor` for outside temperature
82
+ - `OccupancySensor` for engine running
83
+ - `ContactSensor` services for doors, windows, hood, and trunk
84
+ - `LeakSensor` for tire pressure warning
85
+
86
+ ## Notes
87
+
88
+ - This plugin currently targets the US Kia Connect API.
89
+ - Climate control uses a fixed start temperature of `72F`.
90
+ - Polling is periodic and should not be set aggressively.
91
+
92
+ ## Development
93
+
94
+ ```bash
95
+ npm install
96
+ npm run build
97
+ npm run lint
98
+ ```
99
+
100
+ Project layout:
101
+
102
+ - [src](/Users/jordanfreund/Desktop/homebridge-kia-connect/src)
103
+ - [homebridge-ui](/Users/jordanfreund/Desktop/homebridge-kia-connect/homebridge-ui)
104
+ - [config.schema.json](/Users/jordanfreund/Desktop/homebridge-kia-connect/config.schema.json)
105
+
106
+ ## License
107
+
108
+ ISC
@@ -0,0 +1,51 @@
1
+ {
2
+ "pluginAlias": "KiaConnect",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "customUi": true,
6
+ "customUiPath": "./dist/homebridge-ui",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "name": {
11
+ "title": "Name",
12
+ "type": "string",
13
+ "default": "Kia Connect",
14
+ "required": true
15
+ },
16
+ "username": {
17
+ "title": "Kia Connect Email",
18
+ "type": "string",
19
+ "required": true
20
+ },
21
+ "password": {
22
+ "title": "Kia Connect Password",
23
+ "type": "string",
24
+ "required": true
25
+ },
26
+ "pollIntervalMinutes": {
27
+ "title": "Poll Interval (minutes)",
28
+ "type": "integer",
29
+ "default": 30,
30
+ "minimum": 5
31
+ },
32
+ "enableClimateControl": {
33
+ "title": "Enable Climate Control",
34
+ "type": "boolean",
35
+ "default": true
36
+ },
37
+ "enableDoorLock": {
38
+ "title": "Enable Door Lock",
39
+ "type": "boolean",
40
+ "default": true
41
+ },
42
+ "vehicleIndex": {
43
+ "title": "Vehicle Index",
44
+ "type": "integer",
45
+ "default": 0,
46
+ "minimum": 0,
47
+ "description": "If you have multiple vehicles, specify the index (0 = first vehicle)"
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,5 @@
1
+ export type SavedCredentials = {
2
+ username: string;
3
+ password: string;
4
+ };
5
+ export declare function readSavedCredentials(homebridgeConfigPath?: string): SavedCredentials | undefined;
@@ -0,0 +1,23 @@
1
+ import { readFileSync } from 'node:fs';
2
+ export function readSavedCredentials(homebridgeConfigPath) {
3
+ if (!homebridgeConfigPath) {
4
+ return undefined;
5
+ }
6
+ try {
7
+ const config = JSON.parse(readFileSync(homebridgeConfigPath, 'utf-8'));
8
+ const pluginConfig = config.platforms?.find(platform => (platform.platform === 'KiaConnect'
9
+ && typeof platform.username === 'string'
10
+ && typeof platform.password === 'string'));
11
+ if (!pluginConfig) {
12
+ return undefined;
13
+ }
14
+ return {
15
+ username: pluginConfig.username,
16
+ password: pluginConfig.password,
17
+ };
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../homebridge-ui/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAIvC,MAAM,UAAU,oBAAoB,CAAC,oBAA6B;IAChE,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC1B,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAEpE,CAAC;QAEF,MAAM,YAAY,GAAG,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CACtD,QAAQ,CAAC,QAAQ,KAAK,YAAY;eAC/B,OAAO,QAAQ,CAAC,QAAQ,KAAK,QAAQ;eACrC,OAAO,QAAQ,CAAC,QAAQ,KAAK,QAAQ,CACzC,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,OAAO;YACL,QAAQ,EAAE,YAAY,CAAC,QAAkB;YACzC,QAAQ,EAAE,YAAY,CAAC,QAAkB;SAC1C,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,196 @@
1
+ <style>
2
+ .status-card { margin-bottom: 1rem; }
3
+ .otp-section { display: none; }
4
+ .success-section { display: none; }
5
+ .btn-group { margin-bottom: 0.5rem; }
6
+ #status-message { font-weight: bold; }
7
+ </style>
8
+
9
+ <!-- Status -->
10
+ <div class="card status-card">
11
+ <div class="card-body">
12
+ <h5 class="card-title">Kia Connect Authentication</h5>
13
+ <div class="form-group">
14
+ <label for="username">Kia Connect Email</label>
15
+ <input type="email" id="username" class="form-control" autocomplete="username">
16
+ </div>
17
+ <div class="form-group">
18
+ <label for="password">Kia Connect Password</label>
19
+ <input type="password" id="password" class="form-control" autocomplete="current-password">
20
+ </div>
21
+ <p id="status-message">Checking authentication status...</p>
22
+ <button id="login-btn" class="btn btn-primary" style="display:none" onclick="doLogin()">Login</button>
23
+ </div>
24
+ </div>
25
+
26
+ <!-- OTP Section -->
27
+ <div id="otp-section" class="card otp-section">
28
+ <div class="card-body">
29
+ <h5 class="card-title">One-Time Password Required</h5>
30
+ <p id="otp-info"></p>
31
+ <div class="btn-group">
32
+ <button class="btn btn-outline-primary btn-sm" onclick="sendOtp('EMAIL')">Send via Email</button>
33
+ <button class="btn btn-outline-secondary btn-sm" onclick="sendOtp('SMS')">Send via SMS</button>
34
+ </div>
35
+ <div class="input-group mt-3">
36
+ <input type="text" id="otp-code" class="form-control" placeholder="Enter OTP code" maxlength="6">
37
+ <button class="btn btn-success" onclick="verifyOtp()">Verify</button>
38
+ </div>
39
+ <p id="otp-status" class="mt-2 text-muted"></p>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- Success -->
44
+ <div id="success-section" class="card success-section">
45
+ <div class="card-body">
46
+ <h5 class="card-title text-success">Authenticated</h5>
47
+ <p>Kia Connect is authenticated. Your vehicle will be loaded automatically within a few seconds.</p>
48
+ </div>
49
+ </div>
50
+
51
+ <script>
52
+ let currentConfig = {};
53
+ let pluginAlias = 'KiaConnect';
54
+ let pendingOtpCredentials = null;
55
+
56
+ async function loadConfig() {
57
+ const pluginConfig = await homebridge.getPluginConfig();
58
+ const schema = await homebridge.getPluginConfigSchema();
59
+ pluginAlias = schema.pluginAlias || pluginAlias;
60
+ currentConfig = (pluginConfig && pluginConfig[0]) || {};
61
+ document.getElementById('username').value = currentConfig.username || '';
62
+ document.getElementById('password').value = currentConfig.password || '';
63
+ }
64
+
65
+ async function persistCredentials(username, password) {
66
+ const nextConfig = {
67
+ platform: currentConfig.platform || pluginAlias,
68
+ name: currentConfig.name || 'Kia Connect',
69
+ ...currentConfig,
70
+ username,
71
+ password,
72
+ };
73
+
74
+ await homebridge.updatePluginConfig([nextConfig]);
75
+ await homebridge.savePluginConfig();
76
+ currentConfig = nextConfig;
77
+ }
78
+
79
+ function setCredentialInputsDisabled(disabled) {
80
+ document.getElementById('username').disabled = disabled;
81
+ document.getElementById('password').disabled = disabled;
82
+ }
83
+
84
+ (async function checkStatus() {
85
+ try {
86
+ await loadConfig();
87
+ const res = await homebridge.request('/auth/status');
88
+ if (res.authenticated) {
89
+ document.getElementById('status-message').textContent = 'Authenticated';
90
+ document.getElementById('status-message').className = 'text-success';
91
+ document.getElementById('success-section').style.display = 'block';
92
+ } else {
93
+ document.getElementById('status-message').textContent = 'Not authenticated';
94
+ document.getElementById('login-btn').style.display = 'inline-block';
95
+ }
96
+ } catch (e) {
97
+ document.getElementById('status-message').textContent = 'Not authenticated';
98
+ document.getElementById('login-btn').style.display = 'inline-block';
99
+ }
100
+ })();
101
+
102
+ async function doLogin() {
103
+ const btn = document.getElementById('login-btn');
104
+ const username = document.getElementById('username').value.trim();
105
+ const password = document.getElementById('password').value;
106
+ if (!username || !password) {
107
+ homebridge.toast.error('Enter your Kia Connect email and password first.');
108
+ return;
109
+ }
110
+
111
+ btn.disabled = true;
112
+ btn.textContent = 'Logging in...';
113
+
114
+ try {
115
+ const res = await homebridge.request('/auth/login', {
116
+ username,
117
+ password,
118
+ });
119
+ if (res.success) {
120
+ pendingOtpCredentials = null;
121
+ await persistCredentials(username, password);
122
+ document.getElementById('status-message').textContent = 'Authenticated!';
123
+ document.getElementById('status-message').className = 'text-success';
124
+ btn.style.display = 'none';
125
+ document.getElementById('success-section').style.display = 'block';
126
+ homebridge.toast.success('Login successful!');
127
+ } else if (res.otpRequired) {
128
+ pendingOtpCredentials = { username, password };
129
+ setCredentialInputsDisabled(true);
130
+ btn.style.display = 'none';
131
+ document.getElementById('status-message').textContent = 'OTP verification required';
132
+ document.getElementById('otp-section').style.display = 'block';
133
+ let info = 'A one-time password is required.';
134
+ if (res.email) info += ' Email: ' + res.email;
135
+ if (res.sms) info += ' SMS: ' + res.sms;
136
+ document.getElementById('otp-info').textContent = info;
137
+ } else {
138
+ pendingOtpCredentials = null;
139
+ setCredentialInputsDisabled(false);
140
+ homebridge.toast.error('Login failed. Check your credentials.');
141
+ btn.disabled = false;
142
+ btn.textContent = 'Login';
143
+ }
144
+ } catch (e) {
145
+ pendingOtpCredentials = null;
146
+ setCredentialInputsDisabled(false);
147
+ homebridge.toast.error('Login failed: ' + (e.message || e));
148
+ btn.disabled = false;
149
+ btn.textContent = 'Login';
150
+ }
151
+ }
152
+
153
+ async function sendOtp(method) {
154
+ const statusEl = document.getElementById('otp-status');
155
+ statusEl.textContent = 'Sending OTP via ' + method + '...';
156
+ try {
157
+ await homebridge.request('/auth/send-otp', { method });
158
+ statusEl.textContent = 'OTP sent via ' + method + '. Check your ' + (method === 'EMAIL' ? 'email' : 'phone') + '.';
159
+ statusEl.className = 'mt-2 text-success';
160
+ homebridge.toast.success('OTP sent!');
161
+ } catch (e) {
162
+ statusEl.textContent = 'Failed to send OTP: ' + (e.message || e);
163
+ statusEl.className = 'mt-2 text-danger';
164
+ }
165
+ }
166
+
167
+ async function verifyOtp() {
168
+ const code = document.getElementById('otp-code').value.trim();
169
+ if (!code) {
170
+ homebridge.toast.error('Please enter the OTP code');
171
+ return;
172
+ }
173
+
174
+ const statusEl = document.getElementById('otp-status');
175
+ statusEl.textContent = 'Verifying...';
176
+
177
+ try {
178
+ const res = await homebridge.request('/auth/verify-otp', { code });
179
+ if (res.success) {
180
+ if (pendingOtpCredentials) {
181
+ await persistCredentials(pendingOtpCredentials.username, pendingOtpCredentials.password);
182
+ }
183
+ pendingOtpCredentials = null;
184
+ document.getElementById('otp-section').style.display = 'none';
185
+ document.getElementById('success-section').style.display = 'block';
186
+ document.getElementById('status-message').textContent = 'Authenticated!';
187
+ document.getElementById('status-message').className = 'text-success';
188
+ homebridge.toast.success('Authentication successful! Restart Homebridge to load your vehicle.');
189
+ }
190
+ } catch (e) {
191
+ statusEl.textContent = 'Verification failed: ' + (e.message || e);
192
+ statusEl.className = 'mt-2 text-danger';
193
+ homebridge.toast.error('OTP verification failed');
194
+ }
195
+ }
196
+ </script>
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,126 @@
1
+ import { HomebridgePluginUiServer, RequestError } from '@homebridge/plugin-ui-utils/dist/server.js';
2
+ import { KiaAuthManager } from '../src/kia/auth.js';
3
+ import { KiaApiClient } from '../src/kia/client.js';
4
+ import { AuthenticationError } from '../src/kia/types.js';
5
+ import { readSavedCredentials } from './config.js';
6
+ function createConsoleLogger(prefix) {
7
+ return {
8
+ info: (...args) => console.log(`[${prefix}]`, ...args),
9
+ warn: (...args) => console.warn(`[${prefix}]`, ...args),
10
+ error: (...args) => console.error(`[${prefix}]`, ...args),
11
+ debug: (...args) => console.debug(`[${prefix}]`, ...args),
12
+ log: (...args) => console.log(`[${prefix}]`, ...args),
13
+ success: (...args) => console.log(`[${prefix}]`, ...args),
14
+ prefix,
15
+ };
16
+ }
17
+ class KiaConnectUiServer extends HomebridgePluginUiServer {
18
+ authManager;
19
+ apiClient;
20
+ otpState;
21
+ pendingCredentials;
22
+ constructor() {
23
+ super();
24
+ this.onRequest('/auth/status', this.handleAuthStatus.bind(this));
25
+ this.onRequest('/auth/login', this.handleLogin.bind(this));
26
+ this.onRequest('/auth/send-otp', this.handleSendOtp.bind(this));
27
+ this.onRequest('/auth/verify-otp', this.handleVerifyOtp.bind(this));
28
+ this.ready();
29
+ }
30
+ getClients() {
31
+ if (!this.authManager) {
32
+ const storagePath = this.homebridgeStoragePath ?? '/tmp';
33
+ const log = createConsoleLogger('KiaConnect');
34
+ const savedCredentials = readSavedCredentials(this.homebridgeConfigPath);
35
+ this.authManager = new KiaAuthManager(storagePath, log);
36
+ const username = this.pendingCredentials?.username ?? savedCredentials?.username ?? '';
37
+ const password = this.pendingCredentials?.password ?? savedCredentials?.password ?? '';
38
+ this.apiClient = new KiaApiClient(this.authManager, log, username, password);
39
+ }
40
+ return { authManager: this.authManager, apiClient: this.apiClient };
41
+ }
42
+ async handleAuthStatus() {
43
+ const { authManager, apiClient } = this.getClients();
44
+ authManager.reloadToken();
45
+ const isValid = authManager.isTokenValid();
46
+ if (!isValid) {
47
+ return { authenticated: false };
48
+ }
49
+ try {
50
+ await apiClient.getVehicles();
51
+ return { authenticated: true };
52
+ }
53
+ catch (e) {
54
+ if (e instanceof AuthenticationError) {
55
+ authManager.clearToken();
56
+ }
57
+ return { authenticated: false };
58
+ }
59
+ }
60
+ async handleLogin(payload) {
61
+ // Re-create clients with credentials if provided
62
+ if (payload?.username && payload?.password) {
63
+ this.authManager = undefined;
64
+ this.apiClient = undefined;
65
+ this.pendingCredentials = { username: payload.username, password: payload.password };
66
+ }
67
+ const { apiClient } = this.getClients();
68
+ try {
69
+ const result = await apiClient.login();
70
+ if (result.success) {
71
+ return { success: true };
72
+ }
73
+ if (result.otpRequired && result.otpState) {
74
+ this.otpState = result.otpState;
75
+ return {
76
+ success: false,
77
+ otpRequired: true,
78
+ email: result.otpState.email,
79
+ sms: result.otpState.sms,
80
+ };
81
+ }
82
+ return { success: false };
83
+ }
84
+ catch (e) {
85
+ const message = e instanceof Error ? e.message : 'Login failed';
86
+ throw new RequestError(message, { status: 500 });
87
+ }
88
+ }
89
+ async handleSendOtp(payload) {
90
+ if (!this.otpState) {
91
+ throw new RequestError('No OTP session in progress. Login first.', { status: 400 });
92
+ }
93
+ const { apiClient } = this.getClients();
94
+ try {
95
+ await apiClient.sendOtp(this.otpState, payload.method ?? 'EMAIL');
96
+ return { success: true };
97
+ }
98
+ catch (e) {
99
+ const message = e instanceof Error ? e.message : 'Failed to send OTP';
100
+ throw new RequestError(message, { status: 500 });
101
+ }
102
+ }
103
+ async handleVerifyOtp(payload) {
104
+ if (!this.otpState) {
105
+ throw new RequestError('No OTP session in progress. Login first.', { status: 400 });
106
+ }
107
+ const { apiClient } = this.getClients();
108
+ try {
109
+ const success = await apiClient.verifyOtp(this.otpState, payload.code);
110
+ if (success) {
111
+ this.otpState = undefined;
112
+ return { success: true };
113
+ }
114
+ throw new RequestError('OTP verification failed', { status: 400 });
115
+ }
116
+ catch (e) {
117
+ if (e instanceof RequestError) {
118
+ throw e;
119
+ }
120
+ const message = e instanceof Error ? e.message : 'OTP verification failed';
121
+ throw new RequestError(message, { status: 500 });
122
+ }
123
+ }
124
+ }
125
+ (() => new KiaConnectUiServer())();
126
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../homebridge-ui/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { API } from 'homebridge';
2
+ declare const _default: (api: API) => void;
3
+ export default _default;
@@ -0,0 +1,6 @@
1
+ import { KiaConnectPlatform } from './platform.js';
2
+ import { PLATFORM_NAME } from './settings.js';
3
+ export default (api) => {
4
+ api.registerPlatform(PLATFORM_NAME, KiaConnectPlatform);
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,eAAe,CAAC,GAAQ,EAAE,EAAE;IAC1B,GAAG,CAAC,gBAAgB,CAAC,aAAa,EAAE,kBAAkB,CAAC,CAAC;AAC1D,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { Logger } from 'homebridge';
2
+ import type { PersistedToken } from './types.js';
3
+ export declare class KiaAuthManager {
4
+ private readonly storagePath;
5
+ private readonly log;
6
+ private readonly tokenPath;
7
+ private token;
8
+ private cachedDeviceId;
9
+ constructor(storagePath: string, log: Logger);
10
+ loadToken(): PersistedToken | null;
11
+ saveToken(token: PersistedToken): void;
12
+ isTokenValid(): boolean;
13
+ getAccessToken(): string | null;
14
+ getRefreshToken(): string | null;
15
+ getDeviceId(): string;
16
+ getVehicleKey(): string | null;
17
+ reloadToken(): void;
18
+ updateToken(accessToken: string, refreshToken: string, deviceId?: string): void;
19
+ setVehicleKey(vehicleKey: string): void;
20
+ clearToken(): void;
21
+ }
@@ -0,0 +1,106 @@
1
+ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { generateDeviceId } from './crypto.js';
4
+ import { SESSION_LIFETIME_MS } from '../settings.js';
5
+ export class KiaAuthManager {
6
+ storagePath;
7
+ log;
8
+ tokenPath;
9
+ token = null;
10
+ cachedDeviceId = null;
11
+ constructor(storagePath, log) {
12
+ this.storagePath = storagePath;
13
+ this.log = log;
14
+ this.tokenPath = join(storagePath, 'kia-connect-token.json');
15
+ this.token = this.loadToken();
16
+ }
17
+ loadToken() {
18
+ try {
19
+ if (!existsSync(this.tokenPath)) {
20
+ return null;
21
+ }
22
+ const data = readFileSync(this.tokenPath, 'utf-8');
23
+ const parsed = JSON.parse(data);
24
+ if (parsed.accessToken && parsed.refreshToken && parsed.deviceId) {
25
+ this.log.debug('Loaded persisted token');
26
+ return parsed;
27
+ }
28
+ return null;
29
+ }
30
+ catch (e) {
31
+ this.log.warn('Could not load persisted token:', e);
32
+ return null;
33
+ }
34
+ }
35
+ saveToken(token) {
36
+ try {
37
+ if (!existsSync(this.storagePath)) {
38
+ mkdirSync(this.storagePath, { recursive: true });
39
+ }
40
+ const tmpPath = this.tokenPath + '.tmp';
41
+ writeFileSync(tmpPath, JSON.stringify(token, null, 2), 'utf-8');
42
+ renameSync(tmpPath, this.tokenPath);
43
+ this.token = token;
44
+ this.log.debug('Token persisted to disk');
45
+ }
46
+ catch (e) {
47
+ this.log.error('Failed to save token:', e);
48
+ }
49
+ }
50
+ isTokenValid() {
51
+ return this.token !== null && Date.now() < this.token.validUntil;
52
+ }
53
+ getAccessToken() {
54
+ return this.token?.accessToken ?? null;
55
+ }
56
+ getRefreshToken() {
57
+ return this.token?.refreshToken ?? null;
58
+ }
59
+ getDeviceId() {
60
+ if (this.token?.deviceId) {
61
+ return this.token.deviceId;
62
+ }
63
+ // Cache in memory so all requests in a session use the same device ID
64
+ // (critical for OTP flow where multiple requests must share identity)
65
+ if (this.cachedDeviceId) {
66
+ return this.cachedDeviceId;
67
+ }
68
+ this.cachedDeviceId = generateDeviceId();
69
+ this.log.debug('Generated new device ID:', this.cachedDeviceId);
70
+ return this.cachedDeviceId;
71
+ }
72
+ getVehicleKey() {
73
+ return this.token?.vehicleKey ?? null;
74
+ }
75
+ reloadToken() {
76
+ this.token = this.loadToken();
77
+ }
78
+ updateToken(accessToken, refreshToken, deviceId) {
79
+ const token = {
80
+ accessToken,
81
+ refreshToken,
82
+ deviceId: deviceId ?? this.getDeviceId(),
83
+ vehicleKey: this.token?.vehicleKey,
84
+ validUntil: Date.now() + SESSION_LIFETIME_MS,
85
+ };
86
+ this.saveToken(token);
87
+ }
88
+ setVehicleKey(vehicleKey) {
89
+ if (this.token) {
90
+ this.token.vehicleKey = vehicleKey;
91
+ this.saveToken(this.token);
92
+ }
93
+ }
94
+ clearToken() {
95
+ this.token = null;
96
+ try {
97
+ if (existsSync(this.tokenPath)) {
98
+ unlinkSync(this.tokenPath);
99
+ }
100
+ }
101
+ catch (e) {
102
+ this.log.warn('Failed to clear token file:', e);
103
+ }
104
+ }
105
+ }
106
+ //# sourceMappingURL=auth.js.map