soajs.controller 4.3.4 → 4.3.6

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,15 @@ 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
+ │ │ └── services # Per-service idempotency config
22
+ │ │ └── [service] # Service-specific settings
23
+ │ ├── cache # GET response caching
24
+ │ │ ├── model # Storage backend (memory/mongo)
25
+ │ │ ├── defaultTTL # Default cache TTL
26
+ │ │ └── services # Per-service cache config
27
+ │ │ └── [service] # Service-specific settings
19
28
  │ ├── maintenanceMode # Gateway maintenance mode
20
29
  │ ├── lastSeen # User activity tracking
21
30
  │ └── gotoService
@@ -122,7 +131,123 @@ Configures rate limiting strategies and storage backend.
122
131
 
123
132
  ---
124
133
 
125
- ## 4. Multi-Tenant IP Whitelist
134
+ ## 4. Idempotency
135
+
136
+ **Path:** `registry.custom.gateway.value.idempotency`
137
+
138
+ **Middleware:** `mw/idempotency/index.js`
139
+
140
+ 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.
141
+
142
+ ```javascript
143
+ {
144
+ "idempotency": {
145
+ "model": "memory", // Storage: "memory" or "mongo"
146
+ "services": { // Per-service configuration
147
+ "av": { // Service name
148
+ "enabled": true, // Enable for this service
149
+ "ttl": 60000, // Key expiration (ms)
150
+ "apis": [ // APIs to protect
151
+ "POST /calls",
152
+ "PUT /call/p2p/ended",
153
+ "PUT /call/p2p/rejected"
154
+ ]
155
+ },
156
+ "payment": {
157
+ "enabled": true,
158
+ "ttl": 120000,
159
+ "apis": [
160
+ "POST /charge",
161
+ "POST /refund"
162
+ ]
163
+ }
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ | Field | Type | Default | Description |
170
+ |-------|------|---------|-------------|
171
+ | `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
172
+ | `services.[name].enabled` | boolean | `false` | Enable idempotency for this service |
173
+ | `services.[name].ttl` | number | `60000` | Key expiration time in milliseconds |
174
+ | `services.[name].apis` | string[] | `[]` | API patterns to protect (e.g., `"POST /path"`) |
175
+
176
+ **Client Usage:**
177
+ ```http
178
+ POST /av/calls HTTP/1.1
179
+ Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
180
+ ```
181
+
182
+ **Error Codes:**
183
+ - `180` (400): Invalid Idempotency-Key format (not UUID v4)
184
+ - `181` (409): Request with this key is still being processed
185
+
186
+ ---
187
+
188
+ ## 5. Response Caching
189
+
190
+ **Path:** `registry.custom.gateway.value.cache`
191
+
192
+ **Middleware:** `mw/cache/index.js`
193
+
194
+ Caches GET API responses with per-API TTL configuration. Returns `X-Cache: HIT` or `X-Cache: MISS` headers.
195
+
196
+ ```javascript
197
+ {
198
+ "cache": {
199
+ "model": "memory", // Storage: "memory" or "mongo"
200
+ "defaultTTL": 300000, // Default TTL: 5 minutes
201
+ "services": { // Per-service configuration
202
+ "av": { // Service name
203
+ "enabled": true, // Enable caching for this service
204
+ "apis": {
205
+ "GET /calls": {
206
+ "enabled": true,
207
+ "ttl": 30000 // 30 seconds
208
+ },
209
+ "GET /call/:id": {
210
+ "enabled": true,
211
+ "ttl": 60000 // 60 seconds
212
+ }
213
+ }
214
+ },
215
+ "catalog": {
216
+ "enabled": true,
217
+ "apis": {
218
+ "GET /products": {
219
+ "enabled": true,
220
+ "ttl": 600000 // 10 minutes
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ ```
228
+
229
+ | Field | Type | Default | Description |
230
+ |-------|------|---------|-------------|
231
+ | `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
232
+ | `defaultTTL` | number | `300000` | Default cache TTL in milliseconds |
233
+ | `services.[name].enabled` | boolean | `false` | Enable caching for this service |
234
+ | `services.[name].apis` | object | `{}` | API-specific cache configuration |
235
+ | `services.[name].apis[api].enabled` | boolean | `false` | Enable caching for this API |
236
+ | `services.[name].apis[api].ttl` | number | `defaultTTL` | Cache TTL for this specific API |
237
+
238
+ **Response Headers:**
239
+ - `X-Cache: HIT` - Response served from cache
240
+ - `X-Cache: MISS` - Response fetched from backend
241
+ - `X-Cache-Age: <seconds>` - Time since cached (on HIT)
242
+
243
+ **Notes:**
244
+ - Only GET requests are cached
245
+ - Only 2xx responses are cached
246
+ - Cache key includes tenant ID, service name, path, and query hash
247
+
248
+ ---
249
+
250
+ ## 6. Multi-Tenant IP Whitelist
126
251
 
127
252
  **Path:** `registry.custom.gateway.value.mt.whitelist`
128
253
 
@@ -150,7 +275,7 @@ Allows specific IP ranges to bypass ACL and/or OAuth checks.
150
275
 
151
276
  ---
152
277
 
153
- ## 5. User Activity Tracking (Last Seen)
278
+ ## 7. User Activity Tracking (Last Seen)
154
279
 
155
280
  **Path:** `registry.custom.gateway.value.lastSeen`
156
281
 
@@ -180,7 +305,7 @@ Tracks user activity by notifying a service (typically URAC) of user access.
180
305
 
181
306
  ---
182
307
 
183
- ## 6. Request Monitoring
308
+ ## 8. Request Monitoring
184
309
 
185
310
  **Path:** `registry.custom.gateway.value.gotoService.monitor`
186
311
 
@@ -208,7 +333,7 @@ Controls which services are monitored for metrics collection.
208
333
 
209
334
  ---
210
335
 
211
- ## 7. Request Timeout Renewal
336
+ ## 9. Request Timeout Renewal
212
337
 
213
338
  **Path:** `registry.custom.gateway.value.gotoService.renewReqMonitorOff`
214
339
 
@@ -230,7 +355,7 @@ Disables the heartbeat renewal mechanism for long-running requests.
230
355
 
231
356
  ---
232
357
 
233
- ## 8. OAuth Roaming (Cross-Environment)
358
+ ## 10. OAuth Roaming (Cross-Environment)
234
359
 
235
360
  **Path:** `registry.custom.oauth.value.roaming`
236
361
 
@@ -260,7 +385,7 @@ Enables cross-environment access for specific services with IP-based whitelistin
260
385
 
261
386
  ---
262
387
 
263
- ## 9. PIN Login Wrapper
388
+ ## 11. PIN Login Wrapper
264
389
 
265
390
  **Path:** `registry.custom.oauth.value.pinWrapper`
266
391
 
@@ -284,7 +409,7 @@ Defines a custom PIN-based login endpoint that bypasses standard OAuth flow.
284
409
 
285
410
  ---
286
411
 
287
- ## 10. PIN Login API Whitelist
412
+ ## 12. PIN Login API Whitelist
288
413
 
289
414
  **Path:** `registry.custom.oauth.value.pinWhitelist`
290
415
 
@@ -359,6 +484,29 @@ Whitelists specific APIs that can be accessed when a user is PIN-locked (during
359
484
  }
360
485
  }
361
486
  },
487
+ "idempotency": {
488
+ "model": "mongo",
489
+ "services": {
490
+ "payment": {
491
+ "enabled": true,
492
+ "ttl": 120000,
493
+ "apis": ["POST /charge", "POST /refund"]
494
+ }
495
+ }
496
+ },
497
+ "cache": {
498
+ "model": "mongo",
499
+ "defaultTTL": 300000,
500
+ "services": {
501
+ "catalog": {
502
+ "enabled": true,
503
+ "apis": {
504
+ "GET /products": { "enabled": true, "ttl": 600000 },
505
+ "GET /categories": { "enabled": true, "ttl": 3600000 }
506
+ }
507
+ }
508
+ }
509
+ },
362
510
  "lastSeen": {
363
511
  "active": true,
364
512
  "serviceName": "urac",
@@ -417,5 +565,7 @@ Custom registry configurations are processed in this order:
417
565
  | 4 | gotoService/preRedirect | `gotoService.renewReqMonitorOff` |
418
566
  | 5 | gotoService/redirectToService | `gotoService.monitor` |
419
567
  | 6 | mt (security checks) | `mt.whitelist`, `oauth.value.pinWrapper`, `oauth.value.pinWhitelist` |
420
- | 7 | traffic | `traffic.model`, `traffic.throttling` |
421
- | 8 | lastSeen | `lastSeen` |
568
+ | 7 | idempotency | `idempotency.model`, `idempotency.services.[name]` |
569
+ | 8 | traffic | `traffic.model`, `traffic.throttling` |
570
+ | 9 | cache | `cache.model`, `cache.defaultTTL`, `cache.services.[name]` |
571
+ | 10 | lastSeen | `lastSeen` |
@@ -0,0 +1,275 @@
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
+ "GET /profile": {
76
+ "enabled": true,
77
+ "scope": "tenant_user" // Cache per tenant + logged-in user
78
+ }
79
+ }
80
+ },
81
+ "catalog": {
82
+ "enabled": true,
83
+ "apis": {
84
+ "GET /products": {
85
+ "enabled": true,
86
+ "ttl": 600000 // 10 minutes
87
+ },
88
+ "GET /categories": {
89
+ "enabled": true,
90
+ "ttl": 3600000 // 1 hour
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ | Field | Type | Default | Description |
100
+ |-------|------|---------|-------------|
101
+ | `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
102
+ | `defaultTTL` | number | `300000` | Default cache TTL in milliseconds (5 min) |
103
+ | `services.[name].enabled` | boolean | `false` | Enable caching for this service |
104
+ | `services.[name].apis` | object | `{}` | API-specific cache configuration |
105
+ | `services.[name].apis[api].enabled` | boolean | `false` | Enable caching for this API |
106
+ | `services.[name].apis[api].ttl` | number | `defaultTTL` | Cache TTL for this API |
107
+ | `services.[name].apis[api].scope` | string | inferred | Cache scope: `"tenant"` or `"tenant_user"`. Defaults to `"tenant"` for public APIs and `"tenant_user"` for private APIs. |
108
+
109
+ ## Caching Scope (Public vs Private APIs)
110
+
111
+ The cache key dimension depends on whether the API is **public** (no `access_token` required) or **private** (requires an `access_token`):
112
+
113
+ - **Public API** → the response is the same for the whole tenant, so it is cached per **tenant** (`scope: "tenant"`). The cache key contains the tenant id only.
114
+ - **Private API** → the response can differ per logged-in user, so it is cached per **tenant + user** (`scope: "tenant_user"`). The cache key also includes the user id, preventing one user from receiving another user's cached response.
115
+
116
+ The gateway determines public vs private automatically from `req.soajs.controller.serviceParams.isAPIPublic` and resolves the default scope accordingly. You can override the scope per API with the `scope` field.
117
+
118
+ | Scope | Cache key dimension | Default for |
119
+ |-------|---------------------|-------------|
120
+ | `tenant` | tenant id | Public APIs |
121
+ | `tenant_user` | tenant id + user id | Private APIs |
122
+
123
+ ### Safety Behavior
124
+
125
+ If an API resolves to `tenant_user` scope but **no logged-in user can be resolved** (no URAC user on the request), the response is **not cached**. This prevents a user-scoped entry from being stored without a user identity and served to the wrong user. A misconfiguration therefore causes a cache miss, never a data leak.
126
+
127
+ An unrecognized `scope` value is ignored (with a warning) and falls back to the inferred default.
128
+
129
+ ## API Pattern Matching
130
+
131
+ APIs are matched using `GET /path` format with support for path parameters:
132
+
133
+ ```javascript
134
+ "apis": {
135
+ "GET /users": { ... }, // Exact match
136
+ "GET /users/:id": { ... }, // :id matches any segment
137
+ "GET /users/:userId/posts/:postId": { ... } // Multiple params
138
+ }
139
+ ```
140
+
141
+ ## Cache Key Structure
142
+
143
+ ```javascript
144
+ // Public API (scope: "tenant")
145
+ {
146
+ "l1": "tenant_id",
147
+ "l2": "serviceName:GET:/path:queryHash"
148
+ }
149
+
150
+ // Private API (scope: "tenant_user")
151
+ {
152
+ "l1": "tenant_id",
153
+ "l2": "serviceName:GET:/path:queryHash:u:user_id"
154
+ }
155
+ ```
156
+
157
+ - **tenant_id**: Isolates cache per tenant
158
+ - **serviceName**: The target service name
159
+ - **path**: The API path
160
+ - **queryHash**: MD5 hash of sorted query parameters
161
+ - **user_id**: The logged-in user id (only for `tenant_user` scope), isolating cache per user within the tenant
162
+
163
+ ### Query Parameter Handling
164
+
165
+ Query parameters are included in the cache key via MD5 hash:
166
+
167
+ ```
168
+ GET /users?page=1&limit=10 → key includes hash of {"limit":"10","page":"1"}
169
+ GET /users?limit=10&page=1 → same hash (sorted)
170
+ GET /users → different key (no query hash)
171
+ ```
172
+
173
+ ## Storage Backends
174
+
175
+ ### Memory (Default)
176
+
177
+ - Fast, in-process storage
178
+ - Lost on restart
179
+ - Good for single-instance deployments
180
+ - Automatic cleanup of expired entries
181
+
182
+ ### MongoDB
183
+
184
+ - Persistent storage
185
+ - Shared across instances
186
+ - Required for multi-instance deployments
187
+ - Uses TTL index for automatic expiration
188
+
189
+ ## Response Headers
190
+
191
+ | Header | Description |
192
+ |--------|-------------|
193
+ | `X-Cache` | `HIT` or `MISS` indicating cache status |
194
+ | `X-Cache-Age` | Seconds since response was cached (on HIT) |
195
+
196
+ ### Cache HIT
197
+
198
+ ```http
199
+ HTTP/1.1 200 OK
200
+ X-Cache: HIT
201
+ X-Cache-Age: 15
202
+ Content-Type: application/json
203
+
204
+ {"data": [...]}
205
+ ```
206
+
207
+ ### Cache MISS
208
+
209
+ ```http
210
+ HTTP/1.1 200 OK
211
+ X-Cache: MISS
212
+ Content-Type: application/json
213
+
214
+ {"data": [...]}
215
+ ```
216
+
217
+ ## Caching Behavior
218
+
219
+ ### What Gets Cached
220
+
221
+ - Only successful responses (HTTP 2xx)
222
+ - Response status code
223
+ - Response headers
224
+ - Response body
225
+
226
+ ### What Doesn't Get Cached
227
+
228
+ - Non-GET requests
229
+ - Error responses (4xx, 5xx)
230
+ - APIs not configured for caching
231
+
232
+ ## Usage Notes
233
+
234
+ - Only GET requests are cached
235
+ - Cache is tenant-isolated; private APIs are additionally user-isolated (`tenant_user` scope)
236
+ - Public APIs default to `tenant` scope, private APIs default to `tenant_user` scope
237
+ - Private (`tenant_user`) requests without a resolved user are not cached (safety)
238
+ - Query parameters affect cache key
239
+ - Only 2xx responses are cached
240
+ - Headers like `X-Cache` are not stored in cache
241
+ - MongoDB backend required for horizontal scaling
242
+ - Consider TTL based on data freshness requirements
243
+
244
+ ## Example Scenarios
245
+
246
+ ### Static Data (Long TTL)
247
+
248
+ ```javascript
249
+ "GET /catalog/categories": {
250
+ "enabled": true,
251
+ "ttl": 3600000 // 1 hour - categories rarely change
252
+ }
253
+ ```
254
+
255
+ ### Dynamic Data (Short TTL)
256
+
257
+ ```javascript
258
+ "GET /av/calls": {
259
+ "enabled": true,
260
+ "ttl": 5000 // 5 seconds - frequently updated
261
+ }
262
+ ```
263
+
264
+ ### User-Specific Data (Private API)
265
+
266
+ ```javascript
267
+ // Private API: cache key includes tenant ID AND user ID,
268
+ // so each user within a tenant has a separate cache entry.
269
+ // "tenant_user" is the default scope for private APIs, shown here explicitly.
270
+ "GET /users/profile": {
271
+ "enabled": true,
272
+ "ttl": 60000, // 1 minute
273
+ "scope": "tenant_user"
274
+ }
275
+ ```