tesla-fleet-api 0.3.0 → 0.4.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.
Files changed (192) hide show
  1. package/dist/_virtual/_rolldown/runtime.cjs +22 -0
  2. package/dist/_virtual/_rolldown/runtime.mjs +12 -0
  3. package/dist/charging.cjs +53 -0
  4. package/dist/charging.d.cts +31 -0
  5. package/dist/charging.d.mts +30 -0
  6. package/dist/charging.mjs +53 -0
  7. package/dist/commands.cjs +802 -0
  8. package/dist/commands.d.cts +350 -0
  9. package/dist/commands.d.mts +349 -0
  10. package/dist/commands.mjs +800 -0
  11. package/dist/const.cjs +300 -0
  12. package/dist/const.d.cts +287 -0
  13. package/dist/const.d.mts +287 -0
  14. package/dist/const.mjs +263 -0
  15. package/dist/energy.cjs +137 -0
  16. package/dist/energy.d.cts +95 -0
  17. package/{src/energy.ts → dist/energy.d.mts} +17 -140
  18. package/dist/energy.mjs +137 -0
  19. package/dist/energyspecific.cjs +99 -0
  20. package/dist/energyspecific.d.cts +78 -0
  21. package/dist/energyspecific.d.mts +77 -0
  22. package/dist/energyspecific.mjs +99 -0
  23. package/dist/exceptions.cjs +1487 -0
  24. package/dist/exceptions.d.cts +1142 -0
  25. package/dist/exceptions.d.mts +1142 -0
  26. package/dist/exceptions.mjs +1324 -0
  27. package/dist/index.cjs +226 -0
  28. package/dist/index.d.cts +14 -0
  29. package/dist/index.d.mts +14 -0
  30. package/dist/index.mjs +14 -0
  31. package/dist/partner.cjs +32 -0
  32. package/dist/partner.d.cts +24 -0
  33. package/dist/partner.d.mts +23 -0
  34. package/dist/partner.mjs +32 -0
  35. package/dist/router.cjs +0 -0
  36. package/dist/router.d.cts +1 -0
  37. package/dist/router.d.mts +1 -0
  38. package/dist/router.mjs +1 -0
  39. package/dist/signing/crypto.cjs +31 -0
  40. package/dist/signing/crypto.mjs +27 -0
  41. package/dist/signing/session.cjs +112 -0
  42. package/dist/signing/session.d.cts +69 -0
  43. package/dist/signing/session.d.mts +69 -0
  44. package/dist/signing/session.mjs +110 -0
  45. package/dist/tariff.cjs +292 -0
  46. package/dist/{tariff.d.ts → tariff.d.cts} +17 -17
  47. package/dist/tariff.d.mts +29 -0
  48. package/dist/tariff.mjs +291 -0
  49. package/dist/teslafleetapi.cjs +125 -0
  50. package/dist/teslafleetapi.d.cts +64 -0
  51. package/dist/teslafleetapi.d.mts +64 -0
  52. package/dist/teslafleetapi.mjs +125 -0
  53. package/dist/teslemetry.cjs +36 -0
  54. package/dist/teslemetry.d.cts +23 -0
  55. package/dist/teslemetry.d.mts +23 -0
  56. package/dist/teslemetry.mjs +36 -0
  57. package/dist/tessie.cjs +34 -0
  58. package/dist/tessie.d.cts +23 -0
  59. package/dist/tessie.d.mts +23 -0
  60. package/dist/tessie.mjs +34 -0
  61. package/dist/types/commands.d.cts +28 -0
  62. package/dist/types/commands.d.mts +28 -0
  63. package/dist/types/live_status.d.cts +30 -0
  64. package/dist/types/live_status.d.mts +30 -0
  65. package/dist/types/products.d.cts +58 -0
  66. package/dist/types/products.d.mts +58 -0
  67. package/dist/types/responses.d.cts +9 -0
  68. package/dist/types/responses.d.mts +9 -0
  69. package/dist/types/site_info.d.cts +154 -0
  70. package/dist/types/site_info.d.mts +154 -0
  71. package/dist/types/vehicle.d.cts +27 -0
  72. package/dist/types/vehicle.d.mts +27 -0
  73. package/dist/types/vehicle_data.d.cts +278 -0
  74. package/dist/types/vehicle_data.d.mts +278 -0
  75. package/dist/types.cjs +0 -0
  76. package/dist/types.d.cts +9 -0
  77. package/dist/types.d.mts +9 -0
  78. package/dist/types.mjs +1 -0
  79. package/dist/user.cjs +28 -0
  80. package/dist/user.d.cts +17 -0
  81. package/dist/user.d.mts +16 -0
  82. package/dist/user.mjs +28 -0
  83. package/dist/vehicle.cjs +804 -0
  84. package/dist/vehicle.d.cts +550 -0
  85. package/dist/vehicle.d.mts +549 -0
  86. package/dist/vehicle.mjs +804 -0
  87. package/dist/vehiclesigned.cjs +20 -0
  88. package/dist/vehiclesigned.d.cts +14 -0
  89. package/dist/{vehiclesigned.d.ts → vehiclesigned.d.mts} +3 -3
  90. package/dist/vehiclesigned.mjs +20 -0
  91. package/dist/vehiclespecific.cjs +604 -0
  92. package/dist/vehiclespecific.d.cts +433 -0
  93. package/dist/vehiclespecific.d.mts +432 -0
  94. package/dist/vehiclespecific.mjs +604 -0
  95. package/package.json +108 -35
  96. package/dist/charging.d.ts +0 -30
  97. package/dist/charging.js +0 -51
  98. package/dist/charging.js.map +0 -1
  99. package/dist/commands.d.ts +0 -352
  100. package/dist/commands.js +0 -875
  101. package/dist/commands.js.map +0 -1
  102. package/dist/energy.d.ts +0 -94
  103. package/dist/energy.js +0 -125
  104. package/dist/energy.js.map +0 -1
  105. package/dist/energyspecific.d.ts +0 -77
  106. package/dist/energyspecific.js +0 -97
  107. package/dist/energyspecific.js.map +0 -1
  108. package/dist/helpers.d.ts +0 -2
  109. package/dist/helpers.js +0 -10
  110. package/dist/helpers.js.map +0 -1
  111. package/dist/index.d.ts +0 -15
  112. package/dist/index.js +0 -15
  113. package/dist/index.js.map +0 -1
  114. package/dist/partner.d.ts +0 -23
  115. package/dist/partner.js +0 -31
  116. package/dist/partner.js.map +0 -1
  117. package/dist/signing/crypto.d.ts +0 -10
  118. package/dist/signing/crypto.js +0 -26
  119. package/dist/signing/crypto.js.map +0 -1
  120. package/dist/signing/errors.d.ts +0 -29
  121. package/dist/signing/errors.js +0 -40
  122. package/dist/signing/errors.js.map +0 -1
  123. package/dist/signing/session.d.ts +0 -69
  124. package/dist/signing/session.js +0 -102
  125. package/dist/signing/session.js.map +0 -1
  126. package/dist/tariff.js +0 -302
  127. package/dist/tariff.js.map +0 -1
  128. package/dist/teslafleetapi.d.ts +0 -58
  129. package/dist/teslafleetapi.js +0 -120
  130. package/dist/teslafleetapi.js.map +0 -1
  131. package/dist/teslemetry.d.ts +0 -29
  132. package/dist/teslemetry.js +0 -39
  133. package/dist/teslemetry.js.map +0 -1
  134. package/dist/tessie.d.ts +0 -23
  135. package/dist/tessie.js +0 -28
  136. package/dist/tessie.js.map +0 -1
  137. package/dist/types/commands.d.ts +0 -44
  138. package/dist/types/commands.js +0 -11
  139. package/dist/types/commands.js.map +0 -1
  140. package/dist/types/live_status.d.ts +0 -26
  141. package/dist/types/live_status.js +0 -2
  142. package/dist/types/live_status.js.map +0 -1
  143. package/dist/types/products.d.ts +0 -54
  144. package/dist/types/products.js +0 -2
  145. package/dist/types/products.js.map +0 -1
  146. package/dist/types/responses.d.ts +0 -5
  147. package/dist/types/responses.js +0 -2
  148. package/dist/types/responses.js.map +0 -1
  149. package/dist/types/site_info.d.ts +0 -150
  150. package/dist/types/site_info.js +0 -2
  151. package/dist/types/site_info.js.map +0 -1
  152. package/dist/types/vehicle.d.ts +0 -23
  153. package/dist/types/vehicle.js +0 -10
  154. package/dist/types/vehicle.js.map +0 -1
  155. package/dist/types/vehicle_data.d.ts +0 -274
  156. package/dist/types/vehicle_data.js +0 -2
  157. package/dist/types/vehicle_data.js.map +0 -1
  158. package/dist/user.d.ts +0 -16
  159. package/dist/user.js +0 -26
  160. package/dist/user.js.map +0 -1
  161. package/dist/vehicle.d.ts +0 -548
  162. package/dist/vehicle.js +0 -774
  163. package/dist/vehicle.js.map +0 -1
  164. package/dist/vehiclesigned.js +0 -19
  165. package/dist/vehiclesigned.js.map +0 -1
  166. package/dist/vehiclespecific.d.ts +0 -432
  167. package/dist/vehiclespecific.js +0 -601
  168. package/dist/vehiclespecific.js.map +0 -1
  169. package/src/charging.ts +0 -66
  170. package/src/commands.ts +0 -1134
  171. package/src/energyspecific.ts +0 -116
  172. package/src/helpers.ts +0 -11
  173. package/src/index.ts +0 -21
  174. package/src/partner.ts +0 -37
  175. package/src/signing/crypto.ts +0 -29
  176. package/src/signing/errors.ts +0 -50
  177. package/src/signing/session.ts +0 -132
  178. package/src/tariff.ts +0 -375
  179. package/src/teslafleetapi.ts +0 -182
  180. package/src/teslemetry.ts +0 -54
  181. package/src/tessie.ts +0 -42
  182. package/src/types/commands.ts +0 -62
  183. package/src/types/live_status.ts +0 -27
  184. package/src/types/products.ts +0 -61
  185. package/src/types/responses.ts +0 -1
  186. package/src/types/site_info.ts +0 -159
  187. package/src/types/vehicle.ts +0 -33
  188. package/src/types/vehicle_data.ts +0 -286
  189. package/src/user.ts +0 -35
  190. package/src/vehicle.ts +0 -886
  191. package/src/vehiclesigned.ts +0 -20
  192. package/src/vehiclespecific.ts +0 -818
