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/.github/workflows/publish-to-npm.yml +80 -0
- package/LICENSE +674 -0
- package/README.md +257 -0
- package/dist/auth/manager.d.ts +19 -0
- package/dist/auth/manager.js +228 -0
- package/dist/auth/types.d.ts +277 -0
- package/dist/auth/types.js +64 -0
- package/dist/core/auth-manager.d.ts +54 -0
- package/dist/core/auth-manager.js +159 -0
- package/dist/core/auth-types.d.ts +238 -0
- package/dist/core/auth-types.js +54 -0
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +18 -0
- package/dist/core/constants.d.ts +25 -0
- package/dist/core/constants.js +51 -0
- package/dist/core/gtfs-types.d.ts +198 -0
- package/dist/core/gtfs-types.js +70 -0
- package/dist/core/manager.d.ts +39 -0
- package/dist/core/manager.js +359 -0
- package/dist/core/parsers.d.ts +893 -0
- package/dist/core/parsers.js +110 -0
- package/dist/core/utils.d.ts +4 -0
- package/dist/core/utils.js +59 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +22 -0
- package/dist/live-polling.d.ts +1 -0
- package/dist/live-polling.js +105 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +35 -0
- package/dist/text.d.ts +1 -0
- package/dist/text.js +35 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +30 -0
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;
|