@cratis/chronicle 2.1.0 → 3.0.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.
@@ -0,0 +1,159 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import type { OAuthTokenResponse } from './fetchOAuthAccessToken';
6
+ import { OAuthTokenProvider } from './TokenProvider';
7
+
8
+ // Long enough to stay outside the 60s refresh margin for the whole spec.
9
+ const longLifetime = 3600;
10
+ // Short enough to be inside the refresh margin immediately.
11
+ const shortLifetime = 30;
12
+
13
+ const token = (value: string, expiresIn?: number | string): OAuthTokenResponse =>
14
+ ({ access_token: value, ...(expiresIn === undefined ? {} : { expires_in: expiresIn }) });
15
+
16
+ /**
17
+ * Creates a provider whose token fetches are served from the given script of
18
+ * responses — the last one repeats for any further fetches.
19
+ */
20
+ function createProvider(responses: Array<OAuthTokenResponse | Error>) {
21
+ let call = 0;
22
+ const fetchToken = vi.fn(() => {
23
+ const response = responses[Math.min(call++, responses.length - 1)];
24
+ return response instanceof Error ? Promise.reject(response) : Promise.resolve(response);
25
+ });
26
+
27
+ const provider = new OAuthTokenProvider('https://localhost:35000/connect/token', 'client', 'secret', true, fetchToken);
28
+ return { provider, fetchToken };
29
+ }
30
+
31
+ describe('OAuthTokenProvider', () => {
32
+ afterEach(() => {
33
+ vi.useRealTimers();
34
+ });
35
+
36
+ describe('when requesting the first token', () => {
37
+ it('should fetch it lazily and return it', async () => {
38
+ const { provider, fetchToken } = createProvider([token('token-1', longLifetime)]);
39
+
40
+ expect(await provider.getAccessToken()).toBe('token-1');
41
+ expect(fetchToken).toHaveBeenCalledTimes(1);
42
+ });
43
+ });
44
+
45
+ describe('when the cached token is fresh', () => {
46
+ it('should serve it without fetching again', async () => {
47
+ const { provider, fetchToken } = createProvider([token('token-1', longLifetime), token('token-2', longLifetime)]);
48
+
49
+ expect(await provider.getAccessToken()).toBe('token-1');
50
+ expect(await provider.getAccessToken()).toBe('token-1');
51
+ expect(fetchToken).toHaveBeenCalledTimes(1);
52
+ });
53
+ });
54
+
55
+ describe('when the token response has no expires_in', () => {
56
+ it('should assume the default lifetime and cache the token', async () => {
57
+ const { provider, fetchToken } = createProvider([token('token-1')]);
58
+
59
+ expect(await provider.getAccessToken()).toBe('token-1');
60
+ expect(await provider.getAccessToken()).toBe('token-1');
61
+ expect(fetchToken).toHaveBeenCalledTimes(1);
62
+ });
63
+ });
64
+
65
+ describe('when the token response sends expires_in as a string', () => {
66
+ it('should parse it and cache the token', async () => {
67
+ const { provider, fetchToken } = createProvider([token('token-1', '3600')]);
68
+
69
+ expect(await provider.getAccessToken()).toBe('token-1');
70
+ expect(await provider.getAccessToken()).toBe('token-1');
71
+ expect(fetchToken).toHaveBeenCalledTimes(1);
72
+ });
73
+ });
74
+
75
+ describe('when the token enters the refresh margin', () => {
76
+ it('should refresh ahead of expiry', async () => {
77
+ const { provider, fetchToken } = createProvider([token('token-1', shortLifetime), token('token-2', shortLifetime)]);
78
+
79
+ expect(await provider.getAccessToken()).toBe('token-1');
80
+
81
+ // The short lifetime is already inside the margin, so the next request
82
+ // refreshes even though the first token has not expired yet.
83
+ expect(await provider.getAccessToken()).toBe('token-2');
84
+ expect(fetchToken).toHaveBeenCalledTimes(2);
85
+ });
86
+ });
87
+
88
+ describe('when a refresh fails while the cached token is still valid', () => {
89
+ it('should keep serving the cached token', async () => {
90
+ const { provider } = createProvider([token('token-1', shortLifetime), new Error('unavailable')]);
91
+
92
+ expect(await provider.getAccessToken()).toBe('token-1');
93
+
94
+ // Refresh is due (inside the margin) and fails — the token is still valid
95
+ // for another 30s, so it must keep flowing rather than dropping auth.
96
+ expect(await provider.getAccessToken()).toBe('token-1');
97
+ });
98
+ });
99
+
100
+ describe('when no token can be fetched', () => {
101
+ it('should return undefined instead of rejecting', async () => {
102
+ const { provider } = createProvider([new Error('unavailable')]);
103
+
104
+ // The RPC proceeds and fails with the server's auth rejection — that is
105
+ // the session machinery's problem, not the token provider's.
106
+ expect(await provider.getAccessToken()).toBeUndefined();
107
+ });
108
+
109
+ it('should throttle further fetch attempts', async () => {
110
+ const { provider, fetchToken } = createProvider([new Error('unavailable')]);
111
+
112
+ expect(await provider.getAccessToken()).toBeUndefined();
113
+ expect(await provider.getAccessToken()).toBeUndefined();
114
+
115
+ // The second request arrives well inside the retry delay — one attempt,
116
+ // not one per RPC (the session answers a keepalive every second).
117
+ expect(fetchToken).toHaveBeenCalledTimes(1);
118
+ });
119
+
120
+ it('should try again once the retry delay has passed', async () => {
121
+ vi.useFakeTimers();
122
+ const { provider, fetchToken } = createProvider([new Error('unavailable'), token('token-1', longLifetime)]);
123
+
124
+ expect(await provider.getAccessToken()).toBeUndefined();
125
+ vi.advanceTimersByTime(5000);
126
+
127
+ expect(await provider.getAccessToken()).toBe('token-1');
128
+ expect(fetchToken).toHaveBeenCalledTimes(2);
129
+ });
130
+ });
131
+
132
+ describe('when multiple requests race', () => {
133
+ it('should share a single fetch', async () => {
134
+ let resolveFetch!: (response: OAuthTokenResponse) => void;
135
+ const fetchToken = vi.fn(() => new Promise<OAuthTokenResponse>(resolve => {
136
+ resolveFetch = resolve;
137
+ }));
138
+ const provider = new OAuthTokenProvider('https://localhost:35000/connect/token', 'client', 'secret', true, fetchToken);
139
+
140
+ const first = provider.getAccessToken();
141
+ const second = provider.getAccessToken();
142
+ resolveFetch(token('token-1', longLifetime));
143
+
144
+ expect(await first).toBe('token-1');
145
+ expect(await second).toBe('token-1');
146
+ expect(fetchToken).toHaveBeenCalledTimes(1);
147
+ });
148
+ });
149
+
150
+ describe('when a refresh is forced', () => {
151
+ it('should discard the cached token and fetch a new one', async () => {
152
+ const { provider, fetchToken } = createProvider([token('token-1', longLifetime), token('token-2', longLifetime)]);
153
+
154
+ expect(await provider.getAccessToken()).toBe('token-1');
155
+ expect(await provider.refresh()).toBe('token-2');
156
+ expect(fetchToken).toHaveBeenCalledTimes(2);
157
+ });
158
+ });
159
+ });
@@ -1,11 +1,16 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- import * as http from 'http';
5
- import * as https from 'https';
4
+ import { diag } from '@opentelemetry/api';
5
+ import { fetchOAuthAccessToken, type OAuthTokenResponse } from './fetchOAuthAccessToken';
6
6
 
7
- const TOKEN_EXPIRY_BUFFER_SECONDS = 60;
7
+ // Refresh once the token has less than this long left before it expires.
8
+ const TOKEN_REFRESH_MARGIN_MS = 60_000;
9
+ // Assumed lifetime when the token response carries no usable expires_in.
8
10
  const DEFAULT_TOKEN_EXPIRY_SECONDS = 3600;
