zet-api 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,257 @@
1
+ # 🚊 ZET API - Zagreb Electric Tram API Client
2
+
3
+ > TypeScript/Node.js client for **ZET (Zagreb Electric Tram)** public transportation API.
4
+ > Access real-time data for routes, trips, stops, vehicles, and service updates.
5
+
6
+ ---
7
+
8
+ ## ✨ Features
9
+
10
+ - 🚋 Get all **tram and bus routes**
11
+ - 🚌 Fetch **real-time trip information**
12
+ - ⏱️ Get **trip stop times** with live tracking
13
+ - 📰 Access **ZET newsfeed** for service updates
14
+ - 🚗 Track **live vehicle positions** with GPS coordinates
15
+ - � **Automatic authentication** with token refresh
16
+ - 💾 Built-in **TTL-based caching** for optimal performance
17
+ - ✅ **Zod schema validation** for type-safe data
18
+ - ⚡ Written in **TypeScript**, ready for Node.js (≥20.2.0)
19
+
20
+ ---
21
+
22
+ ## 📦 Installation
23
+
24
+ ```bash
25
+ npm install zet-api
26
+ # or
27
+ pnpm add zet-api
28
+ ```
29
+
30
+ ---
31
+
32
+ ## 🚀 Quick Start
33
+
34
+ ```typescript
35
+ import { ZetManager } from "zet-api";
36
+
37
+ // Create manager with infinite cache (default)
38
+ const zet = new ZetManager();
39
+
40
+ // Get all routes (cached)
41
+ const routes = await zet.getRoutes();
42
+ console.log("Total routes:", routes.length);
43
+
44
+ // Search for a specific route
45
+ const results = await zet.searchRoutes({ query: "Borongaj", limit: 5 });
46
+
47
+ // Get real-time trips for route 1
48
+ const trips = await zet.getRouteTrips({ routeId: 1, daysFromToday: 0 });
49
+
50
+ // Get stop times with parsed dates
51
+ const stopTimes = await zet.getTripStopTimes({
52
+ tripId: "0_5_105_1_10687",
53
+ });
54
+ console.log("Next stop:", stopTimes.find((s) => !s.isArrived)?.stopName);
55
+
56
+ // Get live vehicles for route 1
57
+ const vehicles = await zet.getLiveVehicles({ routeId: 1 });
58
+ console.log("Active vehicles:", vehicles.length);
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 🎯 Caching Strategy
64
+
65
+ The `ZetManager` caches static data (routes, stops, news) for performance while always fetching fresh real-time data (trips, vehicles).
66
+
67
+ ### Constructor Options
68
+
69
+ ```typescript
70
+ // Infinite cache (never expires) - default
71
+ const zet = new ZetManager();
72
+
73
+ // 5 minute cache
74
+ const zet = new ZetManager(5 * 60 * 1000);
75
+
76
+ // No cache (always fetch fresh)
77
+ const zet = new ZetManager(0);
78
+ ```
79
+
80
+ ### What Gets Cached?
81
+
82
+ | Data Type | Cached? | Reason |
83
+ | ---------- | ------- | -------------------- |
84
+ | Routes | ✅ Yes | Rarely changes |
85
+ | Stops | ✅ Yes | Rarely changes |
86
+ | News | ✅ Yes | Changes occasionally |
87
+ | Trips | ❌ No | Real-time data |
88
+ | Stop Times | ❌ No | Real-time data |
89
+ | Vehicles | ❌ No | Real-time positions |
90
+
91
+ ## 🔐 Authentication
92
+
93
+ The `ZetManager` includes an integrated `authManager` that handles authentication automatically. Use it to access user-specific features like account information and ePurse balance.
94
+
95
+ ### Basic Usage
96
+
97
+ ```typescript
98
+ import { ZetManager } from "zet-api";
99
+
100
+ const zet = new ZetManager();
101
+
102
+ // Login once
103
+ await zet.authManager.login({
104
+ username: "your-email@example.com",
105
+ password: "your-password",
106
+ });
107
+
108
+ // Access user data (tokens refresh automatically)
109
+ const account = await zet.authManager.getAccount();
110
+ console.log(`Welcome, ${account.firstName}!`);
111
+ console.log(`Balance: ${account.ePurseAmount}€`);
112
+
113
+ // Check authentication status
114
+ if (zet.authManager.isAuthenticated()) {
115
+ console.log("User is logged in");
116
+ }
117
+
118
+ // Logout when done
119
+ await zet.authManager.logout();
120
+ ```
121
+
122
+ ### Register New Account
123
+
124
+ ```typescript
125
+ await zet.authManager.register({
126
+ email: "your-email@example.com",
127
+ password: "your-secure-password",
128
+ confirmPassword: "your-secure-password",
129
+ });
130
+
131
+ console.log("✅ Registration successful! Check your email to confirm.");
132
+ ```
133
+
134
+ ### Long-Running Service Example
135
+
136
+ ```typescript
137
+ import { ZetManager } from "zet-api";
138
+
139
+ const zet = new ZetManager();
140
+
141
+ // Login once at startup
142
+ await zet.authManager.login({
143
+ username: process.env.ZET_EMAIL!,
144
+ password: process.env.ZET_PASSWORD!,
145
+ });
146
+
147
+ console.log("🚀 Service started with automatic auth");
148
+
149
+ // Poll live data every 30 seconds
150
+ setInterval(async () => {
151
+ try {
152
+ // Tokens refresh automatically - no manual management needed
153
+ const trips = await zet.getStopIncomingTrips({ stopId: "317_1" });
154
+ console.log(
155
+ `[${new Date().toLocaleTimeString()}] ${trips.length} incoming trips`
156
+ );
157
+ } catch (error) {
158
+ console.error("Error:", error.message);
159
+ }
160
+ }, 30000);
161
+ ```
162
+
163
+ ---
164
+
165
+ ## 💡 Usage Examples
166
+
167
+ ### Real-Time Trip Tracking
168
+
169
+ ```typescript
170
+ const zet = new ZetManager();
171
+
172
+ // Get route info
173
+ const route = await zet.getRouteById(1);
174
+ console.log(`Tracking: ${route?.longName}`);
175
+
176
+ // Get active trips
177
+ const trips = await zet.getRouteTrips({ routeId: 1 });
178
+ const activeTrips = trips.filter((t) => t.tripStatus === 2);
179
+
180
+ // Track first active trip
181
+ if (activeTrips[0]) {
182
+ const stopTimes = await zet.getTripStopTimes({
183
+ tripId: activeTrips[0].id,
184
+ });
185
+
186
+ const nextStops = stopTimes.filter((st) => !st.isArrived);
187
+ console.log("Upcoming stops:");
188
+ nextStops.forEach((stop) => {
189
+ const time = stop.expectedArrivalDateTime.toLocaleTimeString();
190
+ console.log(` ${stop.stopName} - ${time}`);
191
+ });
192
+ }
193
+ ```
194
+
195
+ ### Live Data Polling
196
+
197
+ ```typescript
198
+ const zet = new ZetManager(60000); // 1-minute cache for static data
199
+
200
+ // Poll route 6 every 10 seconds
201
+ setInterval(async () => {
202
+ const liveData = await zet.getLiveTripsForRoute(6);
203
+ console.log(
204
+ `[${new Date().toLocaleTimeString()}] ${liveData.size} active trips`
205
+ );
206
+
207
+ for (const [tripId, stopTimes] of liveData.entries()) {
208
+ const nextStop = stopTimes.find((st) => !st.isArrived);
209
+ if (nextStop) {
210
+ console.log(` Trip ${tripId}: Next stop ${nextStop.stopName}`);
211
+ }
212
+ }
213
+ }, 10000);
214
+ ```
215
+
216
+ ### Search and Filter
217
+
218
+ ```typescript
219
+ const zet = new ZetManager();
220
+
221
+ // Search stops
222
+ const stops = await zet.searchStops({
223
+ query: "Glavni kolodvor",
224
+ limit: 5,
225
+ });
226
+
227
+ stops.forEach((stop) => {
228
+ console.log(`${stop.name}`);
229
+ console.log(` Routes: ${stop.trips.map((t) => t.routeCode).join(", ")}`);
230
+ });
231
+ ```
232
+
233
+ ### Service Updates
234
+
235
+ ```typescript
236
+ const zet = new ZetManager();
237
+
238
+ // Get active news
239
+ const newsWithDates = await zet.getNewsfeed();
240
+ const now = new Date();
241
+
242
+ const activeNews = newsWithDates.filter(
243
+ (n) => n.validFrom <= now && n.validTo >= now
244
+ );
245
+
246
+ console.log(`${activeNews.length} active service updates`);
247
+ activeNews.forEach((item) => {
248
+ console.log(`📰 ${item.title}`);
249
+ console.log(` Lines: ${item.lines.join(", ") || "All"}`);
250
+ });
251
+ ```
252
+
253
+ ---
254
+
255
+ ## 📄 License
256
+
257
+ This project is licensed under the GPL-v3 License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,19 @@
1
+ import { LoginCredentials, RegisterCredentials, AuthTokensLogin } from './types';
2
+ export declare class ZetAuthManager {
3
+ private tokens;
4
+ private refreshPromise;
5
+ private readonly tokenExpiryBufferMs;
6
+ private readonly defaultTokenExpiryMs;
7
+ isAuthenticated(): boolean;
8
+ getAccessToken(): Promise<string>;
9
+ login(credentials: LoginCredentials): Promise<AuthTokensLogin>;
10
+ register(credentials: RegisterCredentials): Promise<void>;
11
+ logout(): Promise<void>;
12
+ private tryReuseAccessToken;
13
+ private tryRefreshToken;
14
+ private performPasswordLogin;
15
+ private ensureTokenRefresh;
16
+ private performTokenRefresh;
17
+ private storeTokens;
18
+ private clearTokens;
19
+ }
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ZetAuthManager = void 0;
40
+ const types_1 = require("./types");
41
+ const config_1 = __importStar(require("../core/config"));
42
+ const utils_1 = require("../core/utils");
43
+ const axios_1 = __importDefault(require("axios"));
44
+ class ZetAuthManager {
45
+ tokens = null;
46
+ refreshPromise = null;
47
+ tokenExpiryBufferMs = 120000; // 2 minutes before expiry, consider it expired.
48
+ defaultTokenExpiryMs = 900000; // 15 minutes default expiry if not provided.
49
+ isAuthenticated() {
50
+ return this.tokens !== null && Date.now() < this.tokens.expiresAt - this.tokenExpiryBufferMs;
51
+ }
52
+ async getAccessToken() {
53
+ if (!this.tokens)
54
+ throw new Error('Not authenticated. Please login first.');
55
+ if (Date.now() < this.tokens.expiresAt - this.tokenExpiryBufferMs)
56
+ return this.tokens.access;
57
+ await this.ensureTokenRefresh();
58
+ if (!this.tokens || Date.now() >= this.tokens.expiresAt - this.tokenExpiryBufferMs)
59
+ throw new Error('Token expired. Please login again.');
60
+ return this.tokens.access;
61
+ }
62
+ async login(credentials) {
63
+ const validated = types_1.LoginCredentialsSchema.parse(credentials);
64
+ if (validated.accessToken && validated.refreshToken) {
65
+ const reusedTokens = this.tryReuseAccessToken(validated.accessToken, validated.refreshToken);
66
+ if (reusedTokens)
67
+ return reusedTokens;
68
+ }
69
+ if (validated.refreshToken) {
70
+ const refreshedTokens = await this.tryRefreshToken(validated.refreshToken);
71
+ if (refreshedTokens)
72
+ return refreshedTokens;
73
+ }
74
+ return await this.performPasswordLogin(validated);
75
+ }
76
+ async register(credentials) {
77
+ const validated = types_1.RegisterCredentialsSchema.parse(credentials);
78
+ if (validated.password !== validated.confirmPassword)
79
+ throw new Error('Passwords do not match.');
80
+ const response = await axios_1.default.post(`${config_1.default.accountServiceUrl}/register`, validated, { headers: config_1.headers }).catch((err) => err.response);
81
+ if (!response || response.status !== 200) {
82
+ const errorMsg = response?.data?.message || response?.statusText || 'Unknown error';
83
+ throw new Error(`Registration failed: ${errorMsg}`);
84
+ }
85
+ }
86
+ async logout() {
87
+ if (this.tokens?.refresh) {
88
+ try {
89
+ const validated = types_1.RefreshTokenRequestSchema.parse({ refreshToken: this.tokens.refresh });
90
+ await axios_1.default.post(`${config_1.default.authServiceUrl}/logout`, validated, { headers: config_1.headers }).catch(() => { });
91
+ }
92
+ catch {
93
+ // *
94
+ }
95
+ }
96
+ this.clearTokens();
97
+ }
98
+ tryReuseAccessToken(accessToken, refreshToken) {
99
+ try {
100
+ const parts = accessToken.split('.');
101
+ if (parts.length !== 3 || !parts[1])
102
+ return null;
103
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
104
+ if (!payload.exp)
105
+ return null;
106
+ const expiresAt = payload.exp * 1000;
107
+ const now = Date.now();
108
+ if (expiresAt > now + this.tokenExpiryBufferMs) {
109
+ this.storeTokens({ accessToken, refreshToken }, expiresAt);
110
+ return {
111
+ accessToken: this.tokens.access,
112
+ refreshToken: this.tokens.refresh,
113
+ expiresIn: Math.max(0, this.tokens.expiresAt - now),
114
+ viaTokenRefresh: false,
115
+ viaAccessToken: true,
116
+ };
117
+ }
118
+ return null;
119
+ }
120
+ catch (error) {
121
+ return null;
122
+ }
123
+ }
124
+ async tryRefreshToken(refreshToken) {
125
+ try {
126
+ const validated = types_1.RefreshTokenRequestSchema.parse({ refreshToken });
127
+ const response = await axios_1.default.post(`${config_1.default.authServiceUrl}/refreshTokens`, validated, { headers: config_1.headers }).catch((err) => err.response);
128
+ if (!response || response.status !== 200)
129
+ return null;
130
+ const parsed = types_1.AuthTokensSchema.safeParse(response.data);
131
+ if (!parsed.success)
132
+ return null;
133
+ this.storeTokens(parsed.data);
134
+ return {
135
+ accessToken: this.tokens.access,
136
+ refreshToken: this.tokens.refresh,
137
+ expiresIn: Math.max(0, this.tokens.expiresAt - Date.now()),
138
+ viaTokenRefresh: true,
139
+ viaAccessToken: false,
140
+ };
141
+ }
142
+ catch (error) {
143
+ return null;
144
+ }
145
+ }
146
+ async performPasswordLogin(validated) {
147
+ if (!validated.username || !validated.password)
148
+ throw new Error('Username and password are required for login.');
149
+ const loginData = {
150
+ username: validated.username,
151
+ password: validated.password,
152
+ revokeOtherTokens: validated.revokeOtherTokens,
153
+ fcmToken: validated.fcmToken,
154
+ };
155
+ const response = await axios_1.default.post(`${config_1.default.authServiceUrl}/login`, loginData, { headers: config_1.headers }).catch((err) => err.response);
156
+ if (!response || response.status !== 200)
157
+ throw new Error(`Login failed: ${response?.statusText || 'Unknown error'}`);
158
+ const parsed = types_1.AuthTokensSchema.safeParse(response.data);
159
+ if (!parsed.success)
160
+ throw new Error(`Failed to parse login response: ${(0, utils_1.parseZodError)(parsed.error).join(', ')}`);
161
+ this.storeTokens(parsed.data);
162
+ return {
163
+ accessToken: this.tokens.access,
164
+ refreshToken: this.tokens.refresh,
165
+ expiresIn: Math.max(0, this.tokens.expiresAt - Date.now()),
166
+ viaTokenRefresh: false,
167
+ viaAccessToken: false,
168
+ };
169
+ }
170
+ async ensureTokenRefresh() {
171
+ if (this.refreshPromise)
172
+ return this.refreshPromise;
173
+ this.refreshPromise = this.performTokenRefresh();
174
+ try {
175
+ await this.refreshPromise;
176
+ }
177
+ finally {
178
+ this.refreshPromise = null;
179
+ }
180
+ }
181
+ async performTokenRefresh() {
182
+ if (!this.tokens?.refresh)
183
+ throw new Error('No refresh token available.');
184
+ const refreshToken = this.tokens.refresh;
185
+ try {
186
+ const validated = types_1.RefreshTokenRequestSchema.parse({ refreshToken });
187
+ const response = await axios_1.default.post(`${config_1.default.authServiceUrl}/refreshTokens`, validated, { headers: config_1.headers }).catch((err) => err.response);
188
+ if (!response || response.status !== 200) {
189
+ this.clearTokens();
190
+ throw new Error(`Token refresh failed: ${response?.statusText || 'Unknown error'}. Please login again.`);
191
+ }
192
+ const parsed = types_1.AuthTokensSchema.safeParse(response.data);
193
+ if (!parsed.success) {
194
+ this.clearTokens();
195
+ throw new Error(`Failed to parse refresh response: ${(0, utils_1.parseZodError)(parsed.error).join(', ')}`);
196
+ }
197
+ this.storeTokens(parsed.data);
198
+ }
199
+ catch (error) {
200
+ this.clearTokens();
201
+ throw error;
202
+ }
203
+ }
204
+ storeTokens(tokens, expiresAt) {
205
+ if (!expiresAt) {
206
+ try {
207
+ const parts = tokens.accessToken.split('.');
208
+ if (parts.length === 3 && parts[1]) {
209
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
210
+ expiresAt = payload.exp ? payload.exp * 1000 : Date.now() + this.defaultTokenExpiryMs;
211
+ }
212
+ }
213
+ catch {
214
+ // *
215
+ }
216
+ }
217
+ this.tokens = {
218
+ access: tokens.accessToken,
219
+ refresh: tokens.refreshToken,
220
+ expiresAt: expiresAt || Date.now() + this.defaultTokenExpiryMs,
221
+ };
222
+ }
223
+ clearTokens() {
224
+ this.tokens = null;
225
+ this.refreshPromise = null;
226
+ }
227
+ }
228
+ exports.ZetAuthManager = ZetAuthManager;