n8n-nodes-vw-weconnect 0.1.0 → 0.1.1

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.
@@ -141,42 +141,148 @@ class VwWeConnect {
141
141
  }
142
142
  }
143
143
  exports.VwWeConnect = VwWeConnect;
144
+ // VW OAuth2 Configuration
145
+ const VW_CLIENT_ID = '9496332b-ea03-4091-a224-8c746b885068@apps_vw-dilab_com';
146
+ const VW_SCOPE = 'openid profile mbb';
147
+ const VW_REDIRECT_URI = 'weconnect://authenticated';
144
148
  async function vwLogin(context, email, password) {
145
- // VW We Connect uses a complex OAuth2 flow
146
- // Step 1: Get initial login page and CSRF token
147
- const baseUrl = 'https://www.portal.volkswagen-we.com';
148
- const loginUrl = `${baseUrl}/portal/en_GB/web/guest/home`;
149
149
  try {
150
- // Initial request to get cookies and CSRF
151
- await context.helpers.httpRequest({
150
+ // Step 1: Get authorization page and extract form data
151
+ const authorizeUrl = 'https://identity.vwgroup.io/oidc/v1/authorize';
152
+ const authorizeParams = new URLSearchParams({
153
+ client_id: VW_CLIENT_ID,
154
+ scope: VW_SCOPE,
155
+ response_type: 'code',
156
+ redirect_uri: VW_REDIRECT_URI,
157
+ nonce: generateNonce(),
158
+ state: generateNonce(),
159
+ });
160
+ const authorizeResponse = await context.helpers.httpRequest({
152
161
  method: 'GET',
153
- url: loginUrl,
162
+ url: `${authorizeUrl}?${authorizeParams.toString()}`,
154
163
  headers: {
155
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
164
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
156
165
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
157
166
  },
167
+ returnFullResponse: true,
168
+ ignoreHttpStatusErrors: true,
158
169
  });
159
- // Step 2: Perform login
160
- const loginPayload = {
161
- email: email,
162
- password: password,
163
- };
164
- await context.helpers.httpRequest({
170
+ // Extract CSRF token and relay state from the response
171
+ const htmlContent = authorizeResponse.body;
172
+ const csrfMatch = htmlContent.match(/name="_csrf"\s+value="([^"]+)"/);
173
+ const relayStateMatch = htmlContent.match(/name="relayState"\s+value="([^"]+)"/);
174
+ const hmacMatch = htmlContent.match(/name="hmac"\s+value="([^"]+)"/);
175
+ if (!csrfMatch) {
176
+ throw new Error('Could not extract CSRF token from login page');
177
+ }
178
+ const csrf = csrfMatch[1];
179
+ const relayState = relayStateMatch ? relayStateMatch[1] : '';
180
+ const hmac = hmacMatch ? hmacMatch[1] : '';
181
+ // Step 2: Submit email
182
+ const identifierResponse = await context.helpers.httpRequest({
183
+ method: 'POST',
184
+ url: 'https://identity.vwgroup.io/signin-service/v1/signin/login/identifier',
185
+ headers: {
186
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
187
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
188
+ 'Content-Type': 'application/x-www-form-urlencoded',
189
+ },
190
+ body: new URLSearchParams({
191
+ _csrf: csrf,
192
+ relayState: relayState,
193
+ hmac: hmac,
194
+ email: email,
195
+ }).toString(),
196
+ returnFullResponse: true,
197
+ ignoreHttpStatusErrors: true,
198
+ });
199
+ // Extract new CSRF for password submission
200
+ const identifierHtml = identifierResponse.body;
201
+ const csrf2Match = identifierHtml.match(/name="_csrf"\s+value="([^"]+)"/);
202
+ const relayState2Match = identifierHtml.match(/name="relayState"\s+value="([^"]+)"/);
203
+ const hmac2Match = identifierHtml.match(/name="hmac"\s+value="([^"]+)"/);
204
+ const csrf2 = csrf2Match ? csrf2Match[1] : csrf;
205
+ const relayState2 = relayState2Match ? relayState2Match[1] : relayState;
206
+ const hmac2 = hmac2Match ? hmac2Match[1] : hmac;
207
+ // Step 3: Submit password
208
+ const authResponse = await context.helpers.httpRequest({
209
+ method: 'POST',
210
+ url: 'https://identity.vwgroup.io/signin-service/v1/signin/login/authenticate',
211
+ headers: {
212
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
213
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
214
+ 'Content-Type': 'application/x-www-form-urlencoded',
215
+ },
216
+ body: new URLSearchParams({
217
+ _csrf: csrf2,
218
+ relayState: relayState2,
219
+ hmac: hmac2,
220
+ email: email,
221
+ password: password,
222
+ }).toString(),
223
+ returnFullResponse: true,
224
+ ignoreHttpStatusErrors: true,
225
+ });
226
+ // Get the redirect URL which contains the authorization code
227
+ const headers = authResponse.headers;
228
+ let redirectUrl = headers.location;
229
+ if (!redirectUrl) {
230
+ // Check if we got a redirect in the response
231
+ const authHtml = authResponse.body;
232
+ const redirectMatch = authHtml.match(/URL=([^"]+)"/i);
233
+ if (redirectMatch) {
234
+ redirectUrl = redirectMatch[1];
235
+ }
236
+ }
237
+ // Follow redirects to get the auth code
238
+ let authCode = '';
239
+ let maxRedirects = 10;
240
+ while (maxRedirects > 0 && redirectUrl && !authCode) {
241
+ if (redirectUrl.includes('code=')) {
242
+ const codeMatch = redirectUrl.match(/code=([^&]+)/);
243
+ if (codeMatch) {
244
+ authCode = codeMatch[1];
245
+ break;
246
+ }
247
+ }
248
+ const followResponse = await context.helpers.httpRequest({
249
+ method: 'GET',
250
+ url: redirectUrl.startsWith('http') ? redirectUrl : `https://identity.vwgroup.io${redirectUrl}`,
251
+ headers: {
252
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
253
+ },
254
+ returnFullResponse: true,
255
+ ignoreHttpStatusErrors: true,
256
+ });
257
+ const followHeaders = followResponse.headers;
258
+ redirectUrl = followHeaders.location;
259
+ maxRedirects--;
260
+ }
261
+ if (!authCode) {
262
+ throw new Error('Could not obtain authorization code. Please check your credentials.');
263
+ }
264
+ // Step 4: Exchange auth code for tokens
265
+ const tokenResponse = await context.helpers.httpRequest({
165
266
  method: 'POST',
166
- url: `${baseUrl}/portal/web/guest/home/-/csrftokenhandling/get-login-url`,
267
+ url: 'https://tokenrefreshservice.apps.emea.vwapps.io/exchangeAuthCode',
167
268
  headers: {
168
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
269
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
169
270
  'Accept': 'application/json',
170
271
  'Content-Type': 'application/json',
272
+ 'X-Client-Id': VW_CLIENT_ID,
273
+ },
274
+ body: {
275
+ auth_code: authCode,
276
+ id_token: '',
277
+ brand: 'vw',
171
278
  },
172
- body: loginPayload,
173
279
  });
174
- // For now, return a mock session - the actual implementation
175
- // requires handling VW's complex OAuth2 flow with multiple redirects
176
280
  return {
177
- accessToken: 'session_token',
178
- refreshToken: 'refresh_token',
179
- csrf: 'csrf_token',
281
+ accessToken: tokenResponse.access_token,
282
+ refreshToken: tokenResponse.refresh_token,
283
+ idToken: tokenResponse.id_token || '',
284
+ homeRegion: 'https://mal-1a.prd.ece.vwg-connect.com',
285
+ userId: '',
180
286
  };
181
287
  }
182
288
  catch (error) {
@@ -186,17 +292,46 @@ async function vwLogin(context, email, password) {
186
292
  });
187
293
  }
