@hazeljs/gateway 0.2.0-beta.41

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 (72) hide show
  1. package/LICENSE +192 -0
  2. package/README.md +255 -0
  3. package/dist/__tests__/canary-engine.test.d.ts +2 -0
  4. package/dist/__tests__/canary-engine.test.d.ts.map +1 -0
  5. package/dist/__tests__/canary-engine.test.js +133 -0
  6. package/dist/__tests__/decorators.test.d.ts +2 -0
  7. package/dist/__tests__/decorators.test.d.ts.map +1 -0
  8. package/dist/__tests__/decorators.test.js +174 -0
  9. package/dist/__tests__/from-config.test.d.ts +2 -0
  10. package/dist/__tests__/from-config.test.d.ts.map +1 -0
  11. package/dist/__tests__/from-config.test.js +67 -0
  12. package/dist/__tests__/gateway-metrics.test.d.ts +2 -0
  13. package/dist/__tests__/gateway-metrics.test.d.ts.map +1 -0
  14. package/dist/__tests__/gateway-metrics.test.js +82 -0
  15. package/dist/__tests__/gateway-module.test.d.ts +2 -0
  16. package/dist/__tests__/gateway-module.test.d.ts.map +1 -0
  17. package/dist/__tests__/gateway-module.test.js +91 -0
  18. package/dist/__tests__/gateway.test.d.ts +2 -0
  19. package/dist/__tests__/gateway.test.d.ts.map +1 -0
  20. package/dist/__tests__/gateway.test.js +257 -0
  21. package/dist/__tests__/hazel-integration.test.d.ts +2 -0
  22. package/dist/__tests__/hazel-integration.test.d.ts.map +1 -0
  23. package/dist/__tests__/hazel-integration.test.js +92 -0
  24. package/dist/__tests__/route-matcher.test.d.ts +2 -0
  25. package/dist/__tests__/route-matcher.test.d.ts.map +1 -0
  26. package/dist/__tests__/route-matcher.test.js +67 -0
  27. package/dist/__tests__/service-proxy.test.d.ts +2 -0
  28. package/dist/__tests__/service-proxy.test.d.ts.map +1 -0
  29. package/dist/__tests__/service-proxy.test.js +110 -0
  30. package/dist/__tests__/traffic-mirror.test.d.ts +2 -0
  31. package/dist/__tests__/traffic-mirror.test.d.ts.map +1 -0
  32. package/dist/__tests__/traffic-mirror.test.js +70 -0
  33. package/dist/__tests__/version-router.test.d.ts +2 -0
  34. package/dist/__tests__/version-router.test.d.ts.map +1 -0
  35. package/dist/__tests__/version-router.test.js +136 -0
  36. package/dist/canary/canary-engine.d.ts +107 -0
  37. package/dist/canary/canary-engine.d.ts.map +1 -0
  38. package/dist/canary/canary-engine.js +334 -0
  39. package/dist/decorators/index.d.ts +74 -0
  40. package/dist/decorators/index.d.ts.map +1 -0
  41. package/dist/decorators/index.js +170 -0
  42. package/dist/gateway.d.ts +67 -0
  43. package/dist/gateway.d.ts.map +1 -0
  44. package/dist/gateway.js +310 -0
  45. package/dist/gateway.module.d.ts +67 -0
  46. package/dist/gateway.module.d.ts.map +1 -0
  47. package/dist/gateway.module.js +82 -0
  48. package/dist/hazel-integration.d.ts +24 -0
  49. package/dist/hazel-integration.d.ts.map +1 -0
  50. package/dist/hazel-integration.js +70 -0
  51. package/dist/index.d.ts +20 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +62 -0
  54. package/dist/metrics/gateway-metrics.d.ts +64 -0
  55. package/dist/metrics/gateway-metrics.d.ts.map +1 -0
  56. package/dist/metrics/gateway-metrics.js +159 -0
  57. package/dist/middleware/traffic-mirror.d.ts +19 -0
  58. package/dist/middleware/traffic-mirror.d.ts.map +1 -0
  59. package/dist/middleware/traffic-mirror.js +60 -0
  60. package/dist/proxy/service-proxy.d.ts +68 -0
  61. package/dist/proxy/service-proxy.d.ts.map +1 -0
  62. package/dist/proxy/service-proxy.js +211 -0
  63. package/dist/routing/route-matcher.d.ts +31 -0
  64. package/dist/routing/route-matcher.d.ts.map +1 -0
  65. package/dist/routing/route-matcher.js +112 -0
  66. package/dist/routing/version-router.d.ts +36 -0
  67. package/dist/routing/version-router.d.ts.map +1 -0
  68. package/dist/routing/version-router.js +136 -0
  69. package/dist/types/index.d.ts +217 -0
  70. package/dist/types/index.d.ts.map +1 -0
  71. package/dist/types/index.js +17 -0
  72. package/package.json +74 -0
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ /**
3
+ * Service Proxy
4
+ * Enhanced HTTP client that integrates discovery, resilience, and traffic policies.
5
+ * The core request engine of the gateway.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.ServiceProxy = void 0;
12
+ const axios_1 = __importDefault(require("axios"));
13
+ const resilience_1 = require("@hazeljs/resilience");
14
+ class ServiceProxy {
15
+ constructor(discoveryClient, config) {
16
+ this.discoveryClient = discoveryClient;
17
+ this.config = config;
18
+ this.axiosInstance = axios_1.default.create({
19
+ timeout: config.timeout ?? 10000,
20
+ });
21
+ this.metrics = new resilience_1.MetricsCollector(60000);
22
+ // Initialize resilience components
23
+ if (config.retry) {
24
+ this.retryPolicy = new resilience_1.RetryPolicy(config.retry);
25
+ }
26
+ if (config.circuitBreaker) {
27
+ const breakerName = `gateway:${config.serviceName}`;
28
+ this.circuitBreaker = resilience_1.CircuitBreakerRegistry.getOrCreate(breakerName, config.circuitBreaker);
29
+ }
30
+ if (config.timeout) {
31
+ this.timeout = new resilience_1.Timeout(config.timeout);
32
+ }
33
+ if (config.rateLimit) {
34
+ this.rateLimiter = new resilience_1.RateLimiter(config.rateLimit);
35
+ }
36
+ }
37
+ /**
38
+ * Forward an incoming request to the target service
39
+ */
40
+ async forward(request) {
41
+ // Apply rate limiting — reject with 429 when limit exceeded
42
+ if (this.rateLimiter) {
43
+ if (!this.rateLimiter.tryAcquire()) {
44
+ const retryAfterMs = this.rateLimiter.getRetryAfterMs();
45
+ throw new resilience_1.RateLimitError(`Rate limit exceeded for ${this.config.serviceName}. Retry after ${retryAfterMs}ms`, retryAfterMs);
46
+ }
47
+ }
48
+ // Apply request transformation
49
+ let transformedRequest = request;
50
+ if (this.config.transform?.request) {
51
+ transformedRequest = this.config.transform.request(request);
52
+ }
53
+ // Build the execution function
54
+ const executeFn = async () => {
55
+ return this.doForward(transformedRequest);
56
+ };
57
+ // Wrap with resilience layers
58
+ let wrappedFn = executeFn;
59
+ // Retry wraps the inner call
60
+ if (this.retryPolicy) {
61
+ const retryPolicy = this.retryPolicy;
62
+ const innerFn = wrappedFn;
63
+ wrappedFn = () => retryPolicy.execute(innerFn);
64
+ }
65
+ // Circuit breaker wraps retry
66
+ if (this.circuitBreaker) {
67
+ const breaker = this.circuitBreaker;
68
+ const innerFn = wrappedFn;
69
+ wrappedFn = () => breaker.execute(innerFn);
70
+ }
71
+ // Timeout wraps circuit breaker
72
+ if (this.timeout) {
73
+ const timeout = this.timeout;
74
+ const innerFn = wrappedFn;
75
+ wrappedFn = () => timeout.execute(innerFn);
76
+ }
77
+ const startTime = Date.now();
78
+ try {
79
+ const response = await wrappedFn();
80
+ this.metrics.recordSuccess(Date.now() - startTime);
81
+ // Apply response transformation
82
+ if (this.config.transform?.response) {
83
+ return this.config.transform.response(response);
84
+ }
85
+ return response;
86
+ }
87
+ catch (error) {
88
+ this.metrics.recordFailure(Date.now() - startTime, String(error));
89
+ throw error;
90
+ }
91
+ }
92
+ /**
93
+ * Forward a request with a specific version filter
94
+ */
95
+ async forwardToVersion(request, version, additionalFilter) {
96
+ const filter = {
97
+ ...this.config.filter,
98
+ ...additionalFilter,
99
+ metadata: {
100
+ ...this.config.filter?.metadata,
101
+ ...additionalFilter?.metadata,
102
+ version,
103
+ },
104
+ };
105
+ return this.forwardWithFilter(request, filter);
106
+ }
107
+ /**
108
+ * Forward with a custom filter override
109
+ */
110
+ async forwardWithFilter(request, filter) {
111
+ const instance = await this.discoveryClient.getInstance(this.config.serviceName, this.config.loadBalancingStrategy, filter);
112
+ if (!instance) {
113
+ throw new Error(`No instances available for service: ${this.config.serviceName} with filter`);
114
+ }
115
+ return this.doForwardToInstance(request, instance);
116
+ }
117
+ /**
118
+ * Get the metrics collector for this proxy
119
+ */
120
+ getMetrics() {
121
+ return this.metrics;
122
+ }
123
+ /**
124
+ * Get the circuit breaker instance (if configured)
125
+ */
126
+ getCircuitBreaker() {
127
+ return this.circuitBreaker;
128
+ }
129
+ /**
130
+ * Get the service name this proxy routes to
131
+ */
132
+ getServiceName() {
133
+ return this.config.serviceName;
134
+ }
135
+ // ─── Internal ───
136
+ async doForward(request) {
137
+ const instance = await this.discoveryClient.getInstance(this.config.serviceName, this.config.loadBalancingStrategy, this.config.filter);
138
+ if (!instance) {
139
+ throw new Error(`No instances available for service: ${this.config.serviceName}`);
140
+ }
141
+ return this.doForwardToInstance(request, instance);
142
+ }
143
+ async doForwardToInstance(request, instance) {
144
+ // Build the target path
145
+ let targetPath = request.path;
146
+ if (this.config.stripPrefix && targetPath.startsWith(this.config.stripPrefix)) {
147
+ targetPath = targetPath.slice(this.config.stripPrefix.length) || '/';
148
+ }
149
+ if (this.config.addPrefix) {
150
+ targetPath = this.config.addPrefix + targetPath;
151
+ }
152
+ // Normalize: remove trailing slash so "/users/" becomes "/users".
153
+ // This avoids 404s when the backend registers routes without a trailing slash.
154
+ if (targetPath.length > 1 && targetPath.endsWith('/')) {
155
+ targetPath = targetPath.slice(0, -1);
156
+ }
157
+ const baseURL = `${instance.protocol || 'http'}://${instance.host}:${instance.port}`;
158
+ // Strip hop-by-hop headers that are specific to the client↔gateway connection.
159
+ // These must NOT be forwarded to the upstream service because:
160
+ // - content-length: the body is re-serialized by axios, so the original
161
+ // content-length would be wrong and the upstream would hang waiting for
162
+ // exactly that many bytes.
163
+ // - transfer-encoding: same issue — axios chooses its own encoding.
164
+ // - connection / keep-alive: per-hop, not end-to-end.
165
+ const HOP_BY_HOP = new Set([
166
+ 'content-length',
167
+ 'transfer-encoding',
168
+ 'connection',
169
+ 'keep-alive',
170
+ 'upgrade',
171
+ 'expect',
172
+ 'host',
173
+ 'te',
174
+ 'trailer',
175
+ ]);
176
+ const headers = {};
177
+ for (const [key, value] of Object.entries(request.headers)) {
178
+ if (value !== undefined && !HOP_BY_HOP.has(key.toLowerCase())) {
179
+ headers[key] = Array.isArray(value) ? value.join(', ') : value;
180
+ }
181
+ }
182
+ headers['host'] = `${instance.host}:${instance.port}`;
183
+ const axiosConfig = {
184
+ method: request.method,
185
+ url: targetPath,
186
+ baseURL,
187
+ headers,
188
+ data: request.body,
189
+ params: request.query,
190
+ };
191
+ try {
192
+ const response = await this.axiosInstance.request(axiosConfig);
193
+ return {
194
+ status: response.status,
195
+ headers: response.headers,
196
+ body: response.data,
197
+ };
198
+ }
199
+ catch (error) {
200
+ if (axios_1.default.isAxiosError(error) && error.response) {
201
+ return {
202
+ status: error.response.status,
203
+ headers: error.response.headers,
204
+ body: error.response.data,
205
+ };
206
+ }
207
+ throw error;
208
+ }
209
+ }
210
+ }
211
+ exports.ServiceProxy = ServiceProxy;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Route Matcher
3
+ * Matches incoming request paths against gateway route patterns.
4
+ * Supports glob patterns (/**), path parameters (:param), and wildcards (*).
5
+ */
6
+ export interface RouteMatch {
7
+ /** Whether the pattern matched */
8
+ matched: boolean;
9
+ /** Extracted path parameters */
10
+ params: Record<string, string>;
11
+ /** The remaining path after the matched prefix (for proxying) */
12
+ remainingPath: string;
13
+ }
14
+ /**
15
+ * Convert a route pattern to a regex for matching
16
+ *
17
+ * Patterns:
18
+ * /api/users -> exact match
19
+ * /api/users/:id -> path parameter
20
+ * /api/users/* -> single segment wildcard
21
+ * /api/users/** -> multi-segment wildcard (greedy)
22
+ */
23
+ export declare function matchRoute(pattern: string, path: string): RouteMatch;
24
+ /**
25
+ * Sort route patterns by specificity (most specific first)
26
+ * Rules:
27
+ * 1. Exact paths > parameterized paths > wildcards > catch-all
28
+ * 2. Longer paths > shorter paths
29
+ */
30
+ export declare function sortRoutesBySpecificity(patterns: string[]): string[];
31
+ //# sourceMappingURL=route-matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/routing/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAoDpE;AAgBD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAOpE"}
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ /**
3
+ * Route Matcher
4
+ * Matches incoming request paths against gateway route patterns.
5
+ * Supports glob patterns (/**), path parameters (:param), and wildcards (*).
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.matchRoute = matchRoute;
9
+ exports.sortRoutesBySpecificity = sortRoutesBySpecificity;
10
+ /**
11
+ * Convert a route pattern to a regex for matching
12
+ *
13
+ * Patterns:
14
+ * /api/users -> exact match
15
+ * /api/users/:id -> path parameter
16
+ * /api/users/* -> single segment wildcard
17
+ * /api/users/** -> multi-segment wildcard (greedy)
18
+ */
19
+ function matchRoute(pattern, path) {
20
+ const noMatch = { matched: false, params: {}, remainingPath: '' };
21
+ // Normalize paths
22
+ const normPattern = normalizePath(pattern);
23
+ const normPath = normalizePath(path);
24
+ // Handle exact "**" at end (catch-all)
25
+ if (normPattern.endsWith('/**')) {
26
+ const prefix = normPattern.slice(0, -3); // Remove /**
27
+ if (normPath === prefix || normPath.startsWith(prefix + '/')) {
28
+ const remaining = normPath.slice(prefix.length) || '/';
29
+ return {
30
+ matched: true,
31
+ params: {},
32
+ remainingPath: remaining,
33
+ };
34
+ }
35
+ return noMatch;
36
+ }
37
+ // Split into segments
38
+ const patternSegments = normPattern.split('/').filter(Boolean);
39
+ const pathSegments = normPath.split('/').filter(Boolean);
40
+ // Different segment counts means no match (unless there's a wildcard)
41
+ if (patternSegments.length !== pathSegments.length) {
42
+ return noMatch;
43
+ }
44
+ const params = {};
45
+ for (let i = 0; i < patternSegments.length; i++) {
46
+ const pSeg = patternSegments[i];
47
+ const rSeg = pathSegments[i];
48
+ if (pSeg.startsWith(':')) {
49
+ // Path parameter
50
+ params[pSeg.slice(1)] = rSeg;
51
+ }
52
+ else if (pSeg === '*') {
53
+ // Single-segment wildcard, matches anything
54
+ continue;
55
+ }
56
+ else if (pSeg !== rSeg) {
57
+ return noMatch;
58
+ }
59
+ }
60
+ return {
61
+ matched: true,
62
+ params,
63
+ remainingPath: normPath,
64
+ };
65
+ }
66
+ /**
67
+ * Normalize a path: ensure leading slash, remove trailing slash
68
+ */
69
+ function normalizePath(path) {
70
+ let normalized = path;
71
+ if (!normalized.startsWith('/')) {
72
+ normalized = '/' + normalized;
73
+ }
74
+ if (normalized.length > 1 && normalized.endsWith('/')) {
75
+ normalized = normalized.slice(0, -1);
76
+ }
77
+ return normalized;
78
+ }
79
+ /**
80
+ * Sort route patterns by specificity (most specific first)
81
+ * Rules:
82
+ * 1. Exact paths > parameterized paths > wildcards > catch-all
83
+ * 2. Longer paths > shorter paths
84
+ */
85
+ function sortRoutesBySpecificity(patterns) {
86
+ return [...patterns].sort((a, b) => {
87
+ const aScore = getSpecificityScore(a);
88
+ const bScore = getSpecificityScore(b);
89
+ if (aScore !== bScore)
90
+ return bScore - aScore; // Higher score = more specific
91
+ return b.length - a.length; // Longer = more specific
92
+ });
93
+ }
94
+ function getSpecificityScore(pattern) {
95
+ const segments = pattern.split('/').filter(Boolean);
96
+ let score = segments.length * 10; // Base score from segment count
97
+ for (const seg of segments) {
98
+ if (seg === '**') {
99
+ score -= 5; // Catch-all is least specific
100
+ }
101
+ else if (seg === '*') {
102
+ score -= 3; // Single wildcard
103
+ }
104
+ else if (seg.startsWith(':')) {
105
+ score -= 1; // Parameter
106
+ }
107
+ else {
108
+ score += 2; // Exact segment is most specific
109
+ }
110
+ }
111
+ return score;
112
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Version Router
3
+ * Routes requests to specific service versions based on:
4
+ * - Header (X-API-Version)
5
+ * - URI prefix (/v2/api/...)
6
+ * - Query parameter (?version=v2)
7
+ * - Weighted random (percentage-based)
8
+ */
9
+ import { VersionRouteConfig, VersionRouteEntry, ProxyRequest } from '../types';
10
+ export interface VersionResolution {
11
+ /** The resolved version to route to */
12
+ version: string;
13
+ /** How the version was resolved */
14
+ resolvedBy: 'header' | 'uri' | 'query' | 'weight' | 'default';
15
+ }
16
+ export declare class VersionRouter {
17
+ private config;
18
+ constructor(config: VersionRouteConfig);
19
+ /**
20
+ * Resolve the target version for an incoming request
21
+ */
22
+ resolve(request: ProxyRequest): VersionResolution;
23
+ /**
24
+ * Get the version configuration for a specific version
25
+ */
26
+ getVersionEntry(version: string): VersionRouteEntry | undefined;
27
+ /**
28
+ * Get all configured versions
29
+ */
30
+ getVersions(): string[];
31
+ private resolveFromHeader;
32
+ private resolveFromUri;
33
+ private resolveFromQuery;
34
+ private resolveByWeight;
35
+ }
36
+ //# sourceMappingURL=version-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-router.d.ts","sourceRoot":"","sources":["../../src/routing/version-router.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE/E,MAAM,WAAW,iBAAiB;IAChC,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,UAAU,EAAE,QAAQ,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;CAC/D;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAqB;gBAEvB,MAAM,EAAE,kBAAkB;IAStC;;OAEG;IACH,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,iBAAiB;IAuCjD;;OAEG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAI/D;;OAEG;IACH,WAAW,IAAI,MAAM,EAAE;IAMvB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,gBAAgB;IAcxB,OAAO,CAAC,eAAe;CA6BxB"}
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /**
3
+ * Version Router
4
+ * Routes requests to specific service versions based on:
5
+ * - Header (X-API-Version)
6
+ * - URI prefix (/v2/api/...)
7
+ * - Query parameter (?version=v2)
8
+ * - Weighted random (percentage-based)
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.VersionRouter = void 0;
12
+ class VersionRouter {
13
+ constructor(config) {
14
+ this.config = {
15
+ strategy: config.strategy ?? 'header',
16
+ header: config.header ?? 'X-API-Version',
17
+ queryParam: config.queryParam ?? 'version',
18
+ ...config,
19
+ };
20
+ }
21
+ /**
22
+ * Resolve the target version for an incoming request
23
+ */
24
+ resolve(request) {
25
+ // 1. Try explicit version from header
26
+ if (this.config.strategy === 'header' || !this.config.strategy) {
27
+ const headerVersion = this.resolveFromHeader(request);
28
+ if (headerVersion)
29
+ return headerVersion;
30
+ }
31
+ // 2. Try explicit version from URI
32
+ if (this.config.strategy === 'uri') {
33
+ const uriVersion = this.resolveFromUri(request);
34
+ if (uriVersion)
35
+ return uriVersion;
36
+ }
37
+ // 3. Try explicit version from query
38
+ if (this.config.strategy === 'query') {
39
+ const queryVersion = this.resolveFromQuery(request);
40
+ if (queryVersion)
41
+ return queryVersion;
42
+ }
43
+ // 4. Fall back to weighted selection
44
+ const weightedVersion = this.resolveByWeight();
45
+ if (weightedVersion)
46
+ return weightedVersion;
47
+ // 5. Fall back to default version
48
+ if (this.config.defaultVersion) {
49
+ return {
50
+ version: this.config.defaultVersion,
51
+ resolvedBy: 'default',
52
+ };
53
+ }
54
+ // 6. Use the first defined version
55
+ const firstVersion = Object.keys(this.config.routes)[0];
56
+ return {
57
+ version: firstVersion,
58
+ resolvedBy: 'default',
59
+ };
60
+ }
61
+ /**
62
+ * Get the version configuration for a specific version
63
+ */
64
+ getVersionEntry(version) {
65
+ return this.config.routes[version];
66
+ }
67
+ /**
68
+ * Get all configured versions
69
+ */
70
+ getVersions() {
71
+ return Object.keys(this.config.routes);
72
+ }
73
+ // ─── Resolution Strategies ───
74
+ resolveFromHeader(request) {
75
+ const headerName = this.config.header;
76
+ const headerValue = request.headers[headerName] || request.headers[headerName.toLowerCase()];
77
+ if (headerValue && typeof headerValue === 'string') {
78
+ const version = headerValue.trim();
79
+ const entry = this.config.routes[version];
80
+ if (entry && (entry.weight > 0 || entry.allowExplicit)) {
81
+ return { version, resolvedBy: 'header' };
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ resolveFromUri(request) {
87
+ // Extract version from URI like /v2/api/users -> v2
88
+ const match = request.path.match(/^\/(v\d+)(\/.*)?$/);
89
+ if (match) {
90
+ const version = match[1];
91
+ const entry = this.config.routes[version];
92
+ if (entry && (entry.weight > 0 || entry.allowExplicit)) {
93
+ return { version, resolvedBy: 'uri' };
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+ resolveFromQuery(request) {
99
+ const paramName = this.config.queryParam;
100
+ const version = request.query?.[paramName];
101
+ if (version) {
102
+ const entry = this.config.routes[version];
103
+ if (entry && (entry.weight > 0 || entry.allowExplicit)) {
104
+ return { version, resolvedBy: 'query' };
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ resolveByWeight() {
110
+ // Collect versions with weight > 0
111
+ const weighted = [];
112
+ let totalWeight = 0;
113
+ for (const [version, entry] of Object.entries(this.config.routes)) {
114
+ if (entry.weight > 0) {
115
+ weighted.push({ version, weight: entry.weight });
116
+ totalWeight += entry.weight;
117
+ }
118
+ }
119
+ if (weighted.length === 0 || totalWeight === 0)
120
+ return null;
121
+ // Weighted random selection
122
+ let random = Math.random() * totalWeight;
123
+ for (const item of weighted) {
124
+ random -= item.weight;
125
+ if (random <= 0) {
126
+ return { version: item.version, resolvedBy: 'weight' };
127
+ }
128
+ }
129
+ // Fallback (shouldn't reach here due to float precision)
130
+ return {
131
+ version: weighted[weighted.length - 1].version,
132
+ resolvedBy: 'weight',
133
+ };
134
+ }
135
+ }
136
+ exports.VersionRouter = VersionRouter;