@@ -0,0 +1,112 @@
1
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ let crypto = require("crypto");
3
+ crypto = require_runtime.__toESM(crypto, 1);
4
+ let _teslemetry_tesla_protocol_command_signatures = require("@teslemetry/tesla-protocol/command/signatures");
5
+ // src/signing/session.ts
6
+ /**
7
+ * Per-domain (VCSEC / Infotainment) signed-command session: tracks the
8
+ * anti-replay counter/epoch handed out by the vehicle's handshake reply and
9
+ * the HMAC keys derived from it.
10
+ *
11
+ * Deriving key material (`keysFor`) is split from committing it (`commit`)
12
+ * because the caller must authenticate a `SessionInfo` reply - by verifying
13
+ * its `session_info_tag` against the very keys derived here - *before*
14
+ * trusting anything in it, including the vehicle public key the shared key
15
+ * itself is derived from. See `Commands.validateAndUpdateSession`.
16
+ *
17
+ * A `Session` is not itself thread-safe; `withLock` serializes the
18
+ * message-build step (which mutates `counter`) so concurrent commands on the
19
+ * same domain cannot race on the same counter value.
20
+ */
21
+ var Session = class {
22
+ deriveSharedKey;
23
+ domain;
24
+ counter = 0;
25
+ epoch;
26
+ delta;
27
+ sharedKey;
28
+ hmacKey;
29
+ sessionInfoKey;
30
+ publicKey;
31
+ /** Clock time of the last *authenticated* SessionInfo committed for the current epoch, guarding against replay/rollback. */
32
+ lastAuthenticatedClockTime;
33
+ queue = Promise.resolve();
34
+ constructor(domain, deriveSharedKey) {
35
+ this.deriveSharedKey = deriveSharedKey;
36
+ this.domain = domain;
37
+ }
38
+ /** Whether the handshake has completed and commands can be signed. */
39
+ get ready() {
40
+ return this.epoch !== void 0 && this.hmacKey !== void 0 && this.delta !== void 0;
41
+ }
42
+ /**
43
+ * Derive the shared key and its two HMAC sub-keys for a candidate vehicle
44
+ * public key, without touching any session state. Pure and side-effect
45
+ * free so a caller can verify authenticity first and discard the result
46
+ * if verification fails.
47
+ */
48
+ keysFor(vehiclePublicKey) {
49
+ const sharedKey = this.deriveSharedKey(vehiclePublicKey);
50
+ return {
51
+ sharedKey,
52
+ hmacKey: crypto.default.createHmac("sha256", sharedKey).update("authenticated command").digest(),
53
+ sessionInfoKey: crypto.default.createHmac("sha256", sharedKey).update("session info").digest()
54
+ };
55
+ }
56
+ /**
57
+ * Commit an already-authenticated `SessionInfo` plus the `keysFor` result
58
+ * derived from its public key. Per the vehicle-command protocol, the
59
+ * anti-replay counter must never roll back within the same epoch, and a
60
+ * clock time that regresses within the same epoch marks the reply as
61
+ * stale/replayed - returns `false` (without mutating any state) in that
62
+ * case so the caller can reject it.
63
+ */
64
+ commit(sessionInfo, keys) {
65
+ const incomingEpoch = Buffer.from(sessionInfo.epoch);
66
+ const sameEpoch = this.epoch !== void 0 && this.epoch.equals(incomingEpoch);
67
+ if (sameEpoch && this.lastAuthenticatedClockTime !== void 0 && sessionInfo.clockTime < this.lastAuthenticatedClockTime) return false;
68
+ this.counter = sameEpoch ? Math.max(this.counter, sessionInfo.counter) : sessionInfo.counter;
69
+ this.epoch = incomingEpoch;
70
+ this.delta = Math.floor(Date.now() / 1e3) - sessionInfo.clockTime;
71
+ this.lastAuthenticatedClockTime = sessionInfo.clockTime;
72
+ this.publicKey = Buffer.from(sessionInfo.publicKey);
73
+ this.sharedKey = keys.sharedKey;
74
+ this.hmacKey = keys.hmacKey;
75
+ this.sessionInfoKey = keys.sessionInfoKey;
76
+ return true;
77
+ }
78
+ /**
79
+ * Advance the anti-replay counter and return fresh signature metadata for
80
+ * an HMAC-personalized command. The command expires 10 seconds from now,
81
+ * corrected for clock drift against the vehicle (`delta`).
82
+ */
83
+ hmacPersonalized() {
84
+ if (!this.ready || !this.epoch || this.delta === void 0) throw new Error("Session is not ready: perform the handshake first.");
85
+ this.counter++;
86
+ return _teslemetry_tesla_protocol_command_signatures.HMACPersonalizedSignatureData.create({
87
+ epoch: this.epoch,
88
+ counter: this.counter,
89
+ expiresAt: Math.floor(Date.now() / 1e3) - this.delta + 10
90
+ });
91
+ }
92
+ /**
93
+ * Run `fn` with exclusive access to this session, queuing behind any
94
+ * in-flight caller. Held only for as long as `fn` takes - callers should
95
+ * keep it scoped to the synchronous message-build step, not a full
96
+ * network round trip.
97
+ */
98
+ async withLock(fn) {
99
+ const previous = this.queue;
100
+ let release;
101
+ this.queue = new Promise((resolve) => {
102
+ release = resolve;
103
+ });
104
+ await previous.catch(() => void 0);
105
+ try {
106
+ return await fn();
107
+ } finally {
108
+ release();
109
+ }
110
+ }
111
+ };
112
+ exports.Session = Session;
@@ -0,0 +1,69 @@
1
+ import { Domain } from "@teslemetry/tesla-protocol/command/universal_message";
2
+ import { HMACPersonalizedSignatureData, SessionInfo } from "@teslemetry/tesla-protocol/command/signatures";
3
+ /** Key material derived (but not yet trusted/committed) for a candidate vehicle public key. */
4
+ export interface SessionKeys {
5
+ sharedKey: Buffer;
6
+ hmacKey: Buffer;
7
+ sessionInfoKey: Buffer;
8
+ }
9
+ /**
10
+ * Per-domain (VCSEC / Infotainment) signed-command session: tracks the
11
+ * anti-replay counter/epoch handed out by the vehicle's handshake reply and
12
+ * the HMAC keys derived from it.
13
+ *
14
+ * Deriving key material (`keysFor`) is split from committing it (`commit`)
15
+ * because the caller must authenticate a `SessionInfo` reply - by verifying
16
+ * its `session_info_tag` against the very keys derived here - *before*
17
+ * trusting anything in it, including the vehicle public key the shared key
18
+ * itself is derived from. See `Commands.validateAndUpdateSession`.
19
+ *
20
+ * A `Session` is not itself thread-safe; `withLock` serializes the
21
+ * message-build step (which mutates `counter`) so concurrent commands on the
22
+ * same domain cannot race on the same counter value.
23
+ */
24
+ export declare class Session {
25
+ private readonly deriveSharedKey;
26
+ readonly domain: Domain;
27
+ counter: number;
28
+ epoch?: Buffer;
29
+ delta?: number;
30
+ sharedKey?: Buffer;
31
+ hmacKey?: Buffer;
32
+ sessionInfoKey?: Buffer;
33
+ publicKey?: Buffer;
34
+ /** Clock time of the last *authenticated* SessionInfo committed for the current epoch, guarding against replay/rollback. */
35
+ private lastAuthenticatedClockTime?;
36
+ private queue;
37
+ constructor(domain: Domain, deriveSharedKey: (vehiclePublicKey: Buffer) => Buffer);
38
+ /** Whether the handshake has completed and commands can be signed. */
39
+ get ready(): boolean;
40
+ /**
41
+ * Derive the shared key and its two HMAC sub-keys for a candidate vehicle
42
+ * public key, without touching any session state. Pure and side-effect
43
+ * free so a caller can verify authenticity first and discard the result
44
+ * if verification fails.
45
+ */
46
+ keysFor(vehiclePublicKey: Buffer): SessionKeys;
47
+ /**
48
+ * Commit an already-authenticated `SessionInfo` plus the `keysFor` result
49
+ * derived from its public key. Per the vehicle-command protocol, the
50
+ * anti-replay counter must never roll back within the same epoch, and a
51
+ * clock time that regresses within the same epoch marks the reply as
52
+ * stale/replayed - returns `false` (without mutating any state) in that
53
+ * case so the caller can reject it.
54
+ */
55
+ commit(sessionInfo: SessionInfo, keys: SessionKeys): boolean;
56
+ /**
57
+ * Advance the anti-replay counter and return fresh signature metadata for
58
+ * an HMAC-personalized command. The command expires 10 seconds from now,
59
+ * corrected for clock drift against the vehicle (`delta`).
60
+ */
61
+ hmacPersonalized(): HMACPersonalizedSignatureData;
62
+ /**
63
+ * Run `fn` with exclusive access to this session, queuing behind any
64
+ * in-flight caller. Held only for as long as `fn` takes - callers should
65
+ * keep it scoped to the synchronous message-build step, not a full
66
+ * network round trip.
67
+ */
68
+ withLock<T>(fn: () => T | Promise<T>): Promise<T>;
69
+ }
@@ -0,0 +1,69 @@
1
+ import { Domain } from "@teslemetry/tesla-protocol/command/universal_message";
2
+ import { HMACPersonalizedSignatureData, SessionInfo } from "@teslemetry/tesla-protocol/command/signatures";
3
+ /** Key material derived (but not yet trusted/committed) for a candidate vehicle public key. */
4
+ export interface SessionKeys {
5
+ sharedKey: Buffer;
6
+ hmacKey: Buffer;
7
+ sessionInfoKey: Buffer;
8
+ }
9
+ /**
10
+ * Per-domain (VCSEC / Infotainment) signed-command session: tracks the
11
+ * anti-replay counter/epoch handed out by the vehicle's handshake reply and
12
+ * the HMAC keys derived from it.
13
+ *
14
+ * Deriving key material (`keysFor`) is split from committing it (`commit`)
15
+ * because the caller must authenticate a `SessionInfo` reply - by verifying
16
+ * its `session_info_tag` against the very keys derived here - *before*
17
+ * trusting anything in it, including the vehicle public key the shared key
18
+ * itself is derived from. See `Commands.validateAndUpdateSession`.
19
+ *
20
+ * A `Session` is not itself thread-safe; `withLock` serializes the
21
+ * message-build step (which mutates `counter`) so concurrent commands on the
22
+ * same domain cannot race on the same counter value.
23
+ */
24
+ export declare class Session {
25
+ private readonly deriveSharedKey;
26
+ readonly domain: Domain;
27
+ counter: number;
28
+ epoch?: Buffer;
29
+ delta?: number;
30
+ sharedKey?: Buffer;
31
+ hmacKey?: Buffer;
32
+ sessionInfoKey?: Buffer;
33
+ publicKey?: Buffer;
34
+ /** Clock time of the last *authenticated* SessionInfo committed for the current epoch, guarding against replay/rollback. */
35
+ private lastAuthenticatedClockTime?;
36
+ private queue;
37
+ constructor(domain: Domain, deriveSharedKey: (vehiclePublicKey: Buffer) => Buffer);
38
+ /** Whether the handshake has completed and commands can be signed. */
39
+ get ready(): boolean;
40
+ /**
41
+ * Derive the shared key and its two HMAC sub-keys for a candidate vehicle
42
+ * public key, without touching any session state. Pure and side-effect
43
+ * free so a caller can verify authenticity first and discard the result
44
+ * if verification fails.
45
+ */
46
+ keysFor(vehiclePublicKey: Buffer): SessionKeys;
47
+ /**
48
+ * Commit an already-authenticated `SessionInfo` plus the `keysFor` result
49
+ * derived from its public key. Per the vehicle-command protocol, the
50
+ * anti-replay counter must never roll back within the same epoch, and a
51
+ * clock time that regresses within the same epoch marks the reply as
52
+ * stale/replayed - returns `false` (without mutating any state) in that
53
+ * case so the caller can reject it.
54
+ */
55
+ commit(sessionInfo: SessionInfo, keys: SessionKeys): boolean;
56
+ /**
57
+ * Advance the anti-replay counter and return fresh signature metadata for
58
+ * an HMAC-personalized command. The command expires 10 seconds from now,
59
+ * corrected for clock drift against the vehicle (`delta`).
60
+ */
61
+ hmacPersonalized(): HMACPersonalizedSignatureData;
62
+ /**
63
+ * Run `fn` with exclusive access to this session, queuing behind any
64
+ * in-flight caller. Held only for as long as `fn` takes - callers should
65
+ * keep it scoped to the synchronous message-build step, not a full
66
+ * network round trip.
67
+ */
68
+ withLock<T>(fn: () => T | Promise<T>): Promise<T>;
69
+ }
@@ -0,0 +1,110 @@
1
+ import crypto from "crypto";
2
+ import { HMACPersonalizedSignatureData } from "@teslemetry/tesla-protocol/command/signatures";
3
+ // src/signing/session.ts
4
+ /**
5
+ * Per-domain (VCSEC / Infotainment) signed-command session: tracks the
6
+ * anti-replay counter/epoch handed out by the vehicle's handshake reply and
7
+ * the HMAC keys derived from it.
8
+ *
9
+ * Deriving key material (`keysFor`) is split from committing it (`commit`)
10
+ * because the caller must authenticate a `SessionInfo` reply - by verifying
11
+ * its `session_info_tag` against the very keys derived here - *before*
12
+ * trusting anything in it, including the vehicle public key the shared key
13
+ * itself is derived from. See `Commands.validateAndUpdateSession`.
14
+ *
15
+ * A `Session` is not itself thread-safe; `withLock` serializes the
16
+ * message-build step (which mutates `counter`) so concurrent commands on the
17
+ * same domain cannot race on the same counter value.
18
+ */
19
+ var Session = class {
20
+ deriveSharedKey;
21
+ domain;
22
+ counter = 0;
23
+ epoch;
24
+ delta;
25
+ sharedKey;
26
+ hmacKey;
27
+ sessionInfoKey;
28
+ publicKey;
29
+ /** Clock time of the last *authenticated* SessionInfo committed for the current epoch, guarding against replay/rollback. */
30
+ lastAuthenticatedClockTime;
31
+ queue = Promise.resolve();
32
+ constructor(domain, deriveSharedKey) {
33
+ this.deriveSharedKey = deriveSharedKey;
34
+ this.domain = domain;
35
+ }
36
+ /** Whether the handshake has completed and commands can be signed. */
37
+ get ready() {
38
+ return this.epoch !== void 0 && this.hmacKey !== void 0 && this.delta !== void 0;
39
+ }
40
+ /**
41
+ * Derive the shared key and its two HMAC sub-keys for a candidate vehicle
42
+ * public key, without touching any session state. Pure and side-effect
43
+ * free so a caller can verify authenticity first and discard the result
44
+ * if verification fails.
45
+ */
46
+ keysFor(vehiclePublicKey) {
47
+ const sharedKey = this.deriveSharedKey(vehiclePublicKey);
48
+ return {
49
+ sharedKey,
50
+ hmacKey: crypto.createHmac("sha256", sharedKey).update("authenticated command").digest(),
51
+ sessionInfoKey: crypto.createHmac("sha256", sharedKey).update("session info").digest()
52
+ };
53
+ }
54
+ /**
55
+ * Commit an already-authenticated `SessionInfo` plus the `keysFor` result
56
+ * derived from its public key. Per the vehicle-command protocol, the
57
+ * anti-replay counter must never roll back within the same epoch, and a
58
+ * clock time that regresses within the same epoch marks the reply as
59
+ * stale/replayed - returns `false` (without mutating any state) in that
60
+ * case so the caller can reject it.
61
+ */
62
+ commit(sessionInfo, keys) {
63
+ const incomingEpoch = Buffer.from(sessionInfo.epoch);
64
+ const sameEpoch = this.epoch !== void 0 && this.epoch.equals(incomingEpoch);
65
+ if (sameEpoch && this.lastAuthenticatedClockTime !== void 0 && sessionInfo.clockTime < this.lastAuthenticatedClockTime) return false;
66
+ this.counter = sameEpoch ? Math.max(this.counter, sessionInfo.counter) : sessionInfo.counter;
67
+ this.epoch = incomingEpoch;
68
+ this.delta = Math.floor(Date.now() / 1e3) - sessionInfo.clockTime;
69
+ this.lastAuthenticatedClockTime = sessionInfo.clockTime;
70
+ this.publicKey = Buffer.from(sessionInfo.publicKey);
71
+ this.sharedKey = keys.sharedKey;
72
+ this.hmacKey = keys.hmacKey;
73
+ this.sessionInfoKey = keys.sessionInfoKey;
74
+ return true;
75
+ }
76
+ /**
77
+ * Advance the anti-replay counter and return fresh signature metadata for
78
+ * an HMAC-personalized command. The command expires 10 seconds from now,
79
+ * corrected for clock drift against the vehicle (`delta`).
80
+ */
81
+ hmacPersonalized() {
82
+ if (!this.ready || !this.epoch || this.delta === void 0) throw new Error("Session is not ready: perform the handshake first.");
83
+ this.counter++;
84
+ return HMACPersonalizedSignatureData.create({
85
+ epoch: this.epoch,
86
+ counter: this.counter,
87
+ expiresAt: Math.floor(Date.now() / 1e3) - this.delta + 10
88
+ });
89
+ }
90
+ /**
91
+ * Run `fn` with exclusive access to this session, queuing behind any
92
+ * in-flight caller. Held only for as long as `fn` takes - callers should
93
+ * keep it scoped to the synchronous message-build step, not a full
94
+ * network round trip.
95
+ */
96
+ async withLock(fn) {
97
+ const previous = this.queue;
98
+ let release;
99
+ this.queue = new Promise((resolve) => {
100
+ release = resolve;
101
+ });
102
+ await previous.catch(() => void 0);
103
+ try {
104
+ return await fn();
105
+ } finally {
106
+ release();
107
+ }
108
+ }
109
+ };
110
+ export { Session };
@@ -0,0 +1,292 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // src/tariff.ts
3
+ const MINUTES_PER_DAY = 1440;
4
+ function formatParts(date, timeZone) {
5
+ const parts = new Intl.DateTimeFormat("en-US", {
6
+ timeZone,
7
+ year: "numeric",
8
+ month: "2-digit",
9
+ day: "2-digit",
10
+ hour: "2-digit",
11
+ minute: "2-digit",
12
+ second: "2-digit",
13
+ hourCycle: "h23"
14
+ }).formatToParts(date);
15
+ const out = {};
16
+ for (const p of parts) out[p.type] = Number(p.value);
17
+ return out;
18
+ }
19
+ /** Tesla weekday (Mon=0..Sun=6) for a calendar date, via JS Date.getUTCDay() (Sun=0..Sat=6). */
20
+ function dowFromDate(year, month, day) {
21
+ return (new Date(Date.UTC(year, month - 1, day)).getUTCDay() + 6) % 7;
22
+ }
23
+ function getWallClock(now, timeZone) {
24
+ const { year, month, day, hour, minute } = formatParts(now, timeZone);
25
+ return {
26
+ year,
27
+ month,
28
+ day,
29
+ dow: dowFromDate(year, month, day),
30
+ minuteOfDay: hour * 60 + minute
31
+ };
32
+ }
33
+ /**
34
+ * Converts a target site-local wall-clock (year/month/day/hour/minute in `timeZone`) to the UTC
35
+ * instant it represents. Iterates because the zone's UTC offset at the target instant is itself
36
+ * what we're solving for (DST) - 3 rounds converges for any real-world offset change.
37
+ */
38
+ function wallClockToUtcMillis(year, month, day, hour, minute, timeZone) {
39
+ const target = Date.UTC(year, month - 1, day, hour, minute);
40
+ let guess = target;
41
+ for (let i = 0; i < 3; i++) {
42
+ const p = formatParts(new Date(guess), timeZone);
43
+ const next = target - (Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - guess);
44
+ if (next === guess) break;
45
+ guess = next;
46
+ }
47
+ return guess;
48
+ }
49
+ /**
50
+ * Adds `deltaMinutes` of wall-clock time (not elapsed real time) to a wall-clock reading, staying
51
+ * in pure calendar arithmetic throughout - no `timeZone`/`Intl` round trip, so this is safe to use
52
+ * for "what day/weekday is N minutes from now" even across a DST transition, where a real-time-ms
53
+ * shift and a wall-clock-minutes shift land on different calendar days.
54
+ */
55
+ function wallClockAt(wallClock, deltaMinutes) {
56
+ const naive = new Date(Date.UTC(wallClock.year, wallClock.month - 1, wallClock.day, 0, wallClock.minuteOfDay + deltaMinutes));
57
+ const year = naive.getUTCFullYear();
58
+ const month = naive.getUTCMonth() + 1;
59
+ const day = naive.getUTCDate();
60
+ const minuteOfDay = naive.getUTCHours() * 60 + naive.getUTCMinutes();
61
+ return {
62
+ year,
63
+ month,
64
+ day,
65
+ dow: dowFromDate(year, month, day),
66
+ minuteOfDay
67
+ };
68
+ }
69
+ function dowInRange(d, fromDow, toDow) {
70
+ return fromDow <= toDow ? d >= fromDow && d <= toDow : d >= fromDow || d <= toDow;
71
+ }
72
+ function seasonContains(season, month, day) {
73
+ if (!season?.tou_periods) return false;
74
+ const fromMonth = season.fromMonth ?? 1;
75
+ const fromDay = season.fromDay ?? 1;
76
+ const toMonth = season.toMonth ?? 12;
77
+ const toDay = season.toDay ?? 31;
78
+ const from = fromMonth * 100 + fromDay;
79
+ const to = toMonth * 100 + toDay;
80
+ const cur = month * 100 + day;
81
+ return from <= to ? cur >= from && cur <= to : cur >= from || cur <= to;
82
+ }
83
+ function findSeason(seasons, month, day) {
84
+ if (!seasons) return null;
85
+ for (const [name, season] of Object.entries(seasons)) if (seasonContains(season, month, day)) return {
86
+ name,
87
+ season
88
+ };
89
+ return null;
90
+ }
91
+ /**
92
+ * Expands a season's `tou_periods` into the windows active on one specific calendar day (given
93
+ * as a Tesla weekday, Mon=0..Sun=6). A period whose `fromDayOfWeek..toDayOfWeek` includes `dow`
94
+ * contributes its own window (clamped to this day, `endMin` capped at 1440 if it crosses
95
+ * midnight); a period that crosses midnight and includes *yesterday* also contributes its
96
+ * early-morning continuation here, `trueStartMin` carrying its real (pre-midnight) start so a
97
+ * currently-active continuation can still report an accurate `currentStart`.
98
+ */
99
+ function dayPeriods(touPeriods, dow) {
100
+ const segments = [];
101
+ const prevDow = (dow + 6) % 7;
102
+ for (const [periodName, def] of Object.entries(touPeriods)) for (const window of def.periods ?? []) {
103
+ const fromDow = window.fromDayOfWeek ?? 0;
104
+ const toDow = window.toDayOfWeek ?? 6;
105
+ const fromMin = (window.fromHour ?? 0) * 60 + (window.fromMinute ?? 0);
106
+ const toMin = (window.toHour ?? 0) * 60 + (window.toMinute ?? 0);
107
+ const crossesMidnight = toMin <= fromMin;
108
+ if (dowInRange(dow, fromDow, toDow)) {
109
+ const endMin = crossesMidnight ? MINUTES_PER_DAY : toMin;
110
+ if (endMin > fromMin) segments.push({
111
+ periodName,
112
+ startMin: fromMin,
113
+ endMin,
114
+ trueStartMin: fromMin
115
+ });
116
+ }
117
+ if (crossesMidnight && toMin > 0 && dowInRange(prevDow, fromDow, toDow)) segments.push({
118
+ periodName,
119
+ startMin: 0,
120
+ endMin: toMin,
121
+ trueStartMin: fromMin - MINUTES_PER_DAY
122
+ });
123
+ }
124
+ return segments;
125
+ }
126
+ function lookupPrice(charges, seasonName, periodName) {
127
+ if (!charges) return null;
128
+ const seasonRates = charges[seasonName]?.rates ?? charges["ALL"]?.rates;
129
+ if (!seasonRates) return null;
130
+ const value = Object.prototype.hasOwnProperty.call(seasonRates, periodName) ? seasonRates[periodName] : seasonRates["ALL"];
131
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
132
+ }
133
+ const EMPTY_RATE = {
134
+ price: null,
135
+ periodName: null,
136
+ seasonName: null
137
+ };
138
+ /**
139
+ * Resolves one grid's (buy's, or sell's) state at `wallClock`: the active period if any, and the
140
+ * next boundary regardless of whether a period is currently active - a sparse tariff's gap still
141
+ * has a "next change" (the gap's own end) and a "since" (the gap's own start), just like an active
142
+ * period does (its own end and start).
143
+ */
144
+ function scheduleAt(seasons, wallClock) {
145
+ const matched = findSeason(seasons, wallClock.month, wallClock.day);
146
+ if (!matched) return null;
147
+ const todaySegs = dayPeriods(matched.season.tou_periods, wallClock.dow);
148
+ const current = todaySegs.find((s) => s.startMin <= wallClock.minuteOfDay && wallClock.minuteOfDay < s.endMin) ?? null;
149
+ const laterTodayStarts = todaySegs.filter((s) => s.startMin > wallClock.minuteOfDay).map((s) => s.startMin);
150
+ let nextMin = current ? current.endMin : null;
151
+ if (laterTodayStarts.length > 0) nextMin = nextMin === null ? Math.min(...laterTodayStarts) : Math.min(nextMin, Math.min(...laterTodayStarts));
152
+ else if (nextMin === null || nextMin >= MINUTES_PER_DAY) {
153
+ const tomorrowWallClock = wallClockAt(wallClock, MINUTES_PER_DAY - wallClock.minuteOfDay);
154
+ const tomorrowMatched = findSeason(seasons, tomorrowWallClock.month, tomorrowWallClock.day);
155
+ const tomorrowSegs = tomorrowMatched ? dayPeriods(tomorrowMatched.season.tou_periods, tomorrowWallClock.dow) : [];
156
+ const continuation = current && tomorrowMatched?.name === matched.name ? tomorrowSegs.find((s) => s.startMin === 0 && s.periodName === current.periodName) : void 0;
157
+ if (continuation) nextMin = MINUTES_PER_DAY + continuation.endMin;
158
+ else if (tomorrowSegs.length > 0) {
159
+ const tomorrowFirst = MINUTES_PER_DAY + Math.min(...tomorrowSegs.map((s) => s.startMin));
160
+ nextMin = nextMin === null ? tomorrowFirst : Math.min(nextMin, tomorrowFirst);
161
+ }
162
+ }
163
+ let sinceMin;
164
+ if (current) sinceMin = current.trueStartMin;
165
+ else {
166
+ const earlierTodayEnds = todaySegs.filter((s) => s.endMin <= wallClock.minuteOfDay).map((s) => s.endMin);
167
+ if (earlierTodayEnds.length > 0) sinceMin = Math.max(...earlierTodayEnds);
168
+ else {
169
+ const yesterdayWallClock = wallClockAt(wallClock, -1440);
170
+ const yesterdayMatched = findSeason(seasons, yesterdayWallClock.month, yesterdayWallClock.day);
171
+ const yesterdaySegs = yesterdayMatched ? dayPeriods(yesterdayMatched.season.tou_periods, yesterdayWallClock.dow) : [];
172
+ sinceMin = yesterdaySegs.length > 0 ? Math.max(...yesterdaySegs.map((s) => s.endMin)) - MINUTES_PER_DAY : null;
173
+ }
174
+ }
175
+ return {
176
+ seasonName: matched.name,
177
+ current,
178
+ nextChangeGM: nextMin === null ? null : nextMin - wallClock.minuteOfDay,
179
+ sinceGM: sinceMin === null ? null : sinceMin - wallClock.minuteOfDay
180
+ };
181
+ }
182
+ /** Walks one calendar day at a time across the horizon, re-resolving the season fresh on each day (via pure wall-clock arithmetic, not elapsed-ms/DST-sensitive arithmetic) so a horizon crossing a season boundary re-prices and re-labels correctly. */
183
+ function collectSegments(seasons, wallClock, horizonMinutes) {
184
+ const segments = [];
185
+ let dayStartGM = -wallClock.minuteOfDay;
186
+ while (dayStartGM < horizonMinutes) {
187
+ const dayWallClock = wallClockAt(wallClock, dayStartGM);
188
+ const matched = findSeason(seasons, dayWallClock.month, dayWallClock.day);
189
+ if (matched) for (const seg of dayPeriods(matched.season.tou_periods, dayWallClock.dow)) {
190
+ const startGM = dayStartGM + seg.startMin;
191
+ const endGM = dayStartGM + seg.endMin;
192
+ if (endGM > 0 && startGM < horizonMinutes) segments.push({
193
+ startGM,
194
+ endGM,
195
+ periodName: seg.periodName,
196
+ seasonName: matched.name
197
+ });
198
+ }
199
+ dayStartGM += MINUTES_PER_DAY;
200
+ }
201
+ segments.sort((a, b) => a.startGM - b.startGM);
202
+ const merged = [];
203
+ for (const seg of segments) {
204
+ const last = merged[merged.length - 1];
205
+ if (last && last.endGM === seg.startGM && last.periodName === seg.periodName && last.seasonName === seg.seasonName) last.endGM = seg.endGM;
206
+ else merged.push({ ...seg });
207
+ }
208
+ return merged;
209
+ }
210
+ function buildUpcoming(buySeasons, buyCharges, sellSeasons, sellCharges, hasSell, wallClock, horizonMinutes, toDate) {
211
+ const buySegs = collectSegments(buySeasons, wallClock, horizonMinutes);
212
+ const sellSegs = hasSell ? collectSegments(sellSeasons, wallClock, horizonMinutes) : [];
213
+ const boundaries = /* @__PURE__ */ new Set([0, horizonMinutes]);
214
+ const addBoundary = (gm) => {
215
+ if (gm > 0 && gm < horizonMinutes) boundaries.add(gm);
216
+ };
217
+ for (const s of buySegs) {
218
+ addBoundary(s.startGM);
219
+ addBoundary(s.endGM);
220
+ }
221
+ for (const s of sellSegs) {
222
+ addBoundary(s.startGM);
223
+ addBoundary(s.endGM);
224
+ }
225
+ const sorted = [...boundaries].sort((a, b) => a - b);
226
+ const periods = [];
227
+ for (let i = 0; i < sorted.length - 1; i++) {
228
+ const segStart = sorted[i];
229
+ const segEnd = sorted[i + 1];
230
+ if (segStart === segEnd) continue;
231
+ const buyAt = buySegs.find((s) => s.startGM <= segStart && segStart < s.endGM);
232
+ const sellAt = sellSegs.find((s) => s.startGM <= segStart && segStart < s.endGM);
233
+ periods.push({
234
+ start: toDate(segStart),
235
+ end: toDate(segEnd),
236
+ buy: buyAt ? {
237
+ price: lookupPrice(buyCharges, buyAt.seasonName, buyAt.periodName),
238
+ periodName: buyAt.periodName,
239
+ seasonName: buyAt.seasonName
240
+ } : EMPTY_RATE,
241
+ sell: sellAt ? {
242
+ price: lookupPrice(sellCharges, sellAt.seasonName, sellAt.periodName),
243
+ periodName: sellAt.periodName,
244
+ seasonName: sellAt.seasonName
245
+ } : EMPTY_RATE
246
+ });
247
+ }
248
+ return periods;
249
+ }
250
+ /**
251
+ * Resolves the current buy/sell time-of-use rate for a Tariff V2 object at a given instant.
252
+ * `tariff` carries no timezone; the caller must supply the site's IANA zone (e.g. from
253
+ * `site_info.installation_time_zone`) so wall-clock period boundaries match correctly.
254
+ */
255
+ function getTariffPeriods(tariff, now, opts) {
256
+ if (!tariff?.seasons || !tariff?.energy_charges) return null;
257
+ if (!opts?.timeZone) throw new Error("getTariffPeriods requires opts.timeZone");
258
+ const timeZone = opts.timeZone;
259
+ const wallClock = getWallClock(now, timeZone);
260
+ const buySchedule = scheduleAt(tariff.seasons, wallClock);
261
+ if (!buySchedule?.current) return null;
262
+ const sellSeasons = tariff.sell_tariff?.seasons ?? tariff.seasons;
263
+ const sellSchedule = tariff.sell_tariff ? scheduleAt(sellSeasons, wallClock) : null;
264
+ const buyRate = {
265
+ price: lookupPrice(tariff.energy_charges, buySchedule.seasonName, buySchedule.current.periodName),
266
+ periodName: buySchedule.current.periodName,
267
+ seasonName: buySchedule.seasonName
268
+ };
269
+ const sellRate = sellSchedule?.current ? {
270
+ price: lookupPrice(tariff.sell_tariff.energy_charges, sellSchedule.seasonName, sellSchedule.current.periodName),
271
+ periodName: sellSchedule.current.periodName,
272
+ seasonName: sellSchedule.seasonName
273
+ } : EMPTY_RATE;
274
+ const toDate = (gm) => {
275
+ const target = wallClockAt(wallClock, gm);
276
+ const hour = Math.floor(target.minuteOfDay / 60);
277
+ const minute = target.minuteOfDay % 60;
278
+ return new Date(wallClockToUtcMillis(target.year, target.month, target.day, hour, minute, timeZone));
279
+ };
280
+ const nextChangeGM = sellSchedule?.nextChangeGM != null ? Math.min(buySchedule.nextChangeGM, sellSchedule.nextChangeGM) : buySchedule.nextChangeGM;
281
+ const resolution = {
282
+ buy: buyRate,
283
+ sell: sellRate,
284
+ currentStart: toDate(sellSchedule?.sinceGM != null ? Math.max(buySchedule.sinceGM, sellSchedule.sinceGM) : buySchedule.sinceGM),
285
+ nextChange: toDate(nextChangeGM),
286
+ currency: typeof tariff.currency === "string" ? tariff.currency : null,
287
+ upcoming: null
288
+ };
289
+ if (opts.horizonHours) resolution.upcoming = buildUpcoming(tariff.seasons, tariff.energy_charges, sellSeasons, tariff.sell_tariff?.energy_charges, !!tariff.sell_tariff, wallClock, opts.horizonHours * 60, toDate);
290
+ return resolution;
291
+ }
292
+ exports.getTariffPeriods = getTariffPeriods;