188
294
  }
295
+ function generateNonce() {
296
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
297
+ let result = '';
298
+ for (let i = 0; i < 32; i++) {
299
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
300
+ }
301
+ return result;
302
+ }
303
+ async function getHomeRegion(context, session, vin) {
304
+ try {
305
+ const response = await context.helpers.httpRequest({
306
+ method: 'GET',
307
+ url: `https://mal-1a.prd.ece.vwg-connect.com/api/cs/vds/v1/vehicles/${vin}/homeRegion`,
308
+ headers: {
309
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
310
+ 'Accept': 'application/json',
311
+ 'Authorization': `Bearer ${session.accessToken}`,
312
+ },
313
+ });
314
+ const homeRegion = response.homeRegion;
315
+ if (homeRegion && homeRegion.baseUri) {
316
+ const baseUri = homeRegion.baseUri;
317
+ return baseUri.content || session.homeRegion;
318
+ }
319
+ return session.homeRegion;
320
+ }
321
+ catch {
322
+ return session.homeRegion;
323
+ }
324
+ }
189
325
  async function getPosition(context, session, vin) {
190
- const baseUrl = 'https://www.portal.volkswagen-we.com';
191
326
  try {
327
+ // Try the new CARIAD API first
192
328
  const response = await context.helpers.httpRequest({
193
329
  method: 'GET',
194
- url: `${baseUrl}/portal/delegate/dashboard/${vin}/-/position/get-position`,
330
+ url: `https://emea.bff.cariad.digital/vehicle/v1/vehicles/${vin}/parkingposition`,
195
331
  headers: {
196
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
332
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
197
333
  'Accept': 'application/json',
198
334
  'Authorization': `Bearer ${session.accessToken}`,
199
- 'X-CSRF-Token': session.csrf,
200
335
  },
201
336
  });
202
337
  return {
@@ -206,24 +341,44 @@ async function getPosition(context, session, vin) {
206
341
  timestamp: new Date().toISOString(),
207
342
  };
208
343
  }
209
- catch (error) {
210
- throw new n8n_workflow_1.NodeApiError(context.getNode(), {
211
- message: 'Failed to get vehicle position',
212
- description: error.message,
213
- });
344
+ catch {
345
+ // Fallback to older API
346
+ try {
347
+ const homeRegion = await getHomeRegion(context, session, vin);
348
+ const response = await context.helpers.httpRequest({
349
+ method: 'GET',
350
+ url: `${homeRegion}/api/bs/cf/v1/VW/DE/vehicles/${vin}/position`,
351
+ headers: {
352
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
353
+ 'Accept': 'application/json',
354
+ 'Authorization': `Bearer ${session.accessToken}`,
355
+ },
356
+ });
357
+ return {
358
+ operation: 'getPosition',
359
+ vin: vin,
360
+ position: response,
361
+ timestamp: new Date().toISOString(),
362
+ };
363
+ }
364
+ catch (error) {
365
+ throw new n8n_workflow_1.NodeApiError(context.getNode(), {
366
+ message: 'Failed to get vehicle position',
367
+ description: error.message,
368
+ });
369
+ }
214
370
  }
215
371
  }
216
372
  async function getVehicleStatus(context, session, vin) {
217
- const baseUrl = 'https://www.portal.volkswagen-we.com';
218
373
  try {
374
+ // Try the new CARIAD API first
219
375
  const response = await context.helpers.httpRequest({
220
376
  method: 'GET',
221
- url: `${baseUrl}/portal/delegate/dashboard/${vin}/-/vsr/get-vsr`,
377
+ url: `https://emea.bff.cariad.digital/vehicle/v1/vehicles/${vin}/selectivestatus?jobs=all`,
222
378
  headers: {
223
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
379
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
224
380
  'Accept': 'application/json',
225
381
  'Authorization': `Bearer ${session.accessToken}`,
226
- 'X-CSRF-Token': session.csrf,
227
382
  },
228
383
  });
229
384
  return {
@@ -233,24 +388,44 @@ async function getVehicleStatus(context, session, vin) {
233
388
  timestamp: new Date().toISOString(),
234
389
  };
235
390
  }
236
- catch (error) {
237
- throw new n8n_workflow_1.NodeApiError(context.getNode(), {
238
- message: 'Failed to get vehicle status',
239
- description: error.message,
240
- });
391
+ catch {
392
+ // Fallback to older API
393
+ try {
394
+ const homeRegion = await getHomeRegion(context, session, vin);
395
+ const response = await context.helpers.httpRequest({
396
+ method: 'GET',
397
+ url: `${homeRegion}/api/bs/vsr/v1/VW/DE/vehicles/${vin}/status`,
398
+ headers: {
399
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
400
+ 'Accept': 'application/json',
401
+ 'Authorization': `Bearer ${session.accessToken}`,
402
+ },
403
+ });
404
+ return {
405
+ operation: 'getVehicleStatus',
406
+ vin: vin,
407
+ status: response,
408
+ timestamp: new Date().toISOString(),
409
+ };
410
+ }
411
+ catch (error) {
412
+ throw new n8n_workflow_1.NodeApiError(context.getNode(), {
413
+ message: 'Failed to get vehicle status',
414
+ description: error.message,
415
+ });
416
+ }
241
417
  }
242
418
  }
243
419
  async function getHeaterStatus(context, session, vin) {
244
- const baseUrl = 'https://www.portal.volkswagen-we.com';
245
420
  try {
421
+ const homeRegion = await getHomeRegion(context, session, vin);
246
422
  const response = await context.helpers.httpRequest({
247
423
  method: 'GET',
248
- url: `${baseUrl}/portal/delegate/dashboard/${vin}/-/rah/get-status`,
424
+ url: `${homeRegion}/api/bs/climatisation/v1/VW/DE/vehicles/${vin}/climater`,
249
425
  headers: {
250
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
426
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
251
427
  'Accept': 'application/json',
252
428
  'Authorization': `Bearer ${session.accessToken}`,
253
- 'X-CSRF-Token': session.csrf,
254
429
  },
255
430
  });
256
431
  return {
@@ -268,26 +443,70 @@ async function getHeaterStatus(context, session, vin) {
268
443
  }
269
444
  }
270
445
  async function controlHeater(context, session, vin, spin, action, duration) {
271
- const baseUrl = 'https://www.portal.volkswagen-we.com';
272
- const endpoint = action === 'start' ? 'start' : 'stop';
273
446
  try {
274
- const body = {
275
- spin: spin,
447
+ const homeRegion = await getHomeRegion(context, session, vin);
448
+ // First, get a security token for the action
449
+ const secTokenResponse = await context.helpers.httpRequest({
450
+ method: 'GET',
451
+ url: `${homeRegion}/api/rolesrights/authorization/v2/vehicles/${vin}/services/rclima_v1/operations/P_START_CLIMA_AU/security-pin-auth-requested`,
452
+ headers: {
453
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
454
+ 'Accept': 'application/json',
455
+ 'Authorization': `Bearer ${session.accessToken}`,
456
+ },
457
+ });
458
+ const securityPinAuthInfo = secTokenResponse.securityPinAuthInfo;
459
+ let secToken = '';
460
+ if (securityPinAuthInfo && securityPinAuthInfo.securityToken) {
461
+ // Complete S-PIN authentication
462
+ const spinAuthResponse = await context.helpers.httpRequest({
463
+ method: 'POST',
464
+ url: `${homeRegion}/api/rolesrights/authorization/v2/security-pin-auth-completed`,
465
+ headers: {
466
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
467
+ 'Accept': 'application/json',
468
+ 'Content-Type': 'application/json',
469
+ 'Authorization': `Bearer ${session.accessToken}`,
470
+ },
471
+ body: {
472
+ securityPinAuthentication: {
473
+ securityPin: spin,
474
+ securityToken: securityPinAuthInfo.securityToken,
475
+ },
476
+ },
477
+ });
478
+ const authInfo = spinAuthResponse.securityPinAuthInfo;
479
+ secToken = (authInfo === null || authInfo === void 0 ? void 0 : authInfo.securityToken) || '';
480
+ }
481
+ // Build the action request
482
+ const actionName = action === 'start' ? 'startClimatisation' : 'stopClimatisation';
483
+ const actionBody = {
484
+ action: {
485
+ type: actionName,
486
+ },
276
487
  };
277
488
  if (action === 'start' && duration) {
278
- body.duration = duration;
489
+ actionBody.action.settings = {
490
+ targetTemperature: 2930, // ~20°C in deciKelvin
491
+ climatisationWithoutHVpower: true,
492
+ heaterSource: 'auxiliary',
493
+ runTime: duration,
494
+ };
495
+ }
496
+ const headers = {
497
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
498
+ 'Accept': 'application/json',
499
+ 'Content-Type': 'application/json',
500
+ 'Authorization': `Bearer ${session.accessToken}`,
501
+ };
502
+ if (secToken) {
503
+ headers['x-mbbSecToken'] = secToken;
279
504
  }
280
505
  const response = await context.helpers.httpRequest({
281
506
  method: 'POST',
282
- url: `${baseUrl}/portal/delegate/dashboard/${vin}/-/rah/${endpoint}`,
283
- headers: {
284
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
285
- 'Accept': 'application/json',
286
- 'Content-Type': 'application/json',
287
- 'Authorization': `Bearer ${session.accessToken}`,
288
- 'X-CSRF-Token': session.csrf,
289
- },
290
- body: body,
507
+ url: `${homeRegion}/api/bs/climatisation/v1/VW/DE/vehicles/${vin}/climater/actions`,
508
+ headers: headers,
509
+ body: actionBody,
291
510
  });
292
511
  return {
293
512
  operation: action === 'start' ? 'startHeater' : 'stopHeater',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-vw-weconnect",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "n8n community node for VW We Connect - Control your Volkswagen T6.1 and other VW vehicles",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",