@connectid-tools/rp-nodejs-sdk 5.0.1 → 5.1.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/README.md CHANGED
@@ -120,6 +120,85 @@ const rpClient = new RelyingPartyClientSdk(config)
120
120
  | `include_uncertified_participants` | By default the SDK will filter out all authorisation servers that are not fully certified. If you wish to test one of the uncertified auth servers you will need to set this to `true`. If not provided, defaults to 'false' | `false` |
121
121
  | `required_claims` | The list of claims that the RP will be using and requires IDPs to support. If supplied, this will be used to filter the list of IDPs returned from `getParticipants` so that only IDPs supporting the claims are returned. If this value is not supplied, no filtering by claim support will be performed. | `['name', 'address']` |
122
122
  | `required_participant_certifications` | The list of required certifications a server must support for the IDP use case (eg: TDIF Certification). If supplied, this will be used to filter the list of IDPs returned from `getParticipants` so that only IDPs with the certification are returned. If this value is not supplied, no filtering for specific certifications will be performed. | `[{ profileType: 'TDIF Accreditation', profileVariant: 'Identity Provider'}]` |
123
+ | `http_cache` | Optional configuration for HTTP response caching. When enabled, the SDK caches responses from participant registry, discovery documents, and JWKS endpoints to improve performance. All properties are optional with sensible defaults. | `{ enabled: true, ttl_minutes: 10, max_entries: 100, max_element_size_bytes: 5242880 }` |
124
+ | `http_cache.enabled` | Enable or disable HTTP response caching. Default: `true` | `true` |
125
+ | `http_cache.ttl_minutes` | Time-to-live for cached entries in minutes. Entries expire after this duration. Default: `10` | `15` |
126
+ | `http_cache.max_entries` | Maximum number of entries to store in the cache. When exceeded, least recently used entries are evicted. Default: `100` | `50` |
127
+ | `http_cache.max_element_size_bytes` | Maximum size in bytes for a single cached response. Responses larger than this will not be cached. Default: `5242880` (5MB) | `1048576` |
128
+
129
+ # HTTP Response Cache
130
+
131
+ The SDK includes an in-memory HTTP response cache that improves performance by reducing redundant network calls to stable endpoints. The cache is enabled by default with sensible configuration values.
132
+
133
+ ## Cached Endpoints
134
+
135
+ The following endpoint types are automatically cached:
136
+
137
+ 1. **Participant Registry** (`/participants`) - List of identity providers
138
+ 2. **OIDC Discovery Documents** (`/.well-known/openid-configuration`) - OpenID Connect configuration
139
+ 3. **JWKS Endpoints** - JSON Web Key Sets for token validation
140
+
141
+ ## Cache Behavior
142
+
143
+ - **Cache-Aside Pattern**: The SDK checks the cache before making HTTP requests. On cache miss or stale entry, it fetches from the network and caches the response.
144
+ - **Only Successful Responses**: Only HTTP 2xx responses are cached. Error responses (4xx, 5xx) and network failures are never cached.
145
+ - **LRU Eviction**: When the cache reaches `max_entries`, the least recently used entries are automatically evicted.
146
+ - **TTL Expiration**: Cached entries expire after `ttl_minutes` and are refreshed on the next access.
147
+
148
+ ## Configuration
149
+
150
+ The cache can be configured or disabled using the `http_cache` configuration option:
151
+
152
+ ```typescript
153
+ const sdk = new RelyingPartyClientSdk({
154
+ data: {
155
+ // ... other config ...
156
+
157
+ // Cache enabled with custom settings
158
+ http_cache: {
159
+ enabled: true,
160
+ ttl_minutes: 15, // Cache entries for 15 minutes
161
+ max_entries: 50, // Store up to 50 entries
162
+ max_element_size_bytes: 1048576 // Max 1MB per entry
163
+ }
164
+ }
165
+ })
166
+ ```
167
+
168
+ To disable caching entirely:
169
+
170
+ ```typescript
171
+ const sdk = new RelyingPartyClientSdk({
172
+ data: {
173
+ // ... other config ...
174
+ http_cache: { enabled: false }
175
+ }
176
+ })
177
+ ```
178
+
179
+ ## Default Configuration
180
+
181
+ If `http_cache` is not specified, the SDK uses the following defaults:
182
+
183
+ - `enabled`: `true`
184
+ - `ttl_minutes`: `10`
185
+ - `max_entries`: `100`
186
+ - `max_element_size_bytes`: `5242880` (5MB)
187
+
188
+ ## Performance Benefits
189
+
190
+ - **Reduced Latency**: Cached responses are returned instantly without network round-trips
191
+ - **Lower Network Load**: Fewer HTTP requests to registry and authorization servers
192
+ - **Improved Reliability**: Cached data remains available even if network requests fail
193
+ - **Better User Experience**: Faster response times for repeated operations
194
+
195
+ ## Memory Usage
196
+
197
+ With default settings:
198
+ - Maximum entries: 100
199
+ - Maximum size per entry: 5MB
200
+ - Worst-case memory usage: ~500MB (if all entries are at maximum size)
201
+ - Typical usage: Much lower (~5-10MB) as registry responses average 50KB, discovery documents ~5KB, and JWKS ~2KB
123
202
 
124
203
  # Process Overview Sequence Diagram