11
+ // Minimum pause between failed fetch attempts, so an auth outage does not turn
12
+ // every RPC into a token request.
13
+ const FAILED_FETCH_RETRY_DELAY_MS = 5_000;
9
14
 
10
15
  /**
11
16
  * Interface for providing authentication tokens.
@@ -13,6 +18,10 @@ const DEFAULT_TOKEN_EXPIRY_SECONDS = 3600;
13
18
  export interface ITokenProvider {
14
19
  /**
15
20
  * Gets the current access token.
21
+ *
22
+ * Never rejects — when no token can be obtained the result is undefined, the RPC
23
+ * proceeds without authorization and fails with the server's rejection, which the
24
+ * session machinery recovers from.
16
25
  * @returns Promise resolving to the access token or undefined if not available.
17
26
  */
18
27
  getAccessToken(): Promise<string | undefined>;
@@ -37,28 +46,46 @@ export class NoOpTokenProvider implements ITokenProvider {
37
46
  }
38
47
  }
39
48
 
40
- interface OAuthTokenResponse {
41
- access_token: string;
42
- expires_in: number;
43
- }
44
-
45
49
  /**
46
- * OAuth token provider using client credentials flow.
50
+ * OAuth token provider using the client credentials flow.
51
+ *
52
+ * Owns the access token so it can be attached to every RPC individually instead of
53
+ * being baked into the channel at connect time: the token is fetched lazily on first
54
+ * use, cached, and refreshed once it enters the refresh margin ahead of expiry, so
55
+ * token expiry never invalidates the channel. A failed refresh falls back to the
56
+ * cached token while it is still actually valid — only the refresh margin has been
57
+ * crossed, not the expiry — and further attempts are throttled so an unreachable
58
+ * auth endpoint does not turn every RPC into a fetch attempt.
47
59
  */
