soajs.controller 4.3.3 → 4.3.5

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/Gruntfile.js CHANGED
@@ -65,7 +65,7 @@ module.exports = function (grunt) {
65
65
  "curly": true,
66
66
  "eqeqeq": true,
67
67
  "eqnull": true,
68
- "esversion": 8,
68
+ "esversion": 9,
69
69
  "forin": true,
70
70
  "latedef": "nofunc",
71
71
  "leanswitch": true,
@@ -16,6 +16,13 @@ registry.custom
16
16
  │ │ ├── model # Throttling storage backend
17
17
  │ │ ├── ip2ban # Banned IP addresses
18
18
  │ │ └── throttling # Service-level rate limiting
19
+ │ ├── idempotency # Idempotency key configuration
20
+ │ │ ├── model # Storage backend (memory/mongo)
21
+ │ │ └── [service] # Per-service idempotency config
22
+ │ ├── cache # GET response caching
23
+ │ │ ├── model # Storage backend (memory/mongo)
24
+ │ │ ├── defaultTTL # Default cache TTL
25
+ │ │ └── [service] # Per-service cache config
19
26
  │ ├── maintenanceMode # Gateway maintenance mode
20
27
  │ ├── lastSeen # User activity tracking
21
28
  │ └── gotoService
@@ -122,7 +129,119 @@ Configures rate limiting strategies and storage backend.
122
129
 
123
130
  ---
124
131
 
125
- ## 4. Multi-Tenant IP Whitelist
132
+ ## 4. Idempotency
133
+
134
+ **Path:** `registry.custom.gateway.value.idempotency`
135
+
136
+ **Middleware:** `mw/idempotency/index.js`
137
+
138
+ Prevents duplicate write operations by tracking requests via `Idempotency-Key` headers. When a client retries a request with the same key, the cached response is returned without re-executing the operation.
139
+
140
+ ```javascript
141
+ {
142
+ "idempotency": {
143
+ "model": "memory", // Storage: "memory" or "mongo"
144
+ "av": { // Service name
145
+ "enabled": true, // Enable for this service
146
+ "ttl": 60000, // Key expiration (ms)
147
+ "apis": [ // APIs to protect
148
+ "POST /av/calls",
149
+ "PUT /av/call/p2p/ended",
150
+ "PUT /av/call/p2p/rejected"
151
+ ]
152
+ },
153
+ "payment": {
154
+ "enabled": true,
155
+ "ttl": 120000,
156
+ "apis": [
157
+ "POST /payment/charge",
158
+ "POST /payment/refund"
159
+ ]
160
+ }
161
+ }
162
+ }
163
+ ```
164
+
165
+ | Field | Type | Default | Description |
166
+ |-------|------|---------|-------------|
167
+ | `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
168
+ | `[service].enabled` | boolean | `false` | Enable idempotency for this service |
169
+ | `[service].ttl` | number | `60000` | Key expiration time in milliseconds |
170
+ | `[service].apis` | string[] | `[]` | API patterns to protect (e.g., `"POST /path"`) |
171
+
172
+ **Client Usage:**
173
+ ```http
174
+ POST /av/calls HTTP/1.1
175
+ Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
176
+ ```
177
+
178
+ **Error Codes:**
179
+ - `180` (400): Invalid Idempotency-Key format (not UUID v4)
180
+ - `181` (409): Request with this key is still being processed
181
+
182
+ ---
183
+
184
+ ## 5. Response Caching
185
+
186
+ **Path:** `registry.custom.gateway.value.cache`
187
+
188
+ **Middleware:** `mw/cache/index.js`
189
+
190
+ Caches GET API responses with per-API TTL configuration. Returns `X-Cache: HIT` or `X-Cache: MISS` headers.
191
+
192
+ ```javascript
193
+ {
194
+ "cache": {
195
+ "model": "memory", // Storage: "memory" or "mongo"
196
+ "defaultTTL": 300000, // Default TTL: 5 minutes
197
+ "av": { // Service name
198
+ "enabled": true, // Enable caching for this service
199
+ "apis": {
200
+ "GET /av/calls": {
201
+ "enabled": true,
202
+ "ttl": 30000 // 30 seconds
203
+ },
204
+ "GET /av/call/:id": {
205
+ "enabled": true,
206
+ "ttl": 60000 // 60 seconds
207
+ }
208
+ }
209
+ },
210
+ "catalog": {
211
+ "enabled": true,
212
+ "apis": {
213
+ "GET /catalog/products": {
214
+ "enabled": true,
215
+ "ttl": 600000 // 10 minutes
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ | Field | Type | Default | Description |
224
+ |-------|------|---------|-------------|
225
+ | `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
226
+ | `defaultTTL` | number | `300000` | Default cache TTL in milliseconds |
227
+ | `[service].enabled` | boolean | `false` | Enable caching for this service |
228
+ | `[service].apis` | object | `{}` | API-specific cache configuration |
229
+ | `[service].apis[api].enabled` | boolean | `false` | Enable caching for this API |
230
+ | `[service].apis[api].ttl` | number | `defaultTTL` | Cache TTL for this specific API |
231
+
232
+ **Response Headers:**
233
+ - `X-Cache: HIT` - Response served from cache
234
+ - `X-Cache: MISS` - Response fetched from backend
235
+ - `X-Cache-Age: <seconds>` - Time since cached (on HIT)
236
+
237
+ **Notes:**
238
+ - Only GET requests are cached
239
+ - Only 2xx responses are cached
240
+ - Cache key includes tenant ID, service name, path, and query hash
241
+
242
+ ---
243
+
244
+ ## 6. Multi-Tenant IP Whitelist
126
245
 
127
246
  **Path:** `registry.custom.gateway.value.mt.whitelist`
128
247
 
@@ -150,7 +269,7 @@ Allows specific IP ranges to bypass ACL and/or OAuth checks.
150
269
 
151
270
  ---
152
271
 
153
- ## 5. User Activity Tracking (Last Seen)
272
+ ## 7. User Activity Tracking (Last Seen)
154
273
 
155
274
  **Path:** `registry.custom.gateway.value.lastSeen`
156
275
 
@@ -180,7 +299,7 @@ Tracks user activity by notifying a service (typically URAC) of user access.
180
299
 
181
300
  ---
182
301
 
183
- ## 6. Request Monitoring
302
+ ## 8. Request Monitoring
184
303
 
185
304
  **Path:** `registry.custom.gateway.value.gotoService.monitor`
186
305
 
@@ -208,7 +327,7 @@ Controls which services are monitored for metrics collection.
208
327
 
209
328
  ---
210
329
 
211
- ## 7. Request Timeout Renewal
330
+ ## 9. Request Timeout Renewal
212
331
 
213
332
  **Path:** `registry.custom.gateway.value.gotoService.renewReqMonitorOff`
214
333
 
@@ -230,7 +349,7 @@ Disables the heartbeat renewal mechanism for long-running requests.
230
349
 
231
350
  ---
232
351
 
233
- ## 8. OAuth Roaming (Cross-Environment)
352
+ ## 10. OAuth Roaming (Cross-Environment)
234
353
 
235
354
  **Path:** `registry.custom.oauth.value.roaming`
236
355
 
@@ -260,7 +379,7 @@ Enables cross-environment access for specific services with IP-based whitelistin
260
379
 
261
380
  ---
262
381
 
263
- ## 9. PIN Login Wrapper
382
+ ## 11. PIN Login Wrapper
264
383
 
265
384
  **Path:** `registry.custom.oauth.value.pinWrapper`
266
385
 
@@ -284,7 +403,7 @@ Defines a custom PIN-based login endpoint that bypasses standard OAuth flow.
284
403
 
285
404
  ---
286
405
 
287
- ## 10. PIN Login API Whitelist
406
+ ## 12. PIN Login API Whitelist
288
407
 
289
408
  **Path:** `registry.custom.oauth.value.pinWhitelist`
290
409
 
@@ -359,6 +478,25 @@ Whitelists specific APIs that can be accessed when a user is PIN-locked (during
359
478
  }
360
479
  }
361
480
  },
481
+ "idempotency": {
482
+ "model": "mongo",
483
+ "payment": {
484
+ "enabled": true,
485
+ "ttl": 120000,
486
+ "apis": ["POST /payment/charge", "POST /payment/refund"]
487
+ }
488
+ },
489
+ "cache": {
490
+ "model": "mongo",
491
+ "defaultTTL": 300000,
492
+ "catalog": {
493
+ "enabled": true,
494
+ "apis": {
495
+ "GET /catalog/products": { "enabled": true, "ttl": 600000 },
496
+ "GET /catalog/categories": { "enabled": true, "ttl": 3600000 }
497
+ }
498
+ }
499
+ },
362
500
  "lastSeen": {
363
501
  "active": true,
364
502
  "serviceName": "urac",
@@ -417,5 +555,7 @@ Custom registry configurations are processed in this order:
417
555
  | 4 | gotoService/preRedirect | `gotoService.renewReqMonitorOff` |
418
556
  | 5 | gotoService/redirectToService | `gotoService.monitor` |
419
557
  | 6 | mt (security checks) | `mt.whitelist`, `oauth.value.pinWrapper`, `oauth.value.pinWhitelist` |
420
- | 7 | traffic | `traffic.model`, `traffic.throttling` |
421
- | 8 | lastSeen | `lastSeen` |
558
+ | 7 | idempotency | `idempotency.model`, `idempotency.[service]` |
559
+ | 8 | traffic | `traffic.model`, `traffic.throttling` |
560
+ | 9 | cache | `cache.model`, `cache.defaultTTL`, `cache.[service]` |
561
+ | 10 | lastSeen | `lastSeen` |
@@ -0,0 +1,237 @@
1
+ # Cache Middleware
2
+
3
+ **File:** `mw/cache/index.js`
4
+
5
+ **Order:** 19 (after Traffic middleware)
6
+
7
+ The Cache middleware provides response caching for GET APIs with per-API TTL configuration.
8
+
9
+ ## Purpose
10
+
11
+ - Caches GET API responses to reduce backend load
12
+ - Supports per-API TTL configuration
13
+ - Provides cache hit/miss headers for debugging
14
+ - Supports memory and MongoDB storage backends
15
+
16
+ ## Flow
17
+
18
+ ```
19
+ Incoming Request
20
+ │
21
+ ▼
22
+ ┌─────────────────────────┐
23
+ │ Check Method │ GET request?
24
+ └────────┬────────────────┘
25
+ │
26
+ ├─── Not GET ──▶ next()
27
+ │
28
+ ▼
29
+ ┌─────────────────────────┐
30
+ │ Check Configuration │ Service enabled? API matches?
31
+ └────────┬────────────────┘
32
+ │
33
+ ├─── Not configured ──▶ next()
34
+ │
35
+ ▼
36
+ ┌─────────────────────────┐
37
+ │ Generate Cache Key │ tenant:service:path:queryHash
38
+ └────────┬────────────────┘
39
+ │
40
+ ▼
41
+ ┌─────────────────────────┐
42
+ │ Lookup Cache │
43
+ └────────┬────────────────┘
44
+ │
45
+ ├─── HIT ──▶ Return cached response (X-Cache: HIT)
46
+ │
47
+ └─── MISS ──▶ Execute → Cache response → Return (X-Cache: MISS)
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ **Path:** `registry.custom.gateway.value.cache`
53
+
54
+ ```javascript
55
+ {
56
+ "cache": {
57
+ "model": "memory", // Storage: "memory" or "mongo"
58
+ "defaultTTL": 300000, // Default TTL: 5 minutes
59
+ "services": { // Service configurations
60
+ "av": { // Service name
61
+ "enabled": true, // Enable caching for this service
62
+ "apis": {
63
+ "GET /calls": {
64
+ "enabled": true,
65
+ "ttl": 30000 // 30 seconds
66
+ },
67
+ "GET /call/:id": {
68
+ "enabled": true,
69
+ "ttl": 60000 // 60 seconds
70
+ },
71
+ "GET /users": {
72
+ "enabled": true
73
+ // Uses defaultTTL (300000ms)
74
+ }
75
+ }
76
+ },
77
+ "catalog": {
78
+ "enabled": true,
79
+ "apis": {
80
+ "GET /products": {
81
+ "enabled": true,
82
+ "ttl": 600000 // 10 minutes
83
+ },
84
+ "GET /categories": {
85
+ "enabled": true,
86
+ "ttl": 3600000 // 1 hour
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ | Field | Type | Default | Description |
96
+ |-------|------|---------|-------------|
97
+ | `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
98
+ | `defaultTTL` | number | `300000` | Default cache TTL in milliseconds (5 min) |
99
+ | `services.[name].enabled` | boolean | `false` | Enable caching for this service |
100
+ | `services.[name].apis` | object | `{}` | API-specific cache configuration |
101
+ | `services.[name].apis[api].enabled` | boolean | `false` | Enable caching for this API |
102
+ | `services.[name].apis[api].ttl` | number | `defaultTTL` | Cache TTL for this API |
103
+
104
+ ## API Pattern Matching
105
+
106
+ APIs are matched using `GET /path` format with support for path parameters:
107
+
108
+ ```javascript
109
+ "apis": {
110
+ "GET /users": { ... }, // Exact match
111
+ "GET /users/:id": { ... }, // :id matches any segment
112
+ "GET /users/:userId/posts/:postId": { ... } // Multiple params
113
+ }
114
+ ```
115
+
116
+ ## Cache Key Structure
117
+
118
+ ```javascript
119
+ {
120
+ "l1": "tenant_id",
121
+ "l2": "serviceName:GET:/path:queryHash"
122
+ }
123
+ ```
124
+
125
+ - **tenant_id**: Isolates cache per tenant
126
+ - **serviceName**: The target service name
127
+ - **path**: The API path
128
+ - **queryHash**: MD5 hash of sorted query parameters
129
+
130
+ ### Query Parameter Handling
131
+
132
+ Query parameters are included in the cache key via MD5 hash:
133
+
134
+ ```
135
+ GET /users?page=1&limit=10 → key includes hash of {"limit":"10","page":"1"}
136
+ GET /users?limit=10&page=1 → same hash (sorted)
137
+ GET /users → different key (no query hash)
138
+ ```
139
+
140
+ ## Storage Backends
141
+
142
+ ### Memory (Default)
143
+
144
+ - Fast, in-process storage
145
+ - Lost on restart
146
+ - Good for single-instance deployments
147
+ - Automatic cleanup of expired entries
148
+
149
+ ### MongoDB
150
+
151
+ - Persistent storage
152
+ - Shared across instances
153
+ - Required for multi-instance deployments
154
+ - Uses TTL index for automatic expiration
155
+
156
+ ## Response Headers
157
+
158
+ | Header | Description |
159
+ |--------|-------------|
160
+ | `X-Cache` | `HIT` or `MISS` indicating cache status |
161
+ | `X-Cache-Age` | Seconds since response was cached (on HIT) |
162
+
163
+ ### Cache HIT
164
+
165
+ ```http
166
+ HTTP/1.1 200 OK
167
+ X-Cache: HIT
168
+ X-Cache-Age: 15
169
+ Content-Type: application/json
170
+
171
+ {"data": [...]}
172
+ ```
173
+
174
+ ### Cache MISS
175
+
176
+ ```http
177
+ HTTP/1.1 200 OK
178
+ X-Cache: MISS
179
+ Content-Type: application/json
180
+
181
+ {"data": [...]}
182
+ ```
183
+
184
+ ## Caching Behavior
185
+
186
+ ### What Gets Cached
187
+
188
+ - Only successful responses (HTTP 2xx)
189
+ - Response status code
190
+ - Response headers
191
+ - Response body
192
+
193
+ ### What Doesn't Get Cached
194
+
195
+ - Non-GET requests
196
+ - Error responses (4xx, 5xx)
197
+ - APIs not configured for caching
198
+
199
+ ## Usage Notes
200
+
201
+ - Only GET requests are cached
202
+ - Cache is tenant-isolated
203
+ - Query parameters affect cache key
204
+ - Only 2xx responses are cached
205
+ - Headers like `X-Cache` are not stored in cache
206
+ - MongoDB backend required for horizontal scaling
207
+ - Consider TTL based on data freshness requirements
208
+
209
+ ## Example Scenarios
210
+
211
+ ### Static Data (Long TTL)
212
+
213
+ ```javascript
214
+ "GET /catalog/categories": {
215
+ "enabled": true,
216
+ "ttl": 3600000 // 1 hour - categories rarely change
217
+ }
218
+ ```
219
+
220
+ ### Dynamic Data (Short TTL)
221
+
222
+ ```javascript
223
+ "GET /av/calls": {
224
+ "enabled": true,
225
+ "ttl": 5000 // 5 seconds - frequently updated
226
+ }
227
+ ```
228
+
229
+ ### User-Specific Data
230
+
231
+ ```javascript
232
+ // Cache key includes tenant ID, so each tenant has separate cache
233
+ "GET /users/profile": {
234
+ "enabled": true,
235
+ "ttl": 60000 // 1 minute
236
+ }
237
+ ```
@@ -0,0 +1,209 @@
1
+ # Idempotency Middleware
2
+
3
+ **File:** `mw/idempotency/index.js`
4
+
5
+ **Order:** 17 (after MT middleware)
6
+
7
+ The Idempotency middleware prevents duplicate write operations by tracking requests via `Idempotency-Key` headers.
8
+
9
+ ## Purpose
10
+
11
+ - Prevents duplicate side effects from retried requests
12
+ - Stores and replays responses for completed requests
13
+ - Returns 409 Conflict for in-flight duplicate requests
14
+ - Supports memory and MongoDB storage backends
15
+
16
+ ## Flow
17
+
18
+ ```
19
+ Incoming Request
20
+ │
21
+ ▼
22
+ ┌─────────────────────────┐
23
+ │ Check Idempotency-Key │ Header present?
24
+ └────────┬────────────────┘
25
+ │
26
+ ├─── Missing ──▶ next() (backwards compatible)
27
+ │
28
+ ▼
29
+ ┌─────────────────────────┐
30
+ │ Validate UUID v4 │ Format check
31
+ └────────┬────────────────┘
32
+ │
33
+ ├─── Invalid ──▶ 400 Bad Request (code: 180)
34
+ │
35
+ ▼
36
+ ┌─────────────────────────┐
37
+ │ Check Configuration │ Service enabled? API matches?
38
+ └────────┬────────────────┘
39
+ │
40
+ ├─── Not configured ──▶ next()
41
+ │
42
+ ▼
43
+ ┌─────────────────────────┐
44
+ │ Lookup Existing Key │ Check store
45
+ └────────┬────────────────┘
46
+ │
47
+ ├─── Found (completed) ──▶ Return cached response
48
+ │
49
+ ├─── Found (in_flight) ──▶ 409 Conflict (code: 181)
50
+ │
51
+ └─── Not found ──▶ Lock key → Execute → Store response
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ **Path:** `registry.custom.gateway.value.idempotency`
57
+
58
+ ```javascript
59
+ {
60
+ "idempotency": {
61
+ "model": "memory", // Storage: "memory" or "mongo"
62
+ "services": { // Service configurations
63
+ "av": { // Service name
64
+ "enabled": true, // Enable for this service
65
+ "ttl": 60000, // Time-to-live in ms (default: 60s)
66
+ "apis": [ // APIs to protect
67
+ "POST /calls",
68
+ "PUT /call/p2p/ended",
69
+ "PUT /call/p2p/rejected"
70
+ ]
71
+ },
72
+ "payment": {
73
+ "enabled": true,
74
+ "ttl": 120000,
75
+ "apis": [
76
+ "POST /charge",
77
+ "POST /refund"
78
+ ]
79
+ }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ | Field | Type | Default | Description |
86
+ |-------|------|---------|-------------|
87
+ | `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
88
+ | `services.[name].enabled` | boolean | `false` | Enable idempotency for this service |
89
+ | `services.[name].ttl` | number | `60000` | Key expiration time in milliseconds |
90
+ | `services.[name].apis` | string[] | `[]` | API patterns to protect (e.g., `"POST /path"`) |
91
+
92
+ ## API Pattern Matching
93
+
94
+ APIs are matched using `METHOD /path` format with support for path parameters:
95
+
96
+ ```javascript
97
+ "apis": [
98
+ "POST /av/calls", // Exact match
99
+ "PUT /av/call/:id/ended", // :id matches any segment
100
+ "DELETE /users/:userId/posts/:postId" // Multiple params
101
+ ]
102
+ ```
103
+
104
+ ## Storage Backends
105
+
106
+ ### Memory (Default)
107
+
108
+ - Fast, in-process storage
109
+ - Lost on restart
110
+ - Good for single-instance deployments
111
+ - Automatic cleanup of expired entries
112
+
113
+ ### MongoDB
114
+
115
+ - Persistent storage
116
+ - Shared across instances
117
+ - Required for multi-instance deployments
118
+ - Uses TTL index for automatic expiration
119
+
120
+ ## Client Usage
121
+
122
+ Clients must include the `Idempotency-Key` header with a UUID v4:
123
+
124
+ ```http
125
+ POST /av/calls HTTP/1.1
126
+ Host: api.example.com
127
+ Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
128
+ Content-Type: application/json
129
+
130
+ {"caller": "user1", "callee": "user2"}
131
+ ```
132
+
133
+ ## Response Scenarios
134
+
135
+ ### New Request (First Time)
136
+
137
+ ```
138
+ Request processed normally, response cached
139
+ ```
140
+
141
+ ### Replay (Completed)
142
+
143
+ ```
144
+ HTTP/1.1 200 OK
145
+ (Cached response body returned, no side effects)
146
+ ```
147
+
148
+ ### Replay (In-Flight)
149
+
150
+ ```
151
+ HTTP/1.1 409 Conflict
152
+ Content-Type: application/json
153
+
154
+ {
155
+ "result": false,
156
+ "errors": {
157
+ "codes": [181],
158
+ "details": [{
159
+ "code": 181,
160
+ "message": "Request with this Idempotency-Key is still being processed."
161
+ }]
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### Invalid Key Format
167
+
168
+ ```
169
+ HTTP/1.1 400 Bad Request
170
+ Content-Type: application/json
171
+
172
+ {
173
+ "result": false,
174
+ "errors": {
175
+ "codes": [180],
176
+ "details": [{
177
+ "code": 180,
178
+ "message": "Invalid Idempotency-Key format. Expected UUID v4."
179
+ }]
180
+ }
181
+ }
182
+ ```
183
+
184
+ ## Key Structure
185
+
186
+ ```javascript
187
+ {
188
+ "l1": "tenant_id", // Tenant isolation
189
+ "l2": "idempotency_key" // Client-provided UUID
190
+ }
191
+ ```
192
+
193
+ Keys are isolated per tenant to prevent cross-tenant collisions.
194
+
195
+ ## Error Codes
196
+
197
+ | Code | HTTP Status | Description |
198
+ |------|-------------|-------------|
199
+ | 180 | 400 | Invalid Idempotency-Key format (not UUID v4) |
200
+ | 181 | 409 | Request with this key is still being processed |
201
+
202
+ ## Usage Notes
203
+
204
+ - Only applies to non-GET requests (POST, PUT, DELETE, PATCH)
205
+ - GET requests always pass through (idempotent by nature)
206
+ - Keys expire after TTL (default 60 seconds)
207
+ - Clients should generate a new UUID for each unique operation
208
+ - Same UUID = same operation (for retries)
209
+ - MongoDB backend required for horizontal scaling