125
204
 
@@ -389,6 +468,53 @@ The required function parameters are:
389
468
 
390
469
  # Release Notes
391
470
 
471
+ ### 5.1.0 (Feb 12, 2026)
472
+
473
+ **HTTP Response Cache Implementation**
474
+
475
+ This release adds HTTP response caching to improve SDK performance by reducing redundant network calls to stable endpoints.
476
+
477
+ **New Features:**
478
+ - **In-Memory HTTP Response Cache**: Implements LRU (Least Recently Used) cache with TTL (Time-To-Live) expiration for HTTP responses
479
+ - **Cached Endpoints**:
480
+ - Participant registry (`/participants`)
481
+ - OIDC discovery documents (`/.well-known/openid-configuration`)
482
+ - JWKS endpoints (JSON Web Key Sets)
483
+ - **Configurable Cache Settings**: Optional `http_cache` configuration with the following parameters:
484
+ - `enabled` (default: `true`) - Enable/disable caching globally
485
+ - `ttl_minutes` (default: `10`) - Time-to-live for cache entries in minutes
486
+ - `max_entries` (default: `100`) - Maximum number of cached entries before eviction
487
+ - `max_element_size_bytes` (default: `5242880` / 5MB) - Maximum size per cached entry
488
+
489
+ **Performance Benefits:**
490
+ - Reduces network latency for repeated operations
491
+ - Improves response times for common SDK operations
492
+ - Reduces load on registry and authorization servers
493
+
494
+ **Cache Behavior:**
495
+ - Uses cache-aside pattern: checks cache before making HTTP requests
496
+ - Only caches successful responses (HTTP 2xx status codes)
497
+ - Never caches error responses (4xx, 5xx, network failures)
498
+ - LRU eviction automatically removes oldest entries when cache is full
499
+ - Entries automatically expire based on TTL configuration
500
+
501
+ **Configuration Example:**
502
+ ```typescript
503
+ const sdk = new RelyingPartyClientSdk({
504
+ data: {
505
+ // ... other config ...
506
+ http_cache: {
507
+ enabled: true,
508
+ ttl_minutes: 15,
509
+ max_entries: 50,
510
+ max_element_size_bytes: 1048576 // 1MB
511
+ }
512
+ }
513
+ })
514
+ ```
515
+
516
+ **Note:** The cache is enabled by default with sensible defaults. To disable caching, set `http_cache: { enabled: false }` in your configuration.
517
+
392
518
  ### 5.0.1 (Jan 16, 2026)
393
519
 
394
520
  * Fixed packaging structure which caused conflicts when including the library.