48
60
  export class OAuthTokenProvider implements ITokenProvider {
61
+ private readonly _logger = diag.createComponentLogger({
62
+ namespace: '@cratis/chronicle/OAuthTokenProvider'
63
+ });
64
+
49
65
  private _accessToken?: string;
50
- private _tokenExpiry = new Date(0);
66
+ private _expiresAt = 0;
67
+ private _lastFailedFetch?: number;
51
68
  private _refreshPromise?: Promise<string | undefined>;
52
69
 
70
+ /**
71
+ * Creates a new {@link OAuthTokenProvider}.
72
+ * @param tokenEndpoint - The OAuth2 token endpoint to request tokens from.
73
+ * @param clientId - The client identifier.
74
+ * @param clientSecret - The client secret.
75
+ * @param skipTlsValidation - Whether to skip TLS certificate chain validation.
76
+ * @param _fetchToken - Test-only seam replacing the OAuth2 token request.
77
+ */
53
78
  constructor(
54
- private readonly _tokenEndpoint: string,
55
- private readonly _clientId: string,
56
- private readonly _clientSecret: string,
57
- private readonly _skipTlsValidation: boolean = true
79
+ tokenEndpoint: string,
80
+ clientId: string,
81
+ clientSecret: string,
82
+ skipTlsValidation: boolean = true,
83
+ private readonly _fetchToken: () => Promise<OAuthTokenResponse> = () =>
84
+ fetchOAuthAccessToken(tokenEndpoint, clientId, clientSecret, skipTlsValidation)
58
85
  ) {}
59
86
 
60
87
  async getAccessToken(): Promise<string | undefined> {
61
- if (this._accessToken && new Date() < this._tokenExpiry) {
88
+ if (this.hasFreshToken()) {
62
89
  return this._accessToken;
63
90
  }
64
91
 
@@ -66,7 +93,11 @@ export class OAuthTokenProvider implements ITokenProvider {
66
93
  return this._refreshPromise;
67
94
  }
68
95
 
69
- this._refreshPromise = this.fetchAccessToken();
96
+ if (this.isThrottled()) {
97
+ return this.cachedTokenWhileValid();
98
+ }
99
+
100
+ this._refreshPromise = this.fetchAndCacheAccessToken();
70
101
  try {
71
102
  return await this._refreshPromise;
72
103
  } finally {
@@ -76,65 +107,45 @@ export class OAuthTokenProvider implements ITokenProvider {
76
107
 
77
108
  async refresh(): Promise<string | undefined> {
78
109
  this._accessToken = undefined;
79
- this._tokenExpiry = new Date(0);
110
+ this._expiresAt = 0;
111
+ this._lastFailedFetch = undefined;
80
112
  return this.getAccessToken();
81
113
  }
82
114
 
83
- private async fetchAccessToken(): Promise<string | undefined> {
84
- const params = new URLSearchParams();
85
- params.append('grant_type', 'client_credentials');
86
- params.append('client_id', this._clientId);
87
- params.append('client_secret', this._clientSecret);
88
-
89
- const body = params.toString();
90
-
91
- return new Promise((resolve, reject) => {
92
- const url = new URL(this._tokenEndpoint);
93
- const isHttps = url.protocol === 'https:';
94
- const httpModule = isHttps ? https : http;
95
-
96
- const req = httpModule.request(url, {
97
- method: 'POST',
98
- headers: {
99
- 'Content-Type': 'application/x-www-form-urlencoded',
100
- 'Content-Length': Buffer.byteLength(body)
101
- },
102
- // Chain validation is skipped only when skipTlsValidation is explicitly set,
103
- // matching the gRPC channel's credentials for the same connection string.
104
- ...(isHttps && this._skipTlsValidation ? { rejectUnauthorized: false } : {})
105
- }, response => {
106
- let data = '';
107
-
108
- response.on('data', chunk => {
109
- data += chunk;
110
- });
111
-
112
- response.on('end', () => {
113
- if (response.statusCode !== 200) {
114
- reject(new Error(`Token request failed with status ${response.statusCode}: ${data}`));
115
- return;
116
- }
117
-
118
- try {
119
- const tokenResponse = JSON.parse(data) as OAuthTokenResponse;
120
- this._accessToken = tokenResponse.access_token;
121
- const expiresInSeconds = tokenResponse.expires_in || DEFAULT_TOKEN_EXPIRY_SECONDS;
122
- this._tokenExpiry = new Date(Date.now() + (expiresInSeconds - TOKEN_EXPIRY_BUFFER_SECONDS) * 1000);
123
- resolve(this._accessToken);
124
- } catch (error) {
125
- reject(new Error(`Failed to parse token response: ${error instanceof Error ? error.message : String(error)}`));
126
- }
127
- });
128
- });
115
+ private hasFreshToken(): boolean {
116
+ return !!this._accessToken && this._expiresAt - Date.now() > TOKEN_REFRESH_MARGIN_MS;
117
+ }
129
118
 
130
- req.on('error', error => {
131
- reject(new Error(`Token request failed: ${error.message}`));
119
+ private isThrottled(): boolean {
120
+ return this._lastFailedFetch !== undefined && Date.now() - this._lastFailedFetch < FAILED_FETCH_RETRY_DELAY_MS;
121
+ }
122
+
123
+ private cachedTokenWhileValid(): string | undefined {
124
+ return this._accessToken && Date.now() < this._expiresAt ? this._accessToken : undefined;
125
+ }
126
+
127
+ private async fetchAndCacheAccessToken(): Promise<string | undefined> {
128
+ try {
129
+ const response = await this._fetchToken();
130
+ this._accessToken = response.access_token;
131
+ this._expiresAt = Date.now() + this.lifetimeSecondsFrom(response) * 1000;
132
+ this._lastFailedFetch = undefined;
133
+ return this._accessToken;
134
+ } catch (error) {
135
+ this._logger.warn('Failed to fetch OAuth2 token', {
136
+ error: error instanceof Error ? error.message : String(error)
132
137
  });
138
+ this._lastFailedFetch = Date.now();
139
+ return this.cachedTokenWhileValid();
140
+ }
141
+ }
133
142
 
134
- req.write(body);
135
- req.end();
136
- });
143
+ // expires_in is RECOMMENDED but not required by OAuth2, and some servers send it
144
+ // as a string.
145
+ private lifetimeSecondsFrom(response: OAuthTokenResponse): number {
146
+ const seconds = Number(response.expires_in);
147
+ return Number.isFinite(seconds) && seconds > 0 ? seconds : DEFAULT_TOKEN_EXPIRY_SECONDS;
137
148
  }
138
149
  }
139
150
 
140
- export type TokenProvider = ITokenProvider;
151
+ export type TokenProvider = ITokenProvider;
@@ -0,0 +1,86 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import * as http from 'http';
5
+ import * as https from 'https';
6
+
7
+ /**
8
+ * The shape of a successful OAuth2 token response.
9
+ */
10
+ export interface OAuthTokenResponse {
11
+ access_token: string;
12
+
13
+ /** RECOMMENDED but not required by OAuth2, and some servers send it as a string. */
14
+ expires_in?: number | string;
15
+ }
16
+
17
+ /**
18
+ * Requests an access token from an OAuth2 token endpoint using the client credentials flow.
19
+ * @param tokenEndpoint - The token endpoint URL.
20
+ * @param clientId - The client identifier.
21
+ * @param clientSecret - The client secret.
22
+ * @param skipTlsValidation - Whether to skip TLS certificate chain validation.
23
+ * @returns The parsed token response; rejects when the request fails or the response is not a valid token.
24
+ */
25
+ export function fetchOAuthAccessToken(
26
+ tokenEndpoint: string,
27
+ clientId: string,
28
+ clientSecret: string,
29
+ skipTlsValidation: boolean
30
+ ): Promise<OAuthTokenResponse> {
31
+ const params = new URLSearchParams();
32
+ params.append('grant_type', 'client_credentials');
33
+ params.append('client_id', clientId);
34
+ params.append('client_secret', clientSecret);
35
+
36
+ const body = params.toString();
37
+
38
+ return new Promise((resolve, reject) => {
39
+ const url = new URL(tokenEndpoint);
40
+ const isHttps = url.protocol === 'https:';
41
+ const httpModule = isHttps ? https : http;
42
+
43
+ const request = httpModule.request(url, {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/x-www-form-urlencoded',
47
+ 'Content-Length': Buffer.byteLength(body)
48
+ },
49
+ // Chain validation is skipped only when skipTlsValidation is explicitly set,
50
+ // matching the gRPC channel's credentials for the same connection string.
51
+ ...(isHttps && skipTlsValidation ? { rejectUnauthorized: false } : {})
52
+ }, response => {
53
+ let data = '';
54
+
55
+ response.on('data', chunk => {
56
+ data += chunk;
57
+ });
58
+
59
+ response.on('end', () => {
60
+ if (response.statusCode !== 200) {
61
+ reject(new Error(`Token request failed with status ${response.statusCode}: ${data}`));
62
+ return;
63
+ }
64
+
65
+ try {
66
+ const tokenResponse = JSON.parse(data) as OAuthTokenResponse;
67
+ if (!tokenResponse.access_token) {
68
+ reject(new Error('Token response did not contain an access_token'));
69
+ return;
70
+ }
71
+
72
+ resolve(tokenResponse);
73
+ } catch (error) {
74
+ reject(new Error(`Failed to parse token response: ${error instanceof Error ? error.message : String(error)}`));
75
+ }
76
+ });
77
+ });
78
+
79
+ request.on('error', error => {
80
+ reject(new Error(`Token request failed: ${error.message}`));
81
+ });
82
+
83
+ request.write(body);
84
+ request.end();
85
+ });
86
+ }
@@ -1,9 +1,14 @@
1
+ import { type OAuthTokenResponse } from './fetchOAuthAccessToken';
1
2
  /**
2
3
  * Interface for providing authentication tokens.
3
4
  */
4
5
  export interface ITokenProvider {
5
6
  /**
6
7
  * Gets the current access token.
8
+ *
9
+ * Never rejects — when no token can be obtained the result is undefined, the RPC
10
+ * proceeds without authorization and fails with the server's rejection, which the
11
+ * session machinery recovers from.
7
12
  * @returns Promise resolving to the access token or undefined if not available.
8
13
  */
9
14
  getAccessToken(): Promise<string | undefined>;
@@ -21,20 +26,39 @@ export declare class NoOpTokenProvider implements ITokenProvider {
21
26
  refresh(): Promise<string | undefined>;
22
27
  }
23
28
  /**
24
- * OAuth token provider using client credentials flow.
29
+ * OAuth token provider using the client credentials flow.
30
+ *
31
+ * Owns the access token so it can be attached to every RPC individually instead of
32
+ * being baked into the channel at connect time: the token is fetched lazily on first
33
+ * use, cached, and refreshed once it enters the refresh margin ahead of expiry, so
34
+ * token expiry never invalidates the channel. A failed refresh falls back to the
35
+ * cached token while it is still actually valid — only the refresh margin has been
36
+ * crossed, not the expiry — and further attempts are throttled so an unreachable
37
+ * auth endpoint does not turn every RPC into a fetch attempt.
25
38
  */
26
39
  export declare class OAuthTokenProvider implements ITokenProvider {
27
- private readonly _tokenEndpoint;
28
- private readonly _clientId;
29
- private readonly _clientSecret;
30
- private readonly _skipTlsValidation;
40
+ private readonly _fetchToken;
41
+ private readonly _logger;
31
42
  private _accessToken?;
32
- private _tokenExpiry;
43
+ private _expiresAt;
44
+ private _lastFailedFetch?;
33
45
  private _refreshPromise?;
34
- constructor(_tokenEndpoint: string, _clientId: string, _clientSecret: string, _skipTlsValidation?: boolean);
46
+ /**
47
+ * Creates a new {@link OAuthTokenProvider}.
48
+ * @param tokenEndpoint - The OAuth2 token endpoint to request tokens from.
49
+ * @param clientId - The client identifier.
50
+ * @param clientSecret - The client secret.
51
+ * @param skipTlsValidation - Whether to skip TLS certificate chain validation.
52
+ * @param _fetchToken - Test-only seam replacing the OAuth2 token request.
53
+ */
54
+ constructor(tokenEndpoint: string, clientId: string, clientSecret: string, skipTlsValidation?: boolean, _fetchToken?: () => Promise<OAuthTokenResponse>);
35
55
  getAccessToken(): Promise<string | undefined>;
36
56
  refresh(): Promise<string | undefined>;
37
- private fetchAccessToken;
57
+ private hasFreshToken;
58
+ private isThrottled;
59
+ private cachedTokenWhileValid;
60
+ private fetchAndCacheAccessToken;
61
+ private lifetimeSecondsFrom;
38
62
  }
39
63
  export type TokenProvider = ITokenProvider;
40
64
  //# sourceMappingURL=TokenProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TokenProvider.d.ts","sourceRoot":"","sources":["../../connection/TokenProvider.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC3B;;;OAGG;IACH,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAE9C;;;OAGG;IACH,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC1C;AAED;;GAEG;AACH,qBAAa,iBAAkB,YAAW,cAAc;IAC9C,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAI7C,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;CAG/C;AAOD;;GAEG;AACH,qBAAa,kBAAmB,YAAW,cAAc;IAMjD,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IARvC,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,eAAe,CAAC,CAA8B;gBAGjC,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,kBAAkB,GAAE,OAAc;IAGjD,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAiB7C,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;YAM9B,gBAAgB;CAuDjC;AAED,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC"}
1
+ {"version":3,"file":"TokenProvider.d.ts","sourceRoot":"","sources":["../../connection/TokenProvider.ts"],"names":[],"mappings":"AAIA,OAAO,EAAyB,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAUzF;;GAEG;AACH,MAAM,WAAW,cAAc;IAC3B;;;;;;;OAOG;IACH,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAE9C;;;OAGG;IACH,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC1C;AAED;;GAEG;AACH,qBAAa,iBAAkB,YAAW,cAAc;IAC9C,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAI7C,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;CAG/C;AAED;;;;;;;;;;GAUG;AACH,qBAAa,kBAAmB,YAAW,cAAc;IAuBjD,OAAO,CAAC,QAAQ,CAAC,WAAW;IAtBhC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAErB;IAEH,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,gBAAgB,CAAC,CAAS;IAClC,OAAO,CAAC,eAAe,CAAC,CAA8B;IAEtD;;;;;;;OAOG;gBAEC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,iBAAiB,GAAE,OAAc,EAChB,WAAW,GAAE,MAAM,OAAO,CAAC,kBAAkB,CACqB;IAGjF,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAqB7C,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAO5C,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,qBAAqB;YAIf,wBAAwB;IAkBtC,OAAO,CAAC,mBAAmB;CAI9B;AAED,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC"}
@@ -1,9 +1,14 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
- import * as http from 'http';
4
- import * as https from 'https';
5
- const TOKEN_EXPIRY_BUFFER_SECONDS = 60;
3
+ import { diag } from '@opentelemetry/api';
4
+ import { fetchOAuthAccessToken } from './fetchOAuthAccessToken';
5
+ // Refresh once the token has less than this long left before it expires.
6
+ const TOKEN_REFRESH_MARGIN_MS = 60_000;
7
+ // Assumed lifetime when the token response carries no usable expires_in.
6
8
  const DEFAULT_TOKEN_EXPIRY_SECONDS = 3600;
9
+ // Minimum pause between failed fetch attempts, so an auth outage does not turn
10
+ // every RPC into a token request.
11
+ const FAILED_FETCH_RETRY_DELAY_MS = 5_000;
7
12
  /**
8
13
  * No-op token provider for when authentication is not required.
9
14
  */
@@ -16,30 +21,47 @@ export class NoOpTokenProvider {
16
21
  }
17
22
  }
18
23
  /**
19
- * OAuth token provider using client credentials flow.
24
+ * OAuth token provider using the client credentials flow.
25
+ *
26
+ * Owns the access token so it can be attached to every RPC individually instead of
27
+ * being baked into the channel at connect time: the token is fetched lazily on first
28
+ * use, cached, and refreshed once it enters the refresh margin ahead of expiry, so
29
+ * token expiry never invalidates the channel. A failed refresh falls back to the
30
+ * cached token while it is still actually valid — only the refresh margin has been
31
+ * crossed, not the expiry — and further attempts are throttled so an unreachable
32
+ * auth endpoint does not turn every RPC into a fetch attempt.
20
33
  */
21
34
  export class OAuthTokenProvider {
22
- _tokenEndpoint;
23
- _clientId;
24
- _clientSecret;
25
- _skipTlsValidation;
35
+ _fetchToken;
36
+ _logger = diag.createComponentLogger({
37
+ namespace: '@cratis/chronicle/OAuthTokenProvider'
38
+ });
26
39
  _accessToken;
27
- _tokenExpiry = new Date(0);
40
+ _expiresAt = 0;
41
+ _lastFailedFetch;
28
42
  _refreshPromise;
29
- constructor(_tokenEndpoint, _clientId, _clientSecret, _skipTlsValidation = true) {
30
- this._tokenEndpoint = _tokenEndpoint;
31
- this._clientId = _clientId;
32
- this._clientSecret = _clientSecret;
33
- this._skipTlsValidation = _skipTlsValidation;
43
+ /**
44
+ * Creates a new {@link OAuthTokenProvider}.
45
+ * @param tokenEndpoint - The OAuth2 token endpoint to request tokens from.
46
+ * @param clientId - The client identifier.
47
+ * @param clientSecret - The client secret.
48
+ * @param skipTlsValidation - Whether to skip TLS certificate chain validation.
49
+ * @param _fetchToken - Test-only seam replacing the OAuth2 token request.
50
+ */
51
+ constructor(tokenEndpoint, clientId, clientSecret, skipTlsValidation = true, _fetchToken = () => fetchOAuthAccessToken(tokenEndpoint, clientId, clientSecret, skipTlsValidation)) {
52
+ this._fetchToken = _fetchToken;
34
53
  }
35
54
  async getAccessToken() {
36
- if (this._accessToken && new Date() < this._tokenExpiry) {
55
+ if (this.hasFreshToken()) {
37
56
  return this._accessToken;
38
57
  }
39
58
  if (this._refreshPromise) {
40
59
  return this._refreshPromise;
41
60
  }
42
- this._refreshPromise = this.fetchAccessToken();
61
+ if (this.isThrottled()) {
62
+ return this.cachedTokenWhileValid();
63
+ }
64
+ this._refreshPromise = this.fetchAndCacheAccessToken();
43
65
  try {
44
66
  return await this._refreshPromise;
45
67
  }
@@ -49,56 +71,40 @@ export class OAuthTokenProvider {
49
71
  }
50
72
  async refresh() {
51
73
  this._accessToken = undefined;
52
- this._tokenExpiry = new Date(0);
74
+ this._expiresAt = 0;
75
+ this._lastFailedFetch = undefined;
53
76
  return this.getAccessToken();
54
77
  }
55
- async fetchAccessToken() {
56
- const params = new URLSearchParams();
57
- params.append('grant_type', 'client_credentials');
58
- params.append('client_id', this._clientId);
59
- params.append('client_secret', this._clientSecret);
60
- const body = params.toString();
61
- return new Promise((resolve, reject) => {
62
- const url = new URL(this._tokenEndpoint);
63
- const isHttps = url.protocol === 'https:';
64
- const httpModule = isHttps ? https : http;
65
- const req = httpModule.request(url, {
66
- method: 'POST',
67
- headers: {
68
- 'Content-Type': 'application/x-www-form-urlencoded',
69
- 'Content-Length': Buffer.byteLength(body)
70
- },
71
- // Chain validation is skipped only when skipTlsValidation is explicitly set,
72
- // matching the gRPC channel's credentials for the same connection string.
73
- ...(isHttps && this._skipTlsValidation ? { rejectUnauthorized: false } : {})
74
- }, response => {
75
- let data = '';
76
- response.on('data', chunk => {
77
- data += chunk;
78
- });
79
- response.on('end', () => {
80
- if (response.statusCode !== 200) {
81
- reject(new Error(`Token request failed with status ${response.statusCode}: ${data}`));
82
- return;
83
- }
84
- try {
85
- const tokenResponse = JSON.parse(data);
86
- this._accessToken = tokenResponse.access_token;
87
- const expiresInSeconds = tokenResponse.expires_in || DEFAULT_TOKEN_EXPIRY_SECONDS;
88
- this._tokenExpiry = new Date(Date.now() + (expiresInSeconds - TOKEN_EXPIRY_BUFFER_SECONDS) * 1000);
89
- resolve(this._accessToken);
90
- }
91
- catch (error) {
92
- reject(new Error(`Failed to parse token response: ${error instanceof Error ? error.message : String(error)}`));
93
- }
94
- });
95
- });
96
- req.on('error', error => {
97
- reject(new Error(`Token request failed: ${error.message}`));
78
+ hasFreshToken() {
79
+ return !!this._accessToken && this._expiresAt - Date.now() > TOKEN_REFRESH_MARGIN_MS;
80
+ }
81
+ isThrottled() {
82
+ return this._lastFailedFetch !== undefined && Date.now() - this._lastFailedFetch < FAILED_FETCH_RETRY_DELAY_MS;
83
+ }
84
+ cachedTokenWhileValid() {
85
+ return this._accessToken && Date.now() < this._expiresAt ? this._accessToken : undefined;
86
+ }
87
+ async fetchAndCacheAccessToken() {
88
+ try {
89
+ const response = await this._fetchToken();
90
+ this._accessToken = response.access_token;
91
+ this._expiresAt = Date.now() + this.lifetimeSecondsFrom(response) * 1000;
92
+ this._lastFailedFetch = undefined;
93
+ return this._accessToken;
94
+ }
95
+ catch (error) {
96
+ this._logger.warn('Failed to fetch OAuth2 token', {
97
+ error: error instanceof Error ? error.message : String(error)
98
98
  });
99
- req.write(body);
100
- req.end();
101
- });
99
+ this._lastFailedFetch = Date.now();
100
+ return this.cachedTokenWhileValid();
101
+ }
102
+ }
103
+ // expires_in is RECOMMENDED but not required by OAuth2, and some servers send it
104
+ // as a string.
105
+ lifetimeSecondsFrom(response) {
106
+ const seconds = Number(response.expires_in);
107
+ return Number.isFinite(seconds) && seconds > 0 ? seconds : DEFAULT_TOKEN_EXPIRY_SECONDS;
102
108
  }
103
109
  }
104
110
  //# sourceMappingURL=TokenProvider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TokenProvider.js","sourceRoot":"","sources":["../../connection/TokenProvider.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,qGAAqG;AAErG,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,2BAA2B,GAAG,EAAE,CAAC;AACvC,MAAM,4BAA4B,GAAG,IAAI,CAAC;AAmB1C;;GAEG;AACH,MAAM,OAAO,iBAAiB;IAC1B,KAAK,CAAC,cAAc;QAChB,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,OAAO;QACT,OAAO,SAAS,CAAC;IACrB,CAAC;CACJ;AAOD;;GAEG;AACH,MAAM,OAAO,kBAAkB;IAMN;IACA;IACA;IACA;IARb,YAAY,CAAU;IACtB,YAAY,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3B,eAAe,CAA+B;IAEtD,YACqB,cAAsB,EACtB,SAAiB,EACjB,aAAqB,EACrB,qBAA8B,IAAI;QAHlC,mBAAc,GAAd,cAAc,CAAQ;QACtB,cAAS,GAAT,SAAS,CAAQ;QACjB,kBAAa,GAAb,aAAa,CAAQ;QACrB,uBAAkB,GAAlB,kBAAkB,CAAgB;IACpD,CAAC;IAEJ,KAAK,CAAC,cAAc;QAChB,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,IAAI,EAAE,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YACtD,OAAO,IAAI,CAAC,YAAY,CAAC;QAC7B,CAAC;QAED,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,eAAe,CAAC;QAChC,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC/C,IAAI,CAAC;YACD,OAAO,MAAM,IAAI,CAAC,eAAe,CAAC;QACtC,CAAC;gBAAS,CAAC;YACP,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QACrC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO;QACT,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC;QAC9B,IAAI,CAAC,YAAY,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;QAChC,OAAO,IAAI,CAAC,cAAc,EAAE,CAAC;IACjC,CAAC;IAEO,KAAK,CAAC,gBAAgB;QAC1B,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,oBAAoB,CAAC,CAAC;QAClD,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3C,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAEnD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;QAE/B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACnC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;YAC1C,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;YAE1C,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE;gBAChC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACL,cAAc,EAAE,mCAAmC;oBACnD,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC;iBAC5C;gBACD,6EAA6E;gBAC7E,0EAA0E;gBAC1E,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC/E,EAAE,QAAQ,CAAC,EAAE;gBACV,IAAI,IAAI,GAAG,EAAE,CAAC;gBAEd,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE;oBACxB,IAAI,IAAI,KAAK,CAAC;gBAClB,CAAC,CAAC,CAAC;gBAEH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;oBACpB,IAAI,QAAQ,CAAC,UAAU,KAAK,GAAG,EAAE,CAAC;wBAC9B,MAAM,CAAC,IAAI,KAAK,CAAC,oCAAoC,QAAQ,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;wBACtF,OAAO;oBACX,CAAC;oBAED,IAAI,CAAC;wBACD,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAuB,CAAC;wBAC7D,IAAI,CAAC,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;wBAC/C,MAAM,gBAAgB,GAAG,aAAa,CAAC,UAAU,IAAI,4BAA4B,CAAC;wBAClF,IAAI,CAAC,YAAY,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,gBAAgB,GAAG,2BAA2B,CAAC,GAAG,IAAI,CAAC,CAAC;wBACnG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;oBAC/B,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACb,MAAM,CAAC,IAAI,KAAK,CAAC,mCAAmC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;oBACnH,CAAC;gBACL,CAAC,CAAC,CAAC;YACP,CAAC,CAAC,CAAC;YAEH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE;gBACpB,MAAM,CAAC,IAAI,KAAK,CAAC,yBAAyB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;YAEH,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAChB,GAAG,CAAC,GAAG,EAAE,CAAC;QACd,CAAC,CAAC,CAAC;IACP,CAAC;CACJ"}
1
+ {"version":3,"file":"TokenProvider.js","sourceRoot":"","sources":["../../connection/TokenProvider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The shape of a successful OAuth2 token response.
3
+ */
4
+ export interface OAuthTokenResponse {
5
+ access_token: string;
6
+ /** RECOMMENDED but not required by OAuth2, and some servers send it as a string. */
7
+ expires_in?: number | string;
8
+ }
9
+ /**
10
+ * Requests an access token from an OAuth2 token endpoint using the client credentials flow.
11
+ * @param tokenEndpoint - The token endpoint URL.
12
+ * @param clientId - The client identifier.
13
+ * @param clientSecret - The client secret.
14
+ * @param skipTlsValidation - Whether to skip TLS certificate chain validation.
15
+ * @returns The parsed token response; rejects when the request fails or the response is not a valid token.
16
+ */
17
+ export declare function fetchOAuthAccessToken(tokenEndpoint: string, clientId: string, clientSecret: string, skipTlsValidation: boolean): Promise<OAuthTokenResponse>;
18
+ //# sourceMappingURL=fetchOAuthAccessToken.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetchOAuthAccessToken.d.ts","sourceRoot":"","sources":["../../connection/fetchOAuthAccessToken.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAC/B,YAAY,EAAE,MAAM,CAAC;IAErB,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACjC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,iBAAiB,EAAE,OAAO,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CAwD7B"}
@@ -0,0 +1,62 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+ import * as http from 'http';
4
+ import * as https from 'https';
5
+ /**
6
+ * Requests an access token from an OAuth2 token endpoint using the client credentials flow.
7
+ * @param tokenEndpoint - The token endpoint URL.
8
+ * @param clientId - The client identifier.
9
+ * @param clientSecret - The client secret.
10
+ * @param skipTlsValidation - Whether to skip TLS certificate chain validation.
11
+ * @returns The parsed token response; rejects when the request fails or the response is not a valid token.
12
+ */
13
+ export function fetchOAuthAccessToken(tokenEndpoint, clientId, clientSecret, skipTlsValidation) {
14
+ const params = new URLSearchParams();
15
+ params.append('grant_type', 'client_credentials');
16
+ params.append('client_id', clientId);
17
+ params.append('client_secret', clientSecret);
18
+ const body = params.toString();
19
+ return new Promise((resolve, reject) => {
20
+ const url = new URL(tokenEndpoint);
21
+ const isHttps = url.protocol === 'https:';
22
+ const httpModule = isHttps ? https : http;
23
+ const request = httpModule.request(url, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/x-www-form-urlencoded',
27
+ 'Content-Length': Buffer.byteLength(body)
28
+ },
29
+ // Chain validation is skipped only when skipTlsValidation is explicitly set,
30
+ // matching the gRPC channel's credentials for the same connection string.
31
+ ...(isHttps && skipTlsValidation ? { rejectUnauthorized: false } : {})
32
+ }, response => {
33
+ let data = '';
34
+ response.on('data', chunk => {
35
+ data += chunk;
36
+ });
37
+ response.on('end', () => {
38
+ if (response.statusCode !== 200) {
39
+ reject(new Error(`Token request failed with status ${response.statusCode}: ${data}`));
40
+ return;
41
+ }
42
+ try {
43
+ const tokenResponse = JSON.parse(data);
44
+ if (!tokenResponse.access_token) {
45
+ reject(new Error('Token response did not contain an access_token'));
46
+ return;
47
+ }
48
+ resolve(tokenResponse);
49
+ }
50
+ catch (error) {
51
+ reject(new Error(`Failed to parse token response: ${error instanceof Error ? error.message : String(error)}`));
52
+ }
53
+ });
54
+ });
55
+ request.on('error', error => {
56
+ reject(new Error(`Token request failed: ${error.message}`));
57
+ });
58
+ request.write(body);
59
+ request.end();
60
+ });
61
+ }
62
+ //# sourceMappingURL=fetchOAuthAccessToken.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetchOAuthAccessToken.js","sourceRoot":"","sources":["../../connection/fetchOAuthAccessToken.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,qGAAqG;AAErG,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAY/B;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CACjC,aAAqB,EACrB,QAAgB,EAChB,YAAoB,EACpB,iBAA0B;IAE1B,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;IACrC,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,oBAAoB,CAAC,CAAC;IAClD,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACrC,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;IAE7C,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;IAE/B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACnC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;QAC1C,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;QAE1C,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE;YACpC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACL,cAAc,EAAE,mCAAmC;gBACnD,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC;aAC5C;YACD,6EAA6E;YAC7E,0EAA0E;YAC1E,GAAG,CAAC,OAAO,IAAI,iBAAiB,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACzE,EAAE,QAAQ,CAAC,EAAE;YACV,IAAI,IAAI,GAAG,EAAE,CAAC;YAEd,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE;gBACxB,IAAI,IAAI,KAAK,CAAC;YAClB,CAAC,CAAC,CAAC;YAEH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;gBACpB,IAAI,QAAQ,CAAC,UAAU,KAAK,GAAG,EAAE,CAAC;oBAC9B,MAAM,CAAC,IAAI,KAAK,CAAC,oCAAoC,QAAQ,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;oBACtF,OAAO;gBACX,CAAC;gBAED,IAAI,CAAC;oBACD,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAuB,CAAC;oBAC7D,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;wBAC9B,MAAM,CAAC,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC,CAAC;wBACpE,OAAO;oBACX,CAAC;oBAED,OAAO,CAAC,aAAa,CAAC,CAAC;gBAC3B,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACb,MAAM,CAAC,IAAI,KAAK,CAAC,mCAAmC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;gBACnH,CAAC;YACL,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE;YACxB,MAAM,CAAC,IAAI,KAAK,CAAC,yBAAyB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,CAAC,GAAG,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -1 +1 @@
1
- {"root":["../ChronicleClient.ts","../ChronicleOptions.ts","../EventStore.ts","../EventStoreName.ts","../EventStoreNamespaceName.ts","../IChronicleClient.ts","../IEventStore.ts","../Metrics.ts","../Tracing.ts","../index.ts","../artifacts/DefaultClientArtifactsProvider.ts","../artifacts/IClientArtifactsProvider.ts","../artifacts/index.ts","../auditing/Causation.ts","../auditing/CausationManager.ts","../auditing/CausationType.ts","../auditing/ICausationManager.ts","../auditing/index.ts","../compliance/ComplianceContracts.ts","../compliance/ComplianceMetadata.ts","../compliance/ComplianceMetadataResolver.ts","../compliance/ComplianceMetadataType.ts","../compliance/index.ts","../compliance/pii.ts","../connection/ChronicleConnection.ts","../connection/ChronicleConnectionString.ts","../connection/ChronicleServerAddressResolver.ts","../connection/ChronicleServices.ts","../connection/ChronicleSrvResolutionError.ts","../connection/ChronicleSrvResolver.ts","../connection/ConnectionLifecycle.ts","../connection/DateTimeOffset.ts","../connection/Guid.ts","../connection/ILoadBalancerStrategy.ts","../connection/KernelKeepAlive.ts","../connection/LeastConnectionsLoadBalancerStrategy.ts","../connection/LoadBalancerMode.ts","../connection/LoadBalancerStrategyFactory.ts","../connection/RandomLoadBalancerStrategy.ts","../connection/RoundRobinLoadBalancerStrategy.ts","../connection/TokenProvider.ts","../connection/formatServerAddress.ts","../connection/index.ts","../correlation/CorrelationId.ts","../correlation/CorrelationIdManager.ts","../correlation/ICorrelationIdAccessor.ts","../correlation/ICorrelationIdSetter.ts","../correlation/index.ts","../eventSequences/AppendError.ts","../eventSequences/AppendOptions.ts","../eventSequences/AppendResult.ts","../eventSequences/ConcurrencyScope.ts","../eventSequences/ConstraintViolation.ts","../eventSequences/EventForEventSourceId.ts","../eventSequences/EventLog.ts","../eventSequences/EventSequence.ts","../eventSequences/EventSequenceId.ts","../eventSequences/EventSequenceNumber.ts","../eventSequences/IEventLog.ts","../eventSequences/IEventSequence.ts","../eventSequences/ITransactionalEventSequence.ts","../eventSequences/TransactionalEventSequence.ts","../eventSequences/index.ts","../eventStoreSubscriptions/EventStoreSubscriptionBuilder.ts","../eventStoreSubscriptions/EventStoreSubscriptionDefinition.ts","../eventStoreSubscriptions/EventStoreSubscriptionId.ts","../eventStoreSubscriptions/EventStoreSubscriptions.ts","../eventStoreSubscriptions/IEventStoreSubscriptionBuilder.ts","../eventStoreSubscriptions/IEventStoreSubscriptions.ts","../eventStoreSubscriptions/contracts.ts","../eventStoreSubscriptions/index.ts","../events/AppendedEvent.ts","../events/CausationEntry.ts","../events/EventContext.ts","../events/EventType.ts","../events/EventTypeGeneration.ts","../events/EventTypeId.ts","../events/EventTypes.ts","../events/IEventTypes.ts","../events/eventTypeDecorator.ts","../events/index.ts","../events/constraints/ConstraintBuilder.ts","../events/constraints/ConstraintId.ts","../events/constraints/Constraints.ts","../events/constraints/IConstraint.ts","../events/constraints/IConstraintBuilder.ts","../events/constraints/IConstraints.ts","../events/constraints/IUniqueConstraintBuilder.ts","../events/constraints/UniqueConstraintBuilder.ts","../events/constraints/constraint.ts","../events/constraints/index.ts","../events/migrations/EventMigrationBuilder.ts","../events/migrations/EventMigrationPropertyBuilder.ts","../events/migrations/EventTypeMigrators.ts","../events/migrations/IEventMigrationBuilder.ts","../events/migrations/IEventMigrationPropertyBuilder.ts","../events/migrations/IEventTypeMigration.ts","../events/migrations/IEventTypeMigrators.ts","../events/migrations/InvalidMigrationGenerationGap.ts","../events/migrations/eventTypeMigration.ts","../events/migrations/index.ts","../identity/IIdentityProvider.ts","../identity/Identity.ts","../identity/IdentityProvider.ts","../identity/index.ts","../jobs/IJobs.ts","../jobs/JobId.ts","../jobs/Jobs.ts","../jobs/index.ts","../observation/ObserverId.ts","../observation/ObserverRunningState.ts","../observation/index.ts","../projections/IProjections.ts","../projections/ProjectionId.ts","../projections/Projections.ts","../projections/index.ts","../projections/declarative/AllSetBuilder.ts","../projections/declarative/FromBuilder.ts","../projections/declarative/FromEveryBuilder.ts","../projections/declarative/IAddBuilder.ts","../projections/declarative/IAddChildBuilder.ts","../projections/declarative/IAllSetBuilder.ts","../projections/declarative/IChildrenBuilder.ts","../projections/declarative/ICompositeKeyBuilder.ts","../projections/declarative/IFromBuilder.ts","../projections/declarative/IFromEveryBuilder.ts","../projections/declarative/IJoinBuilder.ts","../projections/declarative/INestedBuilder.ts","../projections/declarative/IProjectionBuilder.ts","../projections/declarative/IProjectionBuilderFor.ts","../projections/declarative/IProjectionFor.ts","../projections/declarative/IReadModelPropertiesBuilder.ts","../projections/declarative/IRemovedWithBuilder.ts","../projections/declarative/IRemovedWithJoinBuilder.ts","../projections/declarative/ISetBuilder.ts","../projections/declarative/ISubtractBuilder.ts","../projections/declarative/JoinBuilder.ts","../projections/declarative/ProjectionBuilderFor.ts","../projections/declarative/RemovedWithBuilder.ts","../projections/declarative/RemovedWithJoinBuilder.ts","../projections/declarative/SetBuilder.ts","../projections/declarative/index.ts","../projections/declarative/projection.ts","../projections/modelBound/FromEventMetadata.ts","../projections/modelBound/FromEventOptions.ts","../projections/modelBound/addFrom.ts","../projections/modelBound/childrenFrom.ts","../projections/modelBound/clearWith.ts","../projections/modelBound/count.ts","../projections/modelBound/decrement.ts","../projections/modelBound/fromEvent.ts","../projections/modelBound/fromEvery.ts","../projections/modelBound/increment.ts","../projections/modelBound/index.ts","../projections/modelBound/join.ts","../projections/modelBound/nested.ts","../projections/modelBound/notRewindable.ts","../projections/modelBound/passive.ts","../projections/modelBound/removedWith.ts","../projections/modelBound/removedWithJoin.ts","../projections/modelBound/setFrom.ts","../projections/modelBound/setFromContext.ts","../projections/modelBound/setValue.ts","../projections/modelBound/subtractFrom.ts","../reactors/IReactors.ts","../reactors/ReactorId.ts","../reactors/Reactors.ts","../reactors/index.ts","../reactors/reactor.ts","../readModels/IMaterializedReadModels.ts","../readModels/IReadModels.ts","../readModels/MaterializedReadModels.ts","../readModels/ReadModelChangeset.ts","../readModels/ReadModelId.ts","../readModels/ReadModelSnapshot.ts","../readModels/ReadModels.ts","../readModels/index.ts","../readModels/readModel.ts","../reducers/IReducers.ts","../reducers/ReducerId.ts","../reducers/Reducers.ts","../reducers/index.ts","../reducers/reducer.ts","../schemas/JsonSchema.ts","../schemas/JsonSchemaGenerator.ts","../schemas/index.ts","../schemas/jsonSchemaProperty.ts","../seeding/EventSeeding.ts","../seeding/ICanSeedEvents.ts","../seeding/IEventSeeding.ts","../seeding/IEventSeedingBuilder.ts","../seeding/IEventSeedingScopeBuilder.ts","../seeding/index.ts","../seeding/seeder.ts","../sinks/WellKnownSinks.ts","../sinks/index.ts","../transactions/IUnitOfWork.ts","../transactions/IUnitOfWorkManager.ts","../transactions/NoUnitOfWorkHasBeenStarted.ts","../transactions/UnitOfWork.ts","../transactions/UnitOfWorkManager.ts","../transactions/index.ts","../types/DecoratorType.ts","../types/TypeDiscoverer.ts","../types/TypeIntrospector.ts","../types/index.ts","../webhooks/IWebhook.ts","../webhooks/IWebhookDefinitionBuilder.ts","../webhooks/IWebhooks.ts","../webhooks/WebhookDefinitionBuilder.ts","../webhooks/WebhookId.ts","../webhooks/WebhookTargetUrl.ts","../webhooks/Webhooks.ts","../webhooks/index.ts","../webhooks/webhook.ts"],"version":"6.0.3"}
1
+ {"root":["../ChronicleClient.ts","../ChronicleOptions.ts","../EventStore.ts","../EventStoreName.ts","../EventStoreNamespaceName.ts","../IChronicleClient.ts","../IEventStore.ts","../Metrics.ts","../Tracing.ts","../index.ts","../artifacts/DefaultClientArtifactsProvider.ts","../artifacts/IClientArtifactsProvider.ts","../artifacts/index.ts","../auditing/Causation.ts","../auditing/CausationManager.ts","../auditing/CausationType.ts","../auditing/ICausationManager.ts","../auditing/index.ts","../compliance/ComplianceContracts.ts","../compliance/ComplianceMetadata.ts","../compliance/ComplianceMetadataResolver.ts","../compliance/ComplianceMetadataType.ts","../compliance/index.ts","../compliance/pii.ts","../connection/ChronicleConnection.ts","../connection/ChronicleConnectionString.ts","../connection/ChronicleServerAddressResolver.ts","../connection/ChronicleServices.ts","../connection/ChronicleSrvResolutionError.ts","../connection/ChronicleSrvResolver.ts","../connection/ConnectionLifecycle.ts","../connection/DateTimeOffset.ts","../connection/Guid.ts","../connection/ILoadBalancerStrategy.ts","../connection/KernelKeepAlive.ts","../connection/LeastConnectionsLoadBalancerStrategy.ts","../connection/LoadBalancerMode.ts","../connection/LoadBalancerStrategyFactory.ts","../connection/RandomLoadBalancerStrategy.ts","../connection/RoundRobinLoadBalancerStrategy.ts","../connection/TokenProvider.ts","../connection/fetchOAuthAccessToken.ts","../connection/formatServerAddress.ts","../connection/index.ts","../correlation/CorrelationId.ts","../correlation/CorrelationIdManager.ts","../correlation/ICorrelationIdAccessor.ts","../correlation/ICorrelationIdSetter.ts","../correlation/index.ts","../eventSequences/AppendError.ts","../eventSequences/AppendOptions.ts","../eventSequences/AppendResult.ts","../eventSequences/ConcurrencyScope.ts","../eventSequences/ConstraintViolation.ts","../eventSequences/EventForEventSourceId.ts","../eventSequences/EventLog.ts","../eventSequences/EventSequence.ts","../eventSequences/EventSequenceId.ts","../eventSequences/EventSequenceNumber.ts","../eventSequences/IEventLog.ts","../eventSequences/IEventSequence.ts","../eventSequences/ITransactionalEventSequence.ts","../eventSequences/TransactionalEventSequence.ts","../eventSequences/index.ts","../eventStoreSubscriptions/EventStoreSubscriptionBuilder.ts","../eventStoreSubscriptions/EventStoreSubscriptionDefinition.ts","../eventStoreSubscriptions/EventStoreSubscriptionId.ts","../eventStoreSubscriptions/EventStoreSubscriptions.ts","../eventStoreSubscriptions/IEventStoreSubscriptionBuilder.ts","../eventStoreSubscriptions/IEventStoreSubscriptions.ts","../eventStoreSubscriptions/contracts.ts","../eventStoreSubscriptions/index.ts","../events/AppendedEvent.ts","../events/CausationEntry.ts","../events/EventContext.ts","../events/EventType.ts","../events/EventTypeGeneration.ts","../events/EventTypeId.ts","../events/EventTypes.ts","../events/IEventTypes.ts","../events/eventTypeDecorator.ts","../events/index.ts","../events/constraints/ConstraintBuilder.ts","../events/constraints/ConstraintId.ts","../events/constraints/Constraints.ts","../events/constraints/IConstraint.ts","../events/constraints/IConstraintBuilder.ts","../events/constraints/IConstraints.ts","../events/constraints/IUniqueConstraintBuilder.ts","../events/constraints/UniqueConstraintBuilder.ts","../events/constraints/constraint.ts","../events/constraints/index.ts","../events/migrations/EventMigrationBuilder.ts","../events/migrations/EventMigrationPropertyBuilder.ts","../events/migrations/EventTypeMigrators.ts","../events/migrations/IEventMigrationBuilder.ts","../events/migrations/IEventMigrationPropertyBuilder.ts","../events/migrations/IEventTypeMigration.ts","../events/migrations/IEventTypeMigrators.ts","../events/migrations/InvalidMigrationGenerationGap.ts","../events/migrations/eventTypeMigration.ts","../events/migrations/index.ts","../identity/IIdentityProvider.ts","../identity/Identity.ts","../identity/IdentityProvider.ts","../identity/index.ts","../jobs/IJobs.ts","../jobs/JobId.ts","../jobs/Jobs.ts","../jobs/index.ts","../observation/ObserverId.ts","../observation/ObserverRunningState.ts","../observation/index.ts","../projections/IProjections.ts","../projections/ProjectionId.ts","../projections/Projections.ts","../projections/index.ts","../projections/declarative/AllSetBuilder.ts","../projections/declarative/FromBuilder.ts","../projections/declarative/FromEveryBuilder.ts","../projections/declarative/IAddBuilder.ts","../projections/declarative/IAddChildBuilder.ts","../projections/declarative/IAllSetBuilder.ts","../projections/declarative/IChildrenBuilder.ts","../projections/declarative/ICompositeKeyBuilder.ts","../projections/declarative/IFromBuilder.ts","../projections/declarative/IFromEveryBuilder.ts","../projections/declarative/IJoinBuilder.ts","../projections/declarative/INestedBuilder.ts","../projections/declarative/IProjectionBuilder.ts","../projections/declarative/IProjectionBuilderFor.ts","../projections/declarative/IProjectionFor.ts","../projections/declarative/IReadModelPropertiesBuilder.ts","../projections/declarative/IRemovedWithBuilder.ts","../projections/declarative/IRemovedWithJoinBuilder.ts","../projections/declarative/ISetBuilder.ts","../projections/declarative/ISubtractBuilder.ts","../projections/declarative/JoinBuilder.ts","../projections/declarative/ProjectionBuilderFor.ts","../projections/declarative/RemovedWithBuilder.ts","../projections/declarative/RemovedWithJoinBuilder.ts","../projections/declarative/SetBuilder.ts","../projections/declarative/index.ts","../projections/declarative/projection.ts","../projections/modelBound/FromEventMetadata.ts","../projections/modelBound/FromEventOptions.ts","../projections/modelBound/addFrom.ts","../projections/modelBound/childrenFrom.ts","../projections/modelBound/clearWith.ts","../projections/modelBound/count.ts","../projections/modelBound/decrement.ts","../projections/modelBound/fromEvent.ts","../projections/modelBound/fromEvery.ts","../projections/modelBound/increment.ts","../projections/modelBound/index.ts","../projections/modelBound/join.ts","../projections/modelBound/nested.ts","../projections/modelBound/notRewindable.ts","../projections/modelBound/passive.ts","../projections/modelBound/removedWith.ts","../projections/modelBound/removedWithJoin.ts","../projections/modelBound/setFrom.ts","../projections/modelBound/setFromContext.ts","../projections/modelBound/setValue.ts","../projections/modelBound/subtractFrom.ts","../reactors/IReactors.ts","../reactors/ReactorId.ts","../reactors/Reactors.ts","../reactors/index.ts","../reactors/reactor.ts","../readModels/IMaterializedReadModels.ts","../readModels/IReadModels.ts","../readModels/MaterializedReadModels.ts","../readModels/ReadModelChangeset.ts","../readModels/ReadModelId.ts","../readModels/ReadModelSnapshot.ts","../readModels/ReadModels.ts","../readModels/index.ts","../readModels/readModel.ts","../reducers/IReducers.ts","../reducers/ReducerId.ts","../reducers/Reducers.ts","../reducers/index.ts","../reducers/reducer.ts","../schemas/JsonSchema.ts","../schemas/JsonSchemaGenerator.ts","../schemas/index.ts","../schemas/jsonSchemaProperty.ts","../seeding/EventSeeding.ts","../seeding/ICanSeedEvents.ts","../seeding/IEventSeeding.ts","../seeding/IEventSeedingBuilder.ts","../seeding/IEventSeedingScopeBuilder.ts","../seeding/index.ts","../seeding/seeder.ts","../sinks/WellKnownSinks.ts","../sinks/index.ts","../transactions/IUnitOfWork.ts","../transactions/IUnitOfWorkManager.ts","../transactions/NoUnitOfWorkHasBeenStarted.ts","../transactions/UnitOfWork.ts","../transactions/UnitOfWorkManager.ts","../transactions/index.ts","../types/DecoratorType.ts","../types/TypeDiscoverer.ts","../types/TypeIntrospector.ts","../types/index.ts","../webhooks/IWebhook.ts","../webhooks/IWebhookDefinitionBuilder.ts","../webhooks/IWebhooks.ts","../webhooks/WebhookDefinitionBuilder.ts","../webhooks/WebhookId.ts","../webhooks/WebhookTargetUrl.ts","../webhooks/Webhooks.ts","../webhooks/index.ts","../webhooks/webhook.ts"],"version":"6.0.3"}
@@ -0,0 +1,31 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { readFileSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { describe, expect, it } from 'vitest';
8
+
9
+ // The peer range's lower bound is a compatibility claim; the exact devDependency pin is the only version this
10
+ // package is ever compiled and tested against. When they disagree the range claims support for versions nothing
11
+ // verifies, and nothing fails until a consumer resolves one of them.
12
+ //
13
+ // That is not hypothetical: the range shipped as ^7 while ConceptAs - used here as a runtime value, compared by
14
+ // class object in JsonSchemaGenerator - only exists from 7.14.0 onward, leaving 48 published versions that
15
+ // satisfied the range and could not link. Tying the bound to the pin is what stops the two drifting again.
16
+ const manifest = JSON.parse(
17
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf-8')) as {
18
+ peerDependencies?: Record<string, string>;
19
+ devDependencies?: Record<string, string>;
20
+ };
21
+
22
+ describe('fundamentals peer range', () => {
23
+ const peer = manifest.peerDependencies?.['@cratis/fundamentals'];
24
+ const pinned = manifest.devDependencies?.['@cratis/fundamentals'];
25
+
26
+ it('should declare fundamentals as a peer dependency', () => expect(peer).toBeDefined());
27
+
28
+ it('should pin one concrete version to build against', () => expect(pinned).toMatch(/^\d+\.\d+\.\d+$/));
29
+
30
+ it('should admit no version below the one it builds against', () => expect(peer).toBe(`^${pinned}`));
31
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/chronicle",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "TypeScript idiomatic client for Cratis Chronicle",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
@@ -136,7 +136,6 @@
136
136
  "dependencies": {
137
137
  "@bufbuild/protobuf": "^2.12.0",
138
138
  "@cratis/chronicle.contracts": "16.4.0",
139
- "@cratis/fundamentals": "7.14.0",
140
139
  "@grpc/grpc-js": "^1.14.4",
141
140
  "@opentelemetry/api": "^1.9.1",
142
141
  "nice-grpc": "^2.1.16",
@@ -145,8 +144,12 @@
145
144
  "undici": "^8.7.0"
146
145
  },
147
146
  "devDependencies": {
147
+ "@cratis/fundamentals": "7.14.0",
148
148
  "@types/node": "^25.9.1",
149
149
  "typescript": "^6.0.3",
150
150
  "vitest": "^4.1.10"
151
+ },
152
+ "peerDependencies": {
153
+ "@cratis/fundamentals": "^7.14.0"
151
154
  }
152
155
  }