aziosxjs 1.0.0 → 1.0.2

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.
@@ -0,0 +1,152 @@
1
+ import * as http from 'http'
2
+ import * as https from 'https'
3
+ import { URL } from 'url'
4
+ import type { AziosRequestConfig } from '../types/config'
5
+ import type { AziosResponse } from '../types/response'
6
+ import { BaseAdapter } from '../runtimes/HttpAdapter'
7
+ import AziosError from '../errors/AziosError'
8
+
9
+ /**
10
+ * Node.js HTTP adapter using built-in http/https modules
11
+ * Compatible with Node.js 16+ (before global fetch was available)
12
+ */
13
+ export class NodeHttpAdapter extends BaseAdapter {
14
+ /**
15
+ * Check if this adapter is supported (Node.js environment)
16
+ */
17
+ isSupported(): boolean {
18
+ // In Node.js, this adapter is always supported
19
+ return true
20
+ }
21
+
22
+ /**
23
+ * Execute HTTP request using Node.js http/https modules
24
+ */
25
+ async request(config: AziosRequestConfig): Promise<AziosResponse> {
26
+ if (!this.isSupported()) {
27
+ throw new AziosError(
28
+ 'Node HTTP adapter is not supported in this runtime',
29
+ 'UNSUPPORTED_RUNTIME',
30
+ config
31
+ )
32
+ }
33
+
34
+ // Check if request is already cancelled
35
+ if (config.signal && config.signal.aborted) {
36
+ throw new AziosError(
37
+ 'Request aborted',
38
+ 'ABORTED',
39
+ config
40
+ )
41
+ }
42
+
43
+ return new Promise((resolve, reject) => {
44
+ const { url, method = 'GET', headers = {}, data, timeout, signal } = config
45
+
46
+ const baseURL = config.baseURL || ''
47
+ const fullURL = new URL(baseURL + url)
48
+
49
+ // Add query parameters
50
+ if (config.params) {
51
+ Object.entries(config.params).forEach(([key, value]) => {
52
+ if (value !== null && value !== undefined) {
53
+ if (Array.isArray(value)) {
54
+ value.forEach(v => fullURL.searchParams.append(key, String(v)))
55
+ } else {
56
+ fullURL.searchParams.set(key, String(value))
57
+ }
58
+ }
59
+ })
60
+ }
61
+
62
+ const isHttps = fullURL.protocol === 'https:'
63
+ const client = isHttps ? https : http
64
+
65
+ const options: http.RequestOptions = {
66
+ hostname: fullURL.hostname,
67
+ port: fullURL.port || (isHttps ? 443 : 80),
68
+ path: fullURL.pathname + fullURL.search,
69
+ method: method.toUpperCase(),
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ ...headers
73
+ }
74
+ }
75
+
76
+ // Handle timeout
77
+ let timeoutId: NodeJS.Timeout | undefined
78
+ if (timeout) {
79
+ timeoutId = setTimeout(() => {
80
+ req.destroy(new Error('Request timeout'))
81
+ }, timeout)
82
+ }
83
+
84
+ // Handle abort signal
85
+ if (signal) {
86
+ signal.addEventListener('abort', () => {
87
+ req.destroy(new Error('Request aborted'))
88
+ })
89
+ }
90
+
91
+ const req = client.request(options, (res) => {
92
+ let body = ''
93
+
94
+ res.on('data', (chunk) => {
95
+ body += chunk
96
+ })
97
+
98
+ res.on('end', () => {
99
+ if (timeoutId) {
100
+ clearTimeout(timeoutId)
101
+ }
102
+
103
+ let responseData: any = body
104
+
105
+ // Parse JSON if content-type is application/json
106
+ const contentType = res.headers['content-type']
107
+ if (contentType?.includes('application/json') && body) {
108
+ try {
109
+ responseData = JSON.parse(body)
110
+ } catch (e) {
111
+ // Keep as string if parsing fails
112
+ }
113
+ }
114
+
115
+ const response: AziosResponse = {
116
+ data: responseData,
117
+ status: res.statusCode || 200,
118
+ statusText: res.statusMessage || '',
119
+ headers: res.headers as Record<string, string>,
120
+ config
121
+ }
122
+
123
+ resolve(response)
124
+ })
125
+ })
126
+
127
+ req.on('error', (error) => {
128
+ if (timeoutId) {
129
+ clearTimeout(timeoutId)
130
+ }
131
+ const errorCode = error.message.includes('aborted') ? 'ABORTED' : 'NETWORK_ERROR'
132
+ reject(new AziosError(
133
+ `Request failed: ${error.message}`,
134
+ errorCode,
135
+ config,
136
+ undefined,
137
+ error
138
+ ))
139
+ })
140
+
141
+ // Send request body
142
+ if (data) {
143
+ const body = typeof data === 'string' ? data : JSON.stringify(data)
144
+ req.write(body)
145
+ }
146
+
147
+ req.end()
148
+ })
149
+ }
150
+ }
151
+
152
+ export default NodeHttpAdapter
@@ -0,0 +1 @@
1
+ export { default as NodeHttpAdapter } from './httpAdapter'
@@ -0,0 +1,283 @@
1
+ import type { AziosInstance } from "../types/request"
2
+ import type { AziosRequestConfig } from "../types/config"
3
+ import type { AziosResponse } from "../types/response"
4
+
5
+ /**
6
+ * Token storage and refresh configuration
7
+ */
8
+ export interface TokenConfig {
9
+ accessToken: string
10
+ refreshToken?: string
11
+ expiresIn?: number
12
+ tokenType?: string
13
+ }
14
+
15
+ /**
16
+ * Auth endpoints configuration
17
+ */
18
+ export interface AuthEndpoints {
19
+ refreshUrl: string
20
+ logoutUrl?: string
21
+ }
22
+
23
+ /**
24
+ * Auth Manager - Handle authentication, token refresh, and 401 responses
25
+ * Production-grade auth system with automatic token refresh and global 401 handling
26
+ */
27
+ export default class AuthManager {
28
+ private token: TokenConfig | null = null
29
+ private endpoints: AuthEndpoints | null = null
30
+ private instance: AziosInstance | null = null
31
+ private isRefreshing: boolean = false
32
+ private refreshSubscribers: ((token: string) => void)[] = []
33
+
34
+ /**
35
+ * Initialize the auth manager with endpoints
36
+ */
37
+ initialize(endpoints: AuthEndpoints): void {
38
+ this.endpoints = endpoints
39
+ }
40
+
41
+ /**
42
+ * Set the Azios instance for making refresh requests
43
+ */
44
+ setInstance(instance: AziosInstance): void {
45
+ this.instance = instance
46
+ }
47
+
48
+ /**
49
+ * Store token
50
+ */
51
+ setToken(token: TokenConfig): void {
52
+ this.token = token
53
+ }
54
+
55
+ /**
56
+ * Get current token
57
+ */
58
+ getToken(): TokenConfig | null {
59
+ return this.token
60
+ }
61
+
62
+ /**
63
+ * Get access token string
64
+ */
65
+ getAccessToken(): string | null {
66
+ return this.token?.accessToken || null
67
+ }
68
+
69
+ /**
70
+ * Check if token exists
71
+ */
72
+ hasToken(): boolean {
73
+ return this.token !== null && this.token.accessToken !== ""
74
+ }
75
+
76
+ /**
77
+ * Check if token is expired
78
+ */
79
+ isTokenExpired(): boolean {
80
+ if (!this.token || !this.token.expiresIn) {
81
+ return false
82
+ }
83
+
84
+ // Check if expiry time has passed (with 60s buffer)
85
+ return Date.now() > this.token.expiresIn - 60000
86
+ }
87
+
88
+ /**
89
+ * Clear token
90
+ */
91
+ clearToken(): void {
92
+ this.token = null
93
+ this.refreshSubscribers = []
94
+ }
95
+
96
+ /**
97
+ * Refresh the access token
98
+ */
99
+ async refreshAccessToken(): Promise<boolean> {
100
+ if (!this.instance || !this.endpoints) {
101
+ console.warn("AuthManager not properly initialized")
102
+ return false
103
+ }
104
+
105
+ if (!this.token?.refreshToken) {
106
+ console.warn("No refresh token available")
107
+ return false
108
+ }
109
+
110
+ try {
111
+ // Prevent multiple simultaneous refresh requests
112
+ if (this.isRefreshing) {
113
+ return new Promise(resolve => {
114
+ this.refreshSubscribers.push(() => {
115
+ resolve(true)
116
+ })
117
+ })
118
+ }
119
+
120
+ this.isRefreshing = true
121
+
122
+ const response = await this.instance.post(
123
+ this.endpoints.refreshUrl,
124
+ { refreshToken: this.token.refreshToken }
125
+ )
126
+
127
+ if (response.data?.accessToken) {
128
+ this.token = {
129
+ ...this.token,
130
+ accessToken: response.data.accessToken,
131
+ expiresIn: Date.now() + (response.data.expiresIn || 3600000)
132
+ }
133
+
134
+ // Notify all subscribers
135
+ this.refreshSubscribers.forEach(cb => cb(this.token!.accessToken))
136
+ this.refreshSubscribers = []
137
+
138
+ this.isRefreshing = false
139
+ return true
140
+ }
141
+
142
+ this.isRefreshing = false
143
+ return false
144
+ } catch (error) {
145
+ this.isRefreshing = false
146
+ console.error("Token refresh failed:", error)
147
+ return false
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Get Authorization header value
153
+ */
154
+ getAuthHeader(): string | null {
155
+ if (!this.hasToken()) {
156
+ return null
157
+ }
158
+
159
+ const tokenType = this.token?.tokenType || "Bearer"
160
+ return `${tokenType} ${this.token?.accessToken}`
161
+ }
162
+
163
+ /**
164
+ * Attach auth header to request
165
+ */
166
+ attachAuthHeader(config: AziosRequestConfig): AziosRequestConfig {
167
+ const authHeader = this.getAuthHeader()
168
+
169
+ if (authHeader) {
170
+ return {
171
+ ...config,
172
+ headers: {
173
+ ...config.headers,
174
+ Authorization: authHeader
175
+ }
176
+ }
177
+ }
178
+
179
+ return config
180
+ }
181
+
182
+ /**
183
+ * Handle 401 response (unauthorized)
184
+ */
185
+ async handle401(config: AziosRequestConfig): Promise<boolean> {
186
+ if (!this.hasToken()) {
187
+ // No token to refresh
188
+ return false
189
+ }
190
+
191
+ const refreshed = await this.refreshAccessToken()
192
+
193
+ if (refreshed) {
194
+ // Update the failed request with new token
195
+ return true
196
+ }
197
+
198
+ // Token refresh failed, logout
199
+ await this.logout()
200
+ return false
201
+ }
202
+
203
+ /**
204
+ * Logout user
205
+ */
206
+ async logout(): Promise<void> {
207
+ if (this.instance && this.endpoints?.logoutUrl) {
208
+ try {
209
+ await this.instance.post(this.endpoints.logoutUrl)
210
+ } catch (error) {
211
+ console.error("Logout failed:", error)
212
+ }
213
+ }
214
+
215
+ this.clearToken()
216
+ }
217
+
218
+ /**
219
+ * Get auth status summary
220
+ */
221
+ getStatus(): {
222
+ authenticated: boolean
223
+ tokenExpired: boolean
224
+ refreshToken: boolean
225
+ } {
226
+ return {
227
+ authenticated: this.hasToken(),
228
+ tokenExpired: this.isTokenExpired(),
229
+ refreshToken: this.token?.refreshToken !== undefined
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Create auth plugin that integrates with Azios
236
+ */
237
+ export function createAuthPlugin(endpoints: AuthEndpoints) {
238
+ const authManager = new AuthManager()
239
+ authManager.initialize(endpoints)
240
+
241
+ return {
242
+ name: "auth",
243
+ version: "1.0.0",
244
+ hooks: {
245
+ onInstall: async (instance: AziosInstance) => {
246
+ authManager.setInstance(instance)
247
+
248
+ // Add response interceptor to handle 401
249
+ instance.interceptors.response.use(
250
+ (response: AziosResponse) => response,
251
+ async (error: any) => {
252
+ const originalRequest = error.config
253
+
254
+ // Check if error is 401 and retry hasn't been attempted
255
+ if (
256
+ error.response?.status === 401 &&
257
+ !originalRequest._retry &&
258
+ authManager.hasToken()
259
+ ) {
260
+ originalRequest._retry = true
261
+
262
+ try {
263
+ const refreshed = await authManager.handle401(originalRequest)
264
+
265
+ if (refreshed) {
266
+ // Retry original request with new token
267
+ const newConfig = authManager.attachAuthHeader(originalRequest)
268
+ return instance.request(newConfig)
269
+ }
270
+ } catch (refreshError) {
271
+ console.error("Auth refresh failed:", refreshError)
272
+ }
273
+ }
274
+
275
+ throw error
276
+ }
277
+ )
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ export type { AuthManager }
package/src/core/Azios.ts CHANGED
@@ -6,6 +6,7 @@ import InterceptorManager from "../interceptors/InterceptorManager"
6
6
  import PluginManager from "../plugins/pluginManager"
7
7
  import MiddlewareManager from "../middleware/middlewareManager"
8
8
  import { compose } from "../middleware/compose"
9
+ import RequestMonitor from "../monitoring/RequestMonitor"
9
10
 
10
11
  /**
11
12
  * Core Azios HTTP client class
@@ -21,6 +22,7 @@ export default class Azios {
21
22
 
22
23
  middlewares: MiddlewareManager
23
24
  plugins: PluginManager
25
+ monitor: RequestMonitor
24
26
 
25
27
  constructor(config: AziosRequestConfig) {
26
28
  this.defaults = config
@@ -32,6 +34,7 @@ export default class Azios {
32
34
 
33
35
  this.middlewares = new MiddlewareManager()
34
36
  this.plugins = new PluginManager()
37
+ this.monitor = new RequestMonitor(100, false) // Disabled by default
35
38
  }
36
39
 
37
40
  /**
@@ -63,52 +66,69 @@ export default class Azios {
63
66
  config = { ...this.defaults, ...config }
64
67
 
65
68
  // -------------------------
66
- // PLUGIN: PRE-REQUEST HOOKS
69
+ // MONITORING: START TRACKING
67
70
  // -------------------------
68
- config = await this.plugins.executeBeforeRequestHooks(config)
71
+ const requestId = this.monitor.startRequest(config)
69
72
 
70
- // -------------------------
71
- // MIDDLEWARE PIPELINE
72
- // -------------------------
73
- const context = { config }
74
- const fn = compose(this.middlewares.middlewares)
75
- await fn(context)
76
- config = context.config
73
+ try {
74
+ // -------------------------
75
+ // PLUGIN: PRE-REQUEST HOOKS
76
+ // -------------------------
77
+ config = await this.plugins.executeBeforeRequestHooks(config)
77
78
 
78
- // -------------------------
79
- // INTERCEPTOR PIPELINE
80
- // -------------------------
81
- const chain: any[] = []
79
+ // -------------------------
80
+ // MIDDLEWARE PIPELINE
81
+ // -------------------------
82
+ const context = { config }
83
+ const fn = compose(this.middlewares.middlewares)
84
+ await fn(context)
85
+ config = context.config
82
86
 
83
- // Request interceptors (reverse order)
84
- this.interceptors.request.forEach(interceptor => {
85
- chain.unshift(interceptor.fulfilled, interceptor.rejected)
86
- })
87
+ // -------------------------
88
+ // INTERCEPTOR PIPELINE
89
+ // -------------------------
90
+ const chain: any[] = []
87
91
 
88
- // Main dispatch
89
- chain.push(dispatchRequest, undefined)
92
+ // Request interceptors (reverse order)
93
+ this.interceptors.request.forEach(interceptor => {
94
+ chain.unshift(interceptor.fulfilled, interceptor.rejected)
95
+ })
90
96
 
91
- // Response interceptors (normal order)
92
- this.interceptors.response.forEach(interceptor => {
93
- chain.push(interceptor.fulfilled, interceptor.rejected)
94
- })
97
+ // Main dispatch
98
+ chain.push(dispatchRequest, undefined)
95
99
 
96
- let promise = Promise.resolve(config)
100
+ // Response interceptors (normal order)
101
+ this.interceptors.response.forEach(interceptor => {
102
+ chain.push(interceptor.fulfilled, interceptor.rejected)
103
+ })
97
104
 
98
- while (chain.length) {
99
- const fulfilled = chain.shift()
100
- const rejected = chain.shift()
101
- promise = promise.then(fulfilled, rejected)
102
- }
105
+ let promise = Promise.resolve(config)
106
+
107
+ while (chain.length) {
108
+ const fulfilled = chain.shift()
109
+ const rejected = chain.shift()
110
+ promise = promise.then(fulfilled, rejected)
111
+ }
103
112
 
104
- try {
105
113
  const response = await promise as any
106
114
 
107
115
  // -------------------------
108
116
  // PLUGIN: POST-RESPONSE HOOKS
109
117
  // -------------------------
110
- return await this.plugins.executeAfterResponseHooks(response)
118
+ const finalResponse = await this.plugins.executeAfterResponseHooks(response)
119
+
120
+ // -------------------------
121
+ // MONITORING: END TRACKING (SUCCESS)
122
+ // -------------------------
123
+ this.monitor.endRequest(requestId, config, finalResponse)
124
+
125
+ return finalResponse
111
126
  } catch (error) {
127
+ // -------------------------
128
+ // MONITORING: END TRACKING (ERROR)
129
+ // -------------------------
130
+ this.monitor.endRequestWithError(requestId, config, error)
131
+
112
132
  // -------------------------
113
133
  // PLUGIN: ERROR HOOKS
114
134
  // -------------------------
package/src/index.ts CHANGED
@@ -5,6 +5,8 @@ const azios = createInstance({
5
5
  url: ""
6
6
  })
7
7
 
8
+ export default azios
9
+
8
10
  // ============================================
9
11
  // Type Exports
10
12
  // ============================================
@@ -48,17 +50,25 @@ export {
48
50
  isBrowserRuntime,
49
51
  RuntimeType
50
52
  } from "./runtimes/detectRuntime"
51
- export { default as AdapterFactory } from "./runtimes/AdapterFactory"
52
53
 
53
54
  // ============================================
54
- // Cache & Rate Limit Exports
55
+ // NEW: Monitoring System Exports
55
56
  // ============================================
56
57
 
57
- export { getCache, setCache, clearCache } from "./cache/memoryCache"
58
- export { schedule } from "./rateLimiter/rateLimiter"
58
+ export { default as RequestMonitor } from "./monitoring/RequestMonitor"
59
+ export type { RequestLog } from "./monitoring/RequestMonitor"
59
60
 
60
61
  // ============================================
61
- // Default Export
62
+ // NEW: Authentication System Exports
62
63
  // ============================================
63
64
 
64
- export default azios
65
+ export { default as AuthManager, createAuthPlugin } from "./auth/AuthManager"
66
+ export type { TokenConfig, AuthEndpoints, AuthManager as IAuthManager } from "./auth/AuthManager"
67
+ export { default as AdapterFactory } from "./runtimes/AdapterFactory"
68
+
69
+ // ============================================
70
+ // Cache & Rate Limit Exports
71
+ // ============================================
72
+
73
+ export { getCache, setCache, clearCache } from "./cache/memoryCache"
74
+ export { schedule } from "./rateLimiter/rateLimiter"