@@ -0,0 +1,83 @@
1
+ import { Logger } from 'winston';
2
+ /**
3
+ * Configuration for HTTP Response Cache
4
+ */
5
+ export interface HttpCacheConfig {
6
+ enabled: boolean;
7
+ ttlMinutes: number;
8
+ maxEntries: number;
9
+ maxElementSizeBytes: number;
10
+ }
11
+ /**
12
+ * HTTP Response Cache
13
+ *
14
+ * In-memory LRU cache with TTL expiration for HTTP responses.
15
+ * Thread-safe for Node.js single-threaded event loop.
16
+ *
17
+ * Features:
18
+ * - LRU (Least Recently Used) eviction when maxEntries exceeded
19
+ * - TTL (Time-To-Live) expiration with lazy staleness checking
20
+ * - Size limits with oversized entry rejection
21
+ * - Configurable enable/disable
22
+ */
23
+ export declare class HttpResponseCache {
24
+ private readonly cache;
25
+ private readonly config;
26
+ private readonly logger;
27
+ constructor(config: HttpCacheConfig, logger: Logger);
28
+ /**
29
+ * Retrieves cached response for a given URL.
30
+ *
31
+ * @param url - The URL key to lookup
32
+ * @returns Cached content if entry exists and is fresh, undefined otherwise
33
+ */
34
+ get(url: string): string | undefined;
35
+ /**
36
+ * Stores HTTP response in cache.
37
+ *
38
+ * @param url - The URL key for caching
39
+ * @param content - The HTTP response body
40
+ */
41
+ put(url: string, content: string): void;
42
+ /**
43
+ * Checks if a cached entry exists and is stale.
44
+ *
45
+ * @param url - The URL key to check
46
+ * @returns true if entry exists and is stale, false otherwise
47
+ */
48
+ isStale(url: string): boolean;
49
+ /**
50
+ * Removes specific entry from cache.
51
+ *
52
+ * @param url - The URL key to remove
53
+ */
54
+ evict(url: string): void;
55
+ /**
56
+ * Removes all entries from cache.
57
+ */
58
+ clear(): void;
59
+ /**
60
+ * Gets current number of entries in cache.
61
+ *
62
+ * @returns Number of entries currently in cache
63
+ */
64
+ size(): number;
65
+ /**
66
+ * Checks if caching is enabled.
67
+ *
68
+ * @returns true if caching is enabled, false otherwise
69
+ */
70
+ isEnabled(): boolean;
71
+ /**
72
+ * Checks if a cache entry is stale based on TTL.
73
+ *
74
+ * @param entry - Cache entry to check
75
+ * @returns true if entry is stale, false if fresh
76
+ */
77
+ private isEntryStale;
78
+ /**
79
+ * Evicts the least recently used entry from cache.
80
+ * Map maintains insertion order, so the first entry is the LRU.
81
+ */
82
+ private evictLRU;
83
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * HTTP Response Cache
3
+ *
4
+ * In-memory LRU cache with TTL expiration for HTTP responses.
5
+ * Thread-safe for Node.js single-threaded event loop.
6
+ *
7
+ * Features:
8
+ * - LRU (Least Recently Used) eviction when maxEntries exceeded
9
+ * - TTL (Time-To-Live) expiration with lazy staleness checking
10
+ * - Size limits with oversized entry rejection
11
+ * - Configurable enable/disable
12
+ */
13
+ export class HttpResponseCache {
14
+ constructor(config, logger) {
15
+ this.config = config;
16
+ this.logger = logger;
17
+ this.cache = new Map();
18
+ this.logger.debug(`HTTP cache initialized: enabled=${config.enabled}, ttlMinutes=${config.ttlMinutes}, maxEntries=${config.maxEntries}, maxElementSize=${config.maxElementSizeBytes}`);
19
+ }
20
+ /**
21
+ * Retrieves cached response for a given URL.
22
+ *
23
+ * @param url - The URL key to lookup
24
+ * @returns Cached content if entry exists and is fresh, undefined otherwise
25
+ */
26
+ get(url) {
27
+ if (!this.config.enabled) {
28
+ return undefined;
29
+ }
30
+ const entry = this.cache.get(url);
31
+ if (!entry) {
32
+ this.logger.debug(`Cache miss for URL: ${url}`);
33
+ return undefined;
34
+ }
35
+ // Check if entry is stale
36
+ if (this.isEntryStale(entry)) {
37
+ this.logger.debug(`Cache entry stale for URL: ${url}`);
38
+ return undefined;
39
+ }
40
+ // Cache hit - promote entry in LRU order by deleting and re-inserting
41
+ this.cache.delete(url);
42
+ this.cache.set(url, entry);
43
+ this.logger.debug(`Cache hit for URL: ${url}`);
44
+ return entry.content;
45
+ }
46
+ /**
47
+ * Stores HTTP response in cache.
48
+ *
49
+ * @param url - The URL key for caching
50
+ * @param content - The HTTP response body
51
+ */
52
+ put(url, content) {
53
+ if (!this.config.enabled) {
54
+ return;
55
+ }
56
+ // Check content size
57
+ const sizeBytes = Buffer.byteLength(content, 'utf8');
58
+ if (sizeBytes > this.config.maxElementSizeBytes) {
59
+ this.logger.warn(`Response too large to cache: ${sizeBytes} bytes (max: ${this.config.maxElementSizeBytes}), URL: ${url}`);
60
+ return;
61
+ }
62
+ // Create cache entry
63
+ const entry = {
64
+ content,
65
+ fetchedAt: Date.now(),
66
+ sizeBytes,
67
+ };
68
+ // Remove existing entry if present (to update position in LRU order)
69
+ this.cache.delete(url);
70
+ // Store entry (added to end of Map, which is most recently used)
71
+ this.cache.set(url, entry);
72
+ this.logger.debug(`Cached response for URL: ${url}, size: ${sizeBytes} bytes`);
73
+ // Evict LRU entry if cache size exceeds maxEntries
74
+ if (this.cache.size > this.config.maxEntries) {
75
+ this.evictLRU();
76
+ }
77
+ }
78
+ /**
79
+ * Checks if a cached entry exists and is stale.
80
+ *
81
+ * @param url - The URL key to check
82
+ * @returns true if entry exists and is stale, false otherwise
83
+ */
84
+ isStale(url) {
85
+ if (!this.config.enabled) {
86
+ return false;
87
+ }
88
+ const entry = this.cache.get(url);
89
+ if (!entry) {
90
+ return false;
91
+ }
92
+ return this.isEntryStale(entry);
93
+ }
94
+ /**
95
+ * Removes specific entry from cache.
96
+ *
97
+ * @param url - The URL key to remove
98
+ */
99
+ evict(url) {
100
+ if (!this.config.enabled) {
101
+ return;
102
+ }
103
+ const deleted = this.cache.delete(url);
104
+ if (deleted) {
105
+ this.logger.debug(`Evicted cache entry for URL: ${url}`);
106
+ }
107
+ }
108
+ /**
109
+ * Removes all entries from cache.
110
+ */
111
+ clear() {
112
+ if (!this.config.enabled) {
113
+ return;
114
+ }
115
+ this.cache.clear();
116
+ this.logger.debug('Cache cleared');
117
+ }
118
+ /**
119
+ * Gets current number of entries in cache.
120
+ *
121
+ * @returns Number of entries currently in cache
122
+ */
123
+ size() {
124
+ return this.cache.size;
125
+ }
126
+ /**
127
+ * Checks if caching is enabled.
128
+ *
129
+ * @returns true if caching is enabled, false otherwise
130
+ */
131
+ isEnabled() {
132
+ return this.config.enabled;
133
+ }
134
+ /**
135
+ * Checks if a cache entry is stale based on TTL.
136
+ *
137
+ * @param entry - Cache entry to check
138
+ * @returns true if entry is stale, false if fresh
139
+ */
140
+ isEntryStale(entry) {
141
+ const now = Date.now();
142
+ const expiryTime = entry.fetchedAt + this.config.ttlMinutes * 60 * 1000;
143
+ return now >= expiryTime;
144
+ }
145
+ /**
146
+ * Evicts the least recently used entry from cache.
147
+ * Map maintains insertion order, so the first entry is the LRU.
148
+ */
149
+ evictLRU() {
150
+ // Get first entry (oldest in insertion order)
151
+ const firstKey = this.cache.keys().next().value;
152
+ if (firstKey) {
153
+ this.cache.delete(firstKey);
154
+ this.logger.debug(`Evicted cache entry for URL: ${firstKey}`);
155
+ }
156
+ }
157
+ }
@@ -2,11 +2,12 @@ import { Agent } from 'undici';
2
2
  import { Logger } from 'winston';
3
3
  import { AuthorisationServer, Participant, RelyingPartyClientSdkConfig } from '../types.js';
4
4
  import ParticipantFilters from '../filter/participant-filters.js';
5
+ import { HttpResponseCache } from '../cache/http-response-cache.js';
5
6
  /**
6
7
  * Participants Endpoint
7
8
  *
8
9
  * Handles fetching and filtering of participant lists from the registry.
9
- * Does NOT cache - fetches fresh data on every call
10
+ * Uses HTTP cache for performance optimization.
10
11
  */
11
12
  export declare class ParticipantsEndpoint {
12
13
  private readonly sdkConfig;
@@ -14,7 +15,8 @@ export declare class ParticipantsEndpoint {
14
15
  private readonly httpClient;
15
16
  private readonly logger;
16
17
  private readonly getCurrentDate;
17
- constructor(sdkConfig: RelyingPartyClientSdkConfig, participantFilters: ParticipantFilters, httpClient: Agent, logger: Logger, getCurrentDate: () => Date);
18
+ private readonly cache;
19
+ constructor(sdkConfig: RelyingPartyClientSdkConfig, participantFilters: ParticipantFilters, httpClient: Agent, logger: Logger, getCurrentDate: () => Date, cache: HttpResponseCache);
18
20
  /**
19
21
  * Retrieves the list of active participants.
20
22
  *
@@ -40,6 +42,7 @@ export declare class ParticipantsEndpoint {
40
42
  getFallbackProviderParticipants(): Promise<Participant[]>;
41
43
  /**
42
44
  * Fetches participants from the registry.
45
+ * Uses cache-aside pattern for performance optimization.
43
46
  *
44
47
  * @param uri - Registry participants URI
45
48
  * @returns Raw list of participants
@@ -4,15 +4,16 @@ import { randomUUID } from 'node:crypto';
4
4
  * Participants Endpoint
5
5
  *
6
6
  * Handles fetching and filtering of participant lists from the registry.
7
- * Does NOT cache - fetches fresh data on every call
7
+ * Uses HTTP cache for performance optimization.
8
8
  */
9
9
  export class ParticipantsEndpoint {
10
- constructor(sdkConfig, participantFilters, httpClient, logger, getCurrentDate) {
10
+ constructor(sdkConfig, participantFilters, httpClient, logger, getCurrentDate, cache) {
11
11
  this.sdkConfig = sdkConfig;
12
12
  this.participantFilters = participantFilters;
13
13
  this.httpClient = httpClient;
14
14
  this.logger = logger;
15
15
  this.getCurrentDate = getCurrentDate;
16
+ this.cache = cache;
16
17
  }
17
18
  /**
18
19
  * Retrieves the list of active participants.
@@ -90,18 +91,34 @@ export class ParticipantsEndpoint {
90
91
  }
91
92
  /**
92
93
  * Fetches participants from the registry.
94
+ * Uses cache-aside pattern for performance optimization.
93
95
  *
94
96
  * @param uri - Registry participants URI
95
97
  * @returns Raw list of participants
96
98
  */
97
99
  async fetchParticipants(uri) {
98
100
  try {
101
+ // Check cache first
102
+ const cachedContent = this.cache.get(uri);
103
+ if (cachedContent) {
104
+ this.logger.debug(`Cache hit for URL: ${uri}`);
105
+ return JSON.parse(cachedContent);
106
+ }
107
+ this.logger.debug(`Cache miss for URL: ${uri}`);
108
+ // Fetch from network
99
109
  const response = await HttpClientExtensions.get(uri, {
100
110
  agent: this.httpClient,
101
111
  clientId: this.sdkConfig.data.client_id,
102
112
  xFapiInteractionId: randomUUID(),
103
113
  });
104
- return await HttpClientExtensions.parseJsonResponse(response);
114
+ const participants = await HttpClientExtensions.parseJsonResponse(response);
115
+ // Cache successful response (only 2xx)
116
+ if (response.status >= 200 && response.status < 300) {
117
+ const content = JSON.stringify(participants);
118
+ this.cache.put(uri, content);
119
+ this.logger.debug(`Cached response for URL: ${uri}, size: ${Buffer.byteLength(content, 'utf8')} bytes`);
120
+ }
121
+ return participants;
105
122
  }
106
123
  catch (error) {
107
124
  throw new Error(`Failed to fetch participants from ${uri}: ${error instanceof Error ? error.message : String(error)}`);
@@ -2,6 +2,7 @@ import { Agent } from 'undici';
2
2
  import { Logger } from 'winston';
3
3
  import { RelyingPartyClientSdkConfig } from '../types.js';
4
4
  import { JwtHelper } from '../crypto/jwt-helper.js';
5
+ import { HttpResponseCache } from '../cache/http-response-cache.js';
5
6
  import { ParticipantsEndpoint } from './participants-endpoint';
6
7
  /**
7
8
  * Response from the Pushed Authorization Request endpoint.
@@ -40,8 +41,9 @@ export declare class PushedAuthorisationRequestEndpoint {
40
41
  private readonly jwtHelper;
41
42
  private readonly logger;
42
43
  private readonly participantsEndpoint;
44
+ private readonly cache;
43
45
  private static readonly EXTENDED_CLAIMS;
44
- constructor(sdkConfig: RelyingPartyClientSdkConfig, httpClient: Agent, jwtHelper: JwtHelper, logger: Logger, participantsEndpoint: ParticipantsEndpoint);
46
+ constructor(sdkConfig: RelyingPartyClientSdkConfig, httpClient: Agent, jwtHelper: JwtHelper, logger: Logger, participantsEndpoint: ParticipantsEndpoint, cache: HttpResponseCache);
45
47
  /**
46
48
  * Sends a Pushed Authorization Request to the authorization server.
47
49
  *
@@ -10,12 +10,13 @@ import { generateXFapiInteractionId } from '../fapi/fapi-utils.js';
10
10
  * Creates signed request objects and submits them to the PAR endpoint.
11
11
  */
12
12
  export class PushedAuthorisationRequestEndpoint {
13
- constructor(sdkConfig, httpClient, jwtHelper, logger, participantsEndpoint) {
13
+ constructor(sdkConfig, httpClient, jwtHelper, logger, participantsEndpoint, cache) {
14
14
  this.sdkConfig = sdkConfig;
15
15
  this.httpClient = httpClient;
16
16
  this.jwtHelper = jwtHelper;
17
17
  this.logger = logger;
18
18
  this.participantsEndpoint = participantsEndpoint;
19
+ this.cache = cache;
19
20
  }
20
21
  /**
21
22
  * Sends a Pushed Authorization Request to the authorization server.
@@ -59,7 +60,7 @@ export class PushedAuthorisationRequestEndpoint {
59
60
  }
60
61
  async generateRequest(authServer, claimsRequest, purpose, xFapiInteractionId) {
61
62
  // Fetch discovery document
62
- const discoveryMetadata = await DiscoveryService.fetchDiscoveryDocument(authServer.OpenIDDiscoveryDocument, this.httpClient);
63
+ const discoveryMetadata = await DiscoveryService.fetchDiscoveryDocument(authServer.OpenIDDiscoveryDocument, this.httpClient, this.cache);
63
64
  if (!discoveryMetadata.pushed_authorization_request_endpoint) {
64
65
  throw new Error(`Authorization server ${authServer.AuthorisationServerId} does not support PAR`);
65
66
  }
@@ -2,6 +2,7 @@ import { Agent } from 'undici';
2
2
  import { Logger } from 'winston';
3
3
  import { CallbackParams, RelyingPartyClientSdkConfig } from '../types.js';
4
4
  import { JwtHelper } from '../crypto/jwt-helper.js';
5
+ import { HttpResponseCache } from '../cache/http-response-cache.js';
5
6
  import { ConsolidatedTokenSet } from '../model/consolidated-token-set.js';
6
7
  import { ParticipantsEndpoint } from './participants-endpoint';
7
8
  /**
@@ -16,7 +17,8 @@ export declare class RetrieveTokenEndpoint {
16
17
  private readonly jwtHelper;
17
18
  private readonly logger;
18
19
  private readonly participantsEndpoint;
19
- constructor(sdkConfig: RelyingPartyClientSdkConfig, httpClient: Agent, jwtHelper: JwtHelper, logger: Logger, participantsEndpoint: ParticipantsEndpoint);
20
+ private readonly cache;
21
+ constructor(sdkConfig: RelyingPartyClientSdkConfig, httpClient: Agent, jwtHelper: JwtHelper, logger: Logger, participantsEndpoint: ParticipantsEndpoint, cache: HttpResponseCache);
20
22
  /**
21
23
  * Retrieves tokens from the authorization server.
22
24
  *
@@ -10,12 +10,13 @@ import { generateXFapiInteractionId } from '../fapi/fapi-utils.js';
10
10
  * Validates the ID token and optionally calls UserInfo for compliance.
11
11
  */
12
12
  export class RetrieveTokenEndpoint {
13
- constructor(sdkConfig, httpClient, jwtHelper, logger, participantsEndpoint) {
13
+ constructor(sdkConfig, httpClient, jwtHelper, logger, participantsEndpoint, cache) {
14
14
  this.sdkConfig = sdkConfig;
15
15
  this.httpClient = httpClient;
16
16
  this.jwtHelper = jwtHelper;
17
17
  this.logger = logger;
18
18
  this.participantsEndpoint = participantsEndpoint;
19
+ this.cache = cache;
19
20
  }
20
21
  /**
21
22
  * Retrieves tokens from the authorization server.
@@ -41,7 +42,7 @@ export class RetrieveTokenEndpoint {
41
42
  this.validateCallbackParams(callbackParams, state);
42
43
  const authServer = await this.participantsEndpoint.getAuthServerDetails(authorisationServerId);
43
44
  // Fetch discovery document
44
- const discoveryMetadata = await DiscoveryService.fetchDiscoveryDocument(authServer.OpenIDDiscoveryDocument, this.httpClient);
45
+ const discoveryMetadata = await DiscoveryService.fetchDiscoveryDocument(authServer.OpenIDDiscoveryDocument, this.httpClient, this.cache);
45
46
  // Validate issuer parameter (REQUIRED per FAPI 2.0)
46
47
  if (!callbackParams.iss) {
47
48
  throw new Error('Authorization response missing required iss parameter');
@@ -54,7 +55,7 @@ export class RetrieveTokenEndpoint {
54
55
  // Exchange authorization code for tokens
55
56
  const tokenResponse = await this.requestToken(discoveryMetadata.token_endpoint, callbackParams.code, codeVerifier, clientAssertion, xFapiInteractionId);
56
57
  const tokenSet = new TokenSet(tokenResponse);
57
- const jwks = await DiscoveryService.fetchJwks(discoveryMetadata.jwks_uri, this.httpClient);
58
+ const jwks = await DiscoveryService.fetchJwks(discoveryMetadata.jwks_uri, this.httpClient, this.cache);
58
59
  await tokenSet.validate(jwks, discoveryMetadata.issuer, this.sdkConfig.data.client_id, nonce, discoveryMetadata.id_token_signing_alg_values_supported);
59
60
  // Must call validate first before accessing claims
60
61
  this.logger.info(`Retrieved tokenSet from auth server: ${authorisationServerId} - ${authServer.CustomerFriendlyName}, x-fapi-interaction-id: ${xFapiInteractionId}, txn: ${tokenSet.claims().txn}`);
@@ -1,5 +1,6 @@
1
1
  import { Agent } from 'undici';
2
2
  import { Logger } from 'winston';
3
+ import { HttpResponseCache } from '../cache/http-response-cache.js';
3
4
  import { ParticipantsEndpoint } from './participants-endpoint';
4
5
  /**
5
6
  * UserInfo Endpoint
@@ -12,7 +13,8 @@ export declare class UserInfoEndpoint {
12
13
  private readonly logger;
13
14
  private readonly clientId;
14
15
  private readonly participantsEndpoint;
15
- constructor(httpClient: Agent, logger: Logger, clientId: string, participantsEndpoint: ParticipantsEndpoint);
16
+ private readonly cache;
17
+ constructor(httpClient: Agent, logger: Logger, clientId: string, participantsEndpoint: ParticipantsEndpoint, cache: HttpResponseCache);
16
18
  /**
17
19
  * Retrieves user information from the UserInfo endpoint.
18
20
  *
@@ -8,11 +8,12 @@ import { generateXFapiInteractionId } from '../fapi/fapi-utils.js';
8
8
  * Returns user claims using an access token.
9
9
  */
10
10
  export class UserInfoEndpoint {
11
- constructor(httpClient, logger, clientId, participantsEndpoint) {
11
+ constructor(httpClient, logger, clientId, participantsEndpoint, cache) {
12
12
  this.httpClient = httpClient;
13
13
  this.logger = logger;
14
14
  this.clientId = clientId;
15
15
  this.participantsEndpoint = participantsEndpoint;
16
+ this.cache = cache;
16
17
  }
17
18
  /**
18
19
  * Retrieves user information from the UserInfo endpoint.
@@ -27,7 +28,7 @@ export class UserInfoEndpoint {
27
28
  try {
28
29
  const authServer = await this.participantsEndpoint.getAuthServerDetails(authorisationServerId);
29
30
  // Fetch discovery document
30
- const discoveryMetadata = await DiscoveryService.fetchDiscoveryDocument(authServer.OpenIDDiscoveryDocument, this.httpClient);
31
+ const discoveryMetadata = await DiscoveryService.fetchDiscoveryDocument(authServer.OpenIDDiscoveryDocument, this.httpClient, this.cache);
31
32
  if (!discoveryMetadata.userinfo_endpoint) {
32
33
  throw new Error(`Authorization server ${authServer.AuthorisationServerId} does not have a UserInfo endpoint`);
33
34
  }
@@ -1,6 +1,7 @@
1
1
  import { Agent } from 'undici';
2
2
  import { IssuerMetadata } from './issuer-metadata.js';
3
3
  import { JWKSet } from './jwks.js';
4
+ import { HttpResponseCache } from '../cache/http-response-cache.js';
4
5
  /**
5
6
  * Service for fetching OIDC discovery documents and JWKS.
6
7
  *
@@ -10,22 +11,26 @@ import { JWKSet } from './jwks.js';
10
11
  export declare class DiscoveryService {
11
12
  /**
12
13
  * Fetches and parses an OIDC discovery document.
14
+ * Uses cache-aside pattern for performance optimization.
13
15
  *
14
16
  * @param discoveryUrl - URL to the .well-known/openid-configuration endpoint
15
17
  * @param httpAgent - Optional undici Agent for mTLS
18
+ * @param cache - Optional HTTP response cache
16
19
  * @returns Parsed issuer metadata
17
20
  * @throws Error if the discovery document cannot be fetched or parsed
18
21
  */
19
- static fetchDiscoveryDocument(discoveryUrl: string, httpAgent?: Agent): Promise<IssuerMetadata>;
22
+ static fetchDiscoveryDocument(discoveryUrl: string, httpAgent?: Agent, cache?: HttpResponseCache): Promise<IssuerMetadata>;
20
23
  /**
21
24
  * Fetches and parses a JWKS document.
25
+ * Uses cache-aside pattern for performance optimization.
22
26
  *
23
27
  * @param jwksUri - URL to the JWKS endpoint
24
28
  * @param httpAgent - Optional HTTPS agent for mTLS
29
+ * @param cache - Optional HTTP response cache
25
30
  * @returns Parsed JWKS
26
31
  * @throws Error if the JWKS cannot be fetched or parsed
27
32
  */
28
- static fetchJwks(jwksUri: string, httpAgent?: Agent): Promise<JWKSet>;
33
+ static fetchJwks(jwksUri: string, httpAgent?: Agent, cache?: HttpResponseCache): Promise<JWKSet>;
29
34
  /**
30
35
  * Validates that required discovery document fields are present.
31
36
  *
@@ -7,14 +7,24 @@
7
7
  export class DiscoveryService {
8
8
  /**
9
9
  * Fetches and parses an OIDC discovery document.
10
+ * Uses cache-aside pattern for performance optimization.
10
11
  *
11
12
  * @param discoveryUrl - URL to the .well-known/openid-configuration endpoint
12
13
  * @param httpAgent - Optional undici Agent for mTLS
14
+ * @param cache - Optional HTTP response cache
13
15
  * @returns Parsed issuer metadata
14
16
  * @throws Error if the discovery document cannot be fetched or parsed
15
17
  */
16
- static async fetchDiscoveryDocument(discoveryUrl, httpAgent) {
18
+ static async fetchDiscoveryDocument(discoveryUrl, httpAgent, cache) {
17
19
  try {
20
+ // Check cache first
21
+ if (cache) {
22
+ const cachedContent = cache.get(discoveryUrl);
23
+ if (cachedContent) {
24
+ return JSON.parse(cachedContent);
25
+ }
26
+ }
27
+ // Fetch from network
18
28
  const response = await fetch(discoveryUrl, {
19
29
  method: 'GET',
20
30
  headers: {
@@ -29,7 +39,12 @@ export class DiscoveryService {
29
39
  // Validate required fields
30
40
  this.validateDiscoveryDocument(metadata);
31
41
  // Apply mtls_endpoint_aliases if present
32
- return this.applyMtlsAliases(metadata);
42
+ const processedMetadata = this.applyMtlsAliases(metadata);
43
+ // Cache successful response
44
+ if (cache && response.status >= 200 && response.status < 300) {
45
+ cache.put(discoveryUrl, JSON.stringify(processedMetadata));
46
+ }
47
+ return processedMetadata;
33
48
  }
34
49
  catch (error) {
35
50
  throw new Error(`Failed to fetch discovery document from ${discoveryUrl}: ${error instanceof Error ? error.message : String(error)}`);
@@ -37,14 +52,24 @@ export class DiscoveryService {
37
52
  }
38
53
  /**
39
54
  * Fetches and parses a JWKS document.
55
+ * Uses cache-aside pattern for performance optimization.
40
56
  *
41
57
  * @param jwksUri - URL to the JWKS endpoint
42
58
  * @param httpAgent - Optional HTTPS agent for mTLS
59
+ * @param cache - Optional HTTP response cache
43
60
  * @returns Parsed JWKS
44
61
  * @throws Error if the JWKS cannot be fetched or parsed
45
62
  */
46
- static async fetchJwks(jwksUri, httpAgent) {
63
+ static async fetchJwks(jwksUri, httpAgent, cache) {
47
64
  try {
65
+ // Check cache first
66
+ if (cache) {
67
+ const cachedContent = cache.get(jwksUri);
68
+ if (cachedContent) {
69
+ return JSON.parse(cachedContent);
70
+ }
71
+ }
72
+ // Fetch from network
48
73
  const response = await fetch(jwksUri, {
49
74
  method: 'GET',
50
75
  headers: {
@@ -60,6 +85,10 @@ export class DiscoveryService {
60
85
  if (!jwks.keys || !Array.isArray(jwks.keys)) {
61
86
  throw new Error('Invalid JWKS: missing or invalid keys array');
62
87
  }
88
+ // Cache successful response
89
+ if (cache && response.status >= 200 && response.status < 300) {
90
+ cache.put(jwksUri, JSON.stringify(jwks));
91
+ }
63
92
  return jwks;
64
93
  }
65
94
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@connectid-tools/rp-nodejs-sdk",
3
- "version": "5.0.1",
3
+ "version": "5.1.0",
4
4
  "description": "Digital Identity Relying Party Node SDK",
5
5
  "main": "relying-party-client-sdk.js",
6
6
  "types": "relying-party-client-sdk.d.ts",
@@ -5,6 +5,7 @@ import { illegalPurposeChars, isValidCertificate, validatePurpose } from './vali
5
5
  import { CryptoLoader } from './crypto/crypto-loader.js';
6
6
  import { JwtHelper } from './crypto/jwt-helper.js';
7
7
  import { HttpClientFactory } from './http/http-client-factory.js';
8
+ import { HttpResponseCache } from './cache/http-response-cache.js';
8
9
  import { ParticipantsEndpoint } from './endpoints/participants-endpoint.js';
9
10
  import { PushedAuthorisationRequestEndpoint } from './endpoints/pushed-authorisation-request-endpoint.js';
10
11
  import { RetrieveTokenEndpoint } from './endpoints/retrieve-token-endpoint.js';
@@ -27,7 +28,7 @@ export default class RelyingPartyClientSdk {
27
28
  throw new Error('Either ca_pem or ca_pem_content must be provided');
28
29
  }
29
30
  this.logger = getLogger(this.config.data.log_level);
30
- this.logger.info(`Creating RelyingPartyClientSdk - version 5.0.1`);
31
+ this.logger.info(`Creating RelyingPartyClientSdk - version 5.1.0`);
31
32
  // Validate and set purpose
32
33
  if (this.config.data.purpose) {
33
34
  const purposeValidation = validatePurpose(this.config.data.purpose);
@@ -73,11 +74,19 @@ export default class RelyingPartyClientSdk {
73
74
  caPem: getCertificate(this.config.data.ca_pem, this.config.data.ca_pem_content),
74
75
  clientId: this.config.data.client_id,
75
76
  });
77
+ // Initialize HTTP response cache
78
+ const cacheConfig = {
79
+ enabled: this.config.data.http_cache?.enabled ?? true,
80
+ ttlMinutes: this.config.data.http_cache?.ttl_minutes ?? 10,
81
+ maxEntries: this.config.data.http_cache?.max_entries ?? 100,
82
+ maxElementSizeBytes: this.config.data.http_cache?.max_element_size_bytes ?? 5242880,
83
+ };
84
+ const httpCache = new HttpResponseCache(cacheConfig, this.logger);
76
85
  // Initialize endpoints
77
- this.participantsEndpoint = new ParticipantsEndpoint(this.config, new ParticipantFilters(), this.httpClient, this.logger, () => this.getCurrentDate());
78
- this.pushedAuthorisationRequestEndpoint = new PushedAuthorisationRequestEndpoint(this.config, this.httpClient, this.jwtHelper, this.logger, this.participantsEndpoint);
79
- this.retrieveTokenEndpoint = new RetrieveTokenEndpoint(this.config, this.httpClient, this.jwtHelper, this.logger, this.participantsEndpoint);
80
- this.userInfoEndpoint = new UserInfoEndpoint(this.httpClient, this.logger, this.config.data.client_id, this.participantsEndpoint);
86
+ this.participantsEndpoint = new ParticipantsEndpoint(this.config, new ParticipantFilters(), this.httpClient, this.logger, () => this.getCurrentDate(), httpCache);
87
+ this.pushedAuthorisationRequestEndpoint = new PushedAuthorisationRequestEndpoint(this.config, this.httpClient, this.jwtHelper, this.logger, this.participantsEndpoint, httpCache);
88
+ this.retrieveTokenEndpoint = new RetrieveTokenEndpoint(this.config, this.httpClient, this.jwtHelper, this.logger, this.participantsEndpoint, httpCache);
89
+ this.userInfoEndpoint = new UserInfoEndpoint(this.httpClient, this.logger, this.config.data.client_id, this.participantsEndpoint, httpCache);
81
90
  }
82
91
  /**
83
92
  * Get the list of participating identity providers within the scheme.
package/types.d.ts CHANGED
@@ -2,6 +2,12 @@ import { IdTokenClaims } from './model/claims.js';
2
2
  export type { IdTokenClaims, AddressClaim, VerifiedClaims } from './model/claims.js';
3
3
  export type { CallbackParams } from './model/callback-params.js';
4
4
  export type { TokenResponse } from './model/token-response.js';
5
+ export type HttpCacheConfig = {
6
+ enabled?: boolean;
7
+ ttl_minutes?: number;
8
+ max_entries?: number;
9
+ max_element_size_bytes?: number;
10
+ };
5
11
  export type RelyingPartyClientSdkConfig = {
6
12
  data: {
7
13
  ca_pem?: string;
@@ -24,6 +30,7 @@ export type RelyingPartyClientSdkConfig = {
24
30
  enable_auto_compliance_verification: boolean;
25
31
  purpose?: string;
26
32
  client_id: string;
33
+ http_cache?: HttpCacheConfig;
27
34
  };
28
35
  };
29
36
  export type Participant = {
@@ -1,2 +1,2 @@
1
- export declare const packageJsonVersion = "5.0.1";
1
+ export declare const packageJsonVersion = "5.1.0";
2
2
  export declare const buildUserAgent: (clientId: string) => string;
@@ -1,4 +1,4 @@
1
1
  import { getSystemInformation } from './system-information.js';
2
2
  // important: Update this every time the package version changes
3
- export const packageJsonVersion = '5.0.1';
3
+ export const packageJsonVersion = '5.1.0';
4
4
  export const buildUserAgent = (clientId) => `cid-rp-nodejs-sdk/${packageJsonVersion} ${getSystemInformation()} +${clientId}`;