@cratis/chronicle 6.9.1 → 6.10.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/ChronicleClient.ts +52 -13
- package/ChronicleOptions.spec.ts +54 -0
- package/ChronicleOptions.ts +20 -5
- package/EventStore.ts +7 -1
- package/README.md +2 -3
- package/artifacts/DefaultClientArtifactsProvider.ts +4 -1
- package/connection/ChronicleConnection.auth.spec.ts +219 -0
- package/connection/ChronicleConnection.ts +70 -31
- package/connection/ChronicleServices.ts +1 -1
- package/connection/CompatibilityPreflight.ts +1 -1
- package/connection/RejectedChronicleCredentials.ts +10 -0
- package/connection/TokenProvider.spec.ts +5 -6
- package/connection/TokenProvider.ts +26 -17
- package/connection/clientVersion.spec.ts +27 -0
- package/connection/clientVersion.ts +9 -0
- package/connection/describeRequestError.spec.ts +22 -0
- package/connection/fetchOAuthAccessToken.ts +33 -2
- package/dist/ChronicleClient.d.ts +3 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +55 -13
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +5 -1
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +20 -5
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/EventStore.d.ts +2 -0
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +6 -1
- package/dist/EventStore.js.map +1 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
- package/dist/connection/ChronicleConnection.d.ts +3 -3
- package/dist/connection/ChronicleConnection.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.js +74 -26
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/ChronicleServices.d.ts +1 -2
- package/dist/connection/ChronicleServices.d.ts.map +1 -1
- package/dist/connection/CompatibilityPreflight.js +1 -1
- package/dist/connection/CompatibilityPreflight.js.map +1 -1
- package/dist/connection/RejectedChronicleCredentials.d.ts +5 -0
- package/dist/connection/RejectedChronicleCredentials.d.ts.map +1 -0
- package/dist/connection/RejectedChronicleCredentials.js +10 -0
- package/dist/connection/RejectedChronicleCredentials.js.map +1 -0
- package/dist/connection/TokenProvider.d.ts +8 -6
- package/dist/connection/TokenProvider.d.ts.map +1 -1
- package/dist/connection/TokenProvider.js +25 -11
- package/dist/connection/TokenProvider.js.map +1 -1
- package/dist/connection/clientVersion.d.ts +3 -0
- package/dist/connection/clientVersion.d.ts.map +1 -0
- package/dist/connection/clientVersion.js +7 -0
- package/dist/connection/clientVersion.js.map +1 -0
- package/dist/connection/fetchOAuthAccessToken.d.ts +12 -0
- package/dist/connection/fetchOAuthAccessToken.d.ts.map +1 -1
- package/dist/connection/fetchOAuthAccessToken.js +37 -2
- package/dist/connection/fetchOAuthAccessToken.js.map +1 -1
- package/dist/externalServices/ExternalServiceBuilder.d.ts +1 -1
- package/dist/externalServices/ExternalServiceBuilder.d.ts.map +1 -1
- package/dist/externalServices/ExternalServiceBuilder.js +1 -1
- package/dist/externalServices/ExternalServiceBuilder.js.map +1 -1
- package/dist/externalServices/ExternalServices.d.ts.map +1 -1
- package/dist/externalServices/ExternalServices.js +3 -1
- package/dist/externalServices/ExternalServices.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +9 -7
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/constantValueExpression.d.ts +3 -0
- package/dist/projections/constantValueExpression.d.ts.map +1 -0
- package/dist/projections/constantValueExpression.js +26 -0
- package/dist/projections/constantValueExpression.js.map +1 -0
- package/dist/projections/declarative/ChildrenBuilder.d.ts +2 -0
- package/dist/projections/declarative/ChildrenBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/ChildrenBuilder.js +5 -0
- package/dist/projections/declarative/ChildrenBuilder.js.map +1 -1
- package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/FromBuilder.js +3 -2
- package/dist/projections/declarative/FromBuilder.js.map +1 -1
- package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/JoinBuilder.js +2 -1
- package/dist/projections/declarative/JoinBuilder.js.map +1 -1
- package/dist/projections/declarative/NestedBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/NestedBuilder.js +3 -1
- package/dist/projections/declarative/NestedBuilder.js.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderCore.d.ts +5 -0
- package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderCore.js +18 -2
- package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts +6 -0
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.js +5 -0
- package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
- package/dist/projections/declarative/SetBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/SetBuilder.js +2 -1
- package/dist/projections/declarative/SetBuilder.js.map +1 -1
- package/dist/projections/modelBound/addFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/addFrom.js +3 -2
- package/dist/projections/modelBound/addFrom.js.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts +20 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.js +14 -11
- package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
- package/dist/projections/modelBound/childrenFrom.d.ts +4 -2
- package/dist/projections/modelBound/childrenFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenFrom.js +8 -4
- package/dist/projections/modelBound/childrenFrom.js.map +1 -1
- package/dist/projections/modelBound/clearWith.d.ts.map +1 -1
- package/dist/projections/modelBound/clearWith.js +3 -2
- package/dist/projections/modelBound/clearWith.js.map +1 -1
- package/dist/projections/modelBound/count.d.ts.map +1 -1
- package/dist/projections/modelBound/count.js +3 -2
- package/dist/projections/modelBound/count.js.map +1 -1
- package/dist/projections/modelBound/decrement.d.ts.map +1 -1
- package/dist/projections/modelBound/decrement.js +3 -2
- package/dist/projections/modelBound/decrement.js.map +1 -1
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
- package/dist/projections/modelBound/fromAll.js +3 -2
- package/dist/projections/modelBound/fromAll.js.map +1 -1
- package/dist/projections/modelBound/fromEvery.d.ts.map +1 -1
- package/dist/projections/modelBound/fromEvery.js +3 -2
- package/dist/projections/modelBound/fromEvery.js.map +1 -1
- package/dist/projections/modelBound/increment.d.ts.map +1 -1
- package/dist/projections/modelBound/increment.js +3 -2
- package/dist/projections/modelBound/increment.js.map +1 -1
- package/dist/projections/modelBound/join.d.ts.map +1 -1
- package/dist/projections/modelBound/join.js +3 -2
- package/dist/projections/modelBound/join.js.map +1 -1
- package/dist/projections/modelBound/nested.d.ts.map +1 -1
- package/dist/projections/modelBound/nested.js +3 -2
- package/dist/projections/modelBound/nested.js.map +1 -1
- package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -1
- package/dist/projections/modelBound/noAutoMap.js +3 -2
- package/dist/projections/modelBound/noAutoMap.js.map +1 -1
- package/dist/projections/modelBound/removedWith.d.ts.map +1 -1
- package/dist/projections/modelBound/removedWith.js +3 -2
- package/dist/projections/modelBound/removedWith.js.map +1 -1
- package/dist/projections/modelBound/removedWithJoin.d.ts.map +1 -1
- package/dist/projections/modelBound/removedWithJoin.js +3 -2
- package/dist/projections/modelBound/removedWithJoin.js.map +1 -1
- package/dist/projections/modelBound/setFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/setFrom.js +3 -2
- package/dist/projections/modelBound/setFrom.js.map +1 -1
- package/dist/projections/modelBound/setFromContext.d.ts.map +1 -1
- package/dist/projections/modelBound/setFromContext.js +3 -2
- package/dist/projections/modelBound/setFromContext.js.map +1 -1
- package/dist/projections/modelBound/setValue.d.ts.map +1 -1
- package/dist/projections/modelBound/setValue.js +3 -2
- package/dist/projections/modelBound/setValue.js.map +1 -1
- package/dist/projections/modelBound/subtractFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/subtractFrom.js +3 -2
- package/dist/projections/modelBound/subtractFrom.js.map +1 -1
- package/dist/projections/notSetPropertyPath.d.ts +3 -0
- package/dist/projections/notSetPropertyPath.d.ts.map +1 -0
- package/dist/projections/notSetPropertyPath.js +5 -0
- package/dist/projections/notSetPropertyPath.js.map +1 -0
- package/dist/reactors/Reactors.d.ts +4 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +24 -6
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reactors/reactor.d.ts +2 -2
- package/dist/reactors/reactor.js +2 -2
- package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
- package/dist/readModels/MaterializedReadModels.js +2 -4
- package/dist/readModels/MaterializedReadModels.js.map +1 -1
- package/dist/readModels/ReadModels.d.ts +2 -1
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +16 -10
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/deserializeReadModel.d.ts +5 -0
- package/dist/readModels/deserializeReadModel.d.ts.map +1 -0
- package/dist/readModels/deserializeReadModel.js +84 -0
- package/dist/readModels/deserializeReadModel.js.map +1 -0
- package/dist/readModels/rootReadModelTypes.d.ts +5 -0
- package/dist/readModels/rootReadModelTypes.d.ts.map +1 -0
- package/dist/readModels/rootReadModelTypes.js +69 -0
- package/dist/readModels/rootReadModelTypes.js.map +1 -0
- package/dist/reducers/Reducers.d.ts +4 -0
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +24 -6
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +4 -4
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/TypeDiscoverer.d.ts +8 -1
- package/dist/types/TypeDiscoverer.d.ts.map +1 -1
- package/dist/types/TypeDiscoverer.js +25 -5
- package/dist/types/TypeDiscoverer.js.map +1 -1
- package/dist/types/TypeIntrospector.d.ts.map +1 -1
- package/dist/types/TypeIntrospector.js +3 -2
- package/dist/types/TypeIntrospector.js.map +1 -1
- package/dist/types/conceptValueType.d.ts +4 -0
- package/dist/types/conceptValueType.d.ts.map +1 -0
- package/dist/types/conceptValueType.js +10 -0
- package/dist/types/conceptValueType.js.map +1 -0
- package/dist/types/modelBoundProperty.d.ts +6 -0
- package/dist/types/modelBoundProperty.d.ts.map +1 -0
- package/dist/types/modelBoundProperty.js +53 -0
- package/dist/types/modelBoundProperty.js.map +1 -0
- package/externalServices/ExternalServiceBuilder.ts +4 -4
- package/externalServices/ExternalServices.spec.ts +87 -0
- package/externalServices/ExternalServices.ts +3 -1
- package/for_ChronicleClient/connectionAuthentication.spec.ts +67 -0
- package/index.ts +1 -0
- package/observation/observationDisposal.spec.ts +74 -0
- package/package.json +2 -1
- package/projections/Projections.childrenAndNested.spec.ts +2 -0
- package/projections/Projections.modelBoundCompleteness.spec.ts +47 -0
- package/projections/Projections.ts +9 -7
- package/projections/constantValueExpression.spec.ts +31 -0
- package/projections/constantValueExpression.ts +28 -0
- package/projections/declarative/ChildrenBuilder.spec.ts +46 -1
- package/projections/declarative/ChildrenBuilder.ts +6 -0
- package/projections/declarative/FromBuilder.ts +3 -2
- package/projections/declarative/JoinBuilder.ts +2 -1
- package/projections/declarative/NestedBuilder.ts +3 -1
- package/projections/declarative/ProjectionBuilderCore.ts +20 -2
- package/projections/declarative/ProjectionBuilderFor.spec.ts +61 -4
- package/projections/declarative/ProjectionBuilderFor.ts +7 -0
- package/projections/declarative/SetBuilder.ts +2 -1
- package/projections/modelBound/addFrom.ts +3 -2
- package/projections/modelBound/childrenAndNestedBuilder.ts +14 -11
- package/projections/modelBound/childrenFrom.ts +11 -5
- package/projections/modelBound/clearWith.ts +3 -2
- package/projections/modelBound/count.ts +3 -2
- package/projections/modelBound/decorators.standard.spec.ts +45 -0
- package/projections/modelBound/decrement.ts +3 -2
- package/projections/modelBound/fromAll.ts +3 -2
- package/projections/modelBound/fromEvery.ts +3 -2
- package/projections/modelBound/increment.ts +3 -2
- package/projections/modelBound/join.ts +3 -2
- package/projections/modelBound/nested.ts +3 -2
- package/projections/modelBound/noAutoMap.ts +3 -2
- package/projections/modelBound/removedWith.ts +3 -2
- package/projections/modelBound/removedWithJoin.ts +3 -2
- package/projections/modelBound/setFrom.ts +3 -2
- package/projections/modelBound/setFromContext.ts +3 -2
- package/projections/modelBound/setValue.ts +3 -2
- package/projections/modelBound/subtractFrom.ts +3 -2
- package/projections/notSetPropertyPath.ts +5 -0
- package/reactors/Reactors.ts +22 -7
- package/reactors/reactor.ts +2 -2
- package/readModels/MaterializedReadModels.spec.ts +22 -0
- package/readModels/MaterializedReadModels.ts +2 -4
- package/readModels/ReadModels.spec.ts +58 -1
- package/readModels/ReadModels.ts +17 -11
- package/readModels/conceptValueType.spec.ts +20 -0
- package/readModels/deserializeReadModel.spec.ts +84 -0
- package/readModels/deserializeReadModel.ts +82 -0
- package/readModels/discovery.spec.ts +27 -0
- package/readModels/rootReadModelTypes.spec.ts +115 -0
- package/readModels/rootReadModelTypes.ts +65 -0
- package/reducers/Reducers.ts +22 -7
- package/schemas/JsonSchemaGenerator.ts +4 -4
- package/schemas/legacyDesignMetadata.spec.ts +24 -2
- package/types/TypeDiscoverer.spec.ts +57 -0
- package/types/TypeDiscoverer.ts +27 -6
- package/types/TypeIntrospector.ts +3 -2
- package/types/conceptValueType.ts +11 -0
- package/types/modelBoundProperty.ts +53 -0
- package/dist/externalServices/ExternalServicesContracts.d.ts +0 -137
- package/dist/externalServices/ExternalServicesContracts.d.ts.map +0 -1
- package/dist/externalServices/ExternalServicesContracts.js +0 -654
- package/dist/externalServices/ExternalServicesContracts.js.map +0 -1
- package/externalServices/ExternalServicesContracts.ts +0 -709
package/ChronicleClient.ts
CHANGED
|
@@ -7,8 +7,11 @@ import { SpanStatusCode } from '@opentelemetry/api';
|
|
|
7
7
|
import { ChronicleOptions } from './ChronicleOptions.js';
|
|
8
8
|
import { ChronicleConnection } from './connection/index.js';
|
|
9
9
|
import { IncompatibleChronicleServer } from './connection/IncompatibleChronicleServer.js';
|
|
10
|
+
import { OAuthTokenHttpError } from './connection/fetchOAuthAccessToken.js';
|
|
11
|
+
import { RejectedChronicleCredentials } from './connection/RejectedChronicleCredentials.js';
|
|
10
12
|
import { ensureCommandSuccess, ensureQuerySuccess } from './connection/callResults.js';
|
|
11
13
|
import { ConnectionLifecycle } from './connection/ConnectionLifecycle.js';
|
|
14
|
+
import { clientVersion } from './connection/clientVersion.js';
|
|
12
15
|
import { KernelKeepAlive } from './connection/KernelKeepAlive.js';
|
|
13
16
|
import { EventStore } from './EventStore.js';
|
|
14
17
|
import { EventStoreName } from './EventStoreName.js';
|
|
@@ -52,6 +55,7 @@ export class ChronicleClient implements IChronicleClient {
|
|
|
52
55
|
private _keepAliveAbortController?: AbortController;
|
|
53
56
|
private _healthCheckInFlight = false;
|
|
54
57
|
private _connectionFailure?: Error;
|
|
58
|
+
private _consecutiveCredentialRejections = 0;
|
|
55
59
|
|
|
56
60
|
/**
|
|
57
61
|
* Creates a new {@link ChronicleClient} using the provided options.
|
|
@@ -194,6 +198,7 @@ export class ChronicleClient implements IChronicleClient {
|
|
|
194
198
|
/** @inheritdoc */
|
|
195
199
|
dispose(): void {
|
|
196
200
|
this._isDisposed = true;
|
|
201
|
+
for (const store of this._stores.values()) store.disposeObservations();
|
|
197
202
|
|
|
198
203
|
if (this._watchdogHandle) {
|
|
199
204
|
clearInterval(this._watchdogHandle);
|
|
@@ -232,6 +237,7 @@ export class ChronicleClient implements IChronicleClient {
|
|
|
232
237
|
// so this effectively waits until the channel reaches READY or fails.
|
|
233
238
|
await this._connection.server.getVersionInfo({}, { signal: AbortSignal.timeout(10_000) });
|
|
234
239
|
|
|
240
|
+
this._consecutiveCredentialRejections = 0;
|
|
235
241
|
this._logger.info('Connected to Chronicle kernel');
|
|
236
242
|
await this.startKernelKeepAlive();
|
|
237
243
|
await this._lifecycle.connected(error => {
|
|
@@ -243,9 +249,10 @@ export class ChronicleClient implements IChronicleClient {
|
|
|
243
249
|
|
|
244
250
|
|
|
245
251
|
} catch (error) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
252
|
+
const terminal = this.terminalConnectionError(error);
|
|
253
|
+
if (terminal) {
|
|
254
|
+
this.failConnection(terminal);
|
|
255
|
+
throw terminal;
|
|
249
256
|
}
|
|
250
257
|
attempt++;
|
|
251
258
|
const delayMs = await this.backOff(attempt, 'Connection attempt failed, retrying', error);
|
|
@@ -303,6 +310,7 @@ export class ChronicleClient implements IChronicleClient {
|
|
|
303
310
|
await this._connection.resetChannel();
|
|
304
311
|
await this._connection.connect();
|
|
305
312
|
await this._connection.server.getVersionInfo({}, { signal: AbortSignal.timeout(10_000) });
|
|
313
|
+
this._consecutiveCredentialRejections = 0;
|
|
306
314
|
this._logger.info('Reconnected to Chronicle kernel', { attempt: attempt + 1 });
|
|
307
315
|
await this.startKernelKeepAlive();
|
|
308
316
|
await this._lifecycle.connected(connectedError => {
|
|
@@ -312,9 +320,10 @@ export class ChronicleClient implements IChronicleClient {
|
|
|
312
320
|
});
|
|
313
321
|
return;
|
|
314
322
|
} catch (reconnectError) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
323
|
+
const terminal = this.terminalConnectionError(reconnectError);
|
|
324
|
+
if (terminal) {
|
|
325
|
+
this.failConnection(terminal);
|
|
326
|
+
throw terminal;
|
|
318
327
|
}
|
|
319
328
|
attempt++;
|
|
320
329
|
await this.backOff(attempt, 'Reconnect attempt failed, retrying', reconnectError);
|
|
@@ -351,17 +360,49 @@ export class ChronicleClient implements IChronicleClient {
|
|
|
351
360
|
try {
|
|
352
361
|
return await action();
|
|
353
362
|
} catch (error) {
|
|
354
|
-
|
|
355
|
-
|
|
363
|
+
const terminal = this.terminalConnectionError(error);
|
|
364
|
+
if (terminal) {
|
|
365
|
+
this.failConnection(terminal);
|
|
366
|
+
throw terminal;
|
|
356
367
|
}
|
|
368
|
+
if (!this.shouldReconnect(error)) throw error;
|
|
357
369
|
|
|
358
370
|
await this.reconnect(operation, error);
|
|
359
|
-
|
|
371
|
+
try {
|
|
372
|
+
return await action();
|
|
373
|
+
} catch (retryError) {
|
|
374
|
+
const retryTerminal = this.terminalConnectionError(retryError);
|
|
375
|
+
if (retryTerminal) {
|
|
376
|
+
this.failConnection(retryTerminal);
|
|
377
|
+
throw retryTerminal;
|
|
378
|
+
}
|
|
379
|
+
throw retryError;
|
|
380
|
+
}
|
|
360
381
|
}
|
|
361
382
|
}
|
|
362
383
|
|
|
384
|
+
private terminalConnectionError(error: unknown): Error | undefined {
|
|
385
|
+
if (error instanceof IncompatibleChronicleServer || error instanceof RejectedChronicleCredentials) return error;
|
|
386
|
+
const rejection = (error as { code?: number })?.code === 16 &&
|
|
387
|
+
(this.options.connectionString.apiKey || this.hasOAuthCredentialRejection(error));
|
|
388
|
+
this._consecutiveCredentialRejections = rejection ? this._consecutiveCredentialRejections + 1 : 0;
|
|
389
|
+
if (this._consecutiveCredentialRejections === 3) {
|
|
390
|
+
return new RejectedChronicleCredentials(`Chronicle credentials were rejected: ${this.toErrorMessage(error)}`, { cause: error });
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private hasOAuthCredentialRejection(error: unknown): boolean {
|
|
396
|
+
const tokenError = (error as { tokenFailure?: Error })?.tokenFailure;
|
|
397
|
+
const httpError = tokenError?.cause;
|
|
398
|
+
return httpError instanceof OAuthTokenHttpError && [400, 401].includes(httpError.statusCode) &&
|
|
399
|
+
['invalid_client', 'unauthorized_client', 'invalid_grant'].includes(httpError.errorCode ?? '');
|
|
400
|
+
}
|
|
401
|
+
|
|
363
402
|
private shouldReconnect(error: unknown): boolean {
|
|
364
|
-
if (error instanceof IncompatibleChronicleServer) return false;
|
|
403
|
+
if (error instanceof IncompatibleChronicleServer || error instanceof RejectedChronicleCredentials) return false;
|
|
404
|
+
if ((error as { code?: number })?.code === 16 &&
|
|
405
|
+
(this.options.connectionString.apiKey || !!(error as { tokenFailure?: Error })?.tokenFailure)) return true;
|
|
365
406
|
const code = Number((error as { code?: number })?.code ?? -1);
|
|
366
407
|
const details = String((error as { details?: string })?.details ?? '');
|
|
367
408
|
const message = this.toErrorMessage(error);
|
|
@@ -439,9 +480,7 @@ export class ChronicleClient implements IChronicleClient {
|
|
|
439
480
|
await keepAlive.start(
|
|
440
481
|
{
|
|
441
482
|
ConnectionId: this._lifecycle.connectionId,
|
|
442
|
-
|
|
443
|
-
// the pre-existing hardcoded placeholder until such a mechanism exists.
|
|
444
|
-
ClientVersion: '1.0.0',
|
|
483
|
+
ClientVersion: clientVersion,
|
|
445
484
|
IsRunningWithDebugger: false,
|
|
446
485
|
ProcessId: process.pid,
|
|
447
486
|
ProcessPath: process.execPath,
|
|
@@ -0,0 +1,54 @@
|
|
|
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 } from 'vitest';
|
|
5
|
+
import { ChronicleOptions } from './ChronicleOptions.js';
|
|
6
|
+
|
|
7
|
+
const entry = process.argv[1];
|
|
8
|
+
const execArgv = [...process.execArgv];
|
|
9
|
+
const vitest = process.env.VITEST;
|
|
10
|
+
const patterns = [
|
|
11
|
+
'**/*.ts', '**/*.tsx', '!**/*.d.ts', '!**/node_modules', '!**/dist', '!**/build',
|
|
12
|
+
'!**/.git', '!**/.vscode', '!**/*.spec.ts', '!**/*.test.ts', '!**/*.spec.tsx', '!**/*.test.tsx'
|
|
13
|
+
];
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
process.argv[1] = entry;
|
|
16
|
+
process.execArgv = [...execArgv];
|
|
17
|
+
if (vitest === undefined) delete process.env.VITEST;
|
|
18
|
+
else process.env.VITEST = vitest;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('default artifact discovery', () => {
|
|
22
|
+
for (const extension of ['ts', 'tsx', 'mts', 'cts']) {
|
|
23
|
+
it(`scans TypeScript sources for a .${extension} entry`, () => {
|
|
24
|
+
process.argv[1] = `/app/index.${extension}`;
|
|
25
|
+
expect(ChronicleOptions.development().discoveryPatterns).toEqual(patterns);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it('scans a JavaScript entry under vitest', () => {
|
|
30
|
+
process.argv[1] = '/app/dist/index.js';
|
|
31
|
+
process.env.VITEST = 'true';
|
|
32
|
+
expect(ChronicleOptions.development().discoveryPatterns).toEqual(patterns);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each(['tsx', 'ts-node', '--experimental-strip-types', '--experimental-transform-types'])(
|
|
36
|
+
'scans a JavaScript entry with %s loader', loader => {
|
|
37
|
+
process.argv[1] = '/app/dist/index.js';
|
|
38
|
+
process.execArgv = [loader];
|
|
39
|
+
expect(ChronicleOptions.development().discoveryPatterns).toEqual(patterns);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not scan files for a compiled JavaScript entry without a TypeScript runtime', () => {
|
|
43
|
+
process.argv[1] = '/app/dist/index.js';
|
|
44
|
+
delete process.env.VITEST;
|
|
45
|
+
process.execArgv = [];
|
|
46
|
+
expect(ChronicleOptions.development().discoveryPatterns).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('always honors explicit discovery patterns', () => {
|
|
50
|
+
process.argv[1] = '/app/dist/index.js';
|
|
51
|
+
const patterns = ['app/**/*.js', '!app/**/*.spec.js'];
|
|
52
|
+
expect(ChronicleOptions.development({ discoveryPatterns: patterns }).discoveryPatterns).toBe(patterns);
|
|
53
|
+
});
|
|
54
|
+
});
|
package/ChronicleOptions.ts
CHANGED
|
@@ -56,7 +56,10 @@ export class ChronicleOptions {
|
|
|
56
56
|
/**
|
|
57
57
|
* Glob patterns used to discover artifact files at startup.
|
|
58
58
|
* Patterns prefixed with '!' are treated as exclusions.
|
|
59
|
-
*
|
|
59
|
+
* By default, TypeScript entry files (.ts, .tsx, .mts, .cts) or TypeScript runtimes scan TypeScript sources;
|
|
60
|
+
* compiled JavaScript entry files do not scan (imported modules register their artifacts).
|
|
61
|
+
* Explicit patterns are used regardless of the entry file. Set to an empty array to
|
|
62
|
+
* disable automatic file discovery.
|
|
60
63
|
*/
|
|
61
64
|
readonly discoveryPatterns: string[];
|
|
62
65
|
|
|
@@ -76,8 +79,20 @@ export class ChronicleOptions {
|
|
|
76
79
|
this.softwareVersion = options.softwareVersion ?? '0.0.0';
|
|
77
80
|
this.softwareCommit = options.softwareCommit ?? 'Unknown';
|
|
78
81
|
this.clientArtifactsProvider = options.clientArtifactsProvider ?? DefaultClientArtifactsProvider.default;
|
|
79
|
-
this.discoveryPatterns = options.discoveryPatterns ??
|
|
82
|
+
this.discoveryPatterns = options.discoveryPatterns ?? ChronicleOptions.defaultDiscoveryPatterns();
|
|
83
|
+
this.defaultSinkTypeId = options.defaultSinkTypeId ?? WellKnownSinks.MongoDB;
|
|
84
|
+
this.reactorResultHandler = options.reactorResultHandler;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private static defaultDiscoveryPatterns(): string[] {
|
|
88
|
+
const typescriptEntry = /\.(?:ts|tsx|mts|cts)$/i.test(process.argv[1] ?? '');
|
|
89
|
+
const typescriptLoader = process.execArgv.some(arg => /(?:tsx|ts-node|--experimental-strip-types|--experimental-transform-types)/i.test(arg));
|
|
90
|
+
// process.features.typescript is set on every Node.js 24+ process (type stripping), so it
|
|
91
|
+
// does not mean the program was started from TypeScript; it is deliberately not consulted.
|
|
92
|
+
if (!typescriptEntry && !process.env.VITEST && !typescriptLoader) return [];
|
|
93
|
+
return [
|
|
80
94
|
'**/*.ts',
|
|
95
|
+
'**/*.tsx',
|
|
81
96
|
'!**/*.d.ts',
|
|
82
97
|
'!**/node_modules',
|
|
83
98
|
'!**/dist',
|
|
@@ -85,10 +100,10 @@ export class ChronicleOptions {
|
|
|
85
100
|
'!**/.git',
|
|
86
101
|
'!**/.vscode',
|
|
87
102
|
'!**/*.spec.ts',
|
|
88
|
-
'!**/*.test.ts'
|
|
103
|
+
'!**/*.test.ts',
|
|
104
|
+
'!**/*.spec.tsx',
|
|
105
|
+
'!**/*.test.tsx'
|
|
89
106
|
];
|
|
90
|
-
this.defaultSinkTypeId = options.defaultSinkTypeId ?? WellKnownSinks.MongoDB;
|
|
91
|
-
this.reactorResultHandler = options.reactorResultHandler;
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
/**
|
package/EventStore.ts
CHANGED
|
@@ -101,7 +101,7 @@ export class EventStore implements IEventStore {
|
|
|
101
101
|
this.projections = new Projections(name.value, namespace.value, _connection, artifacts, defaultSinkTypeId);
|
|
102
102
|
this.reactors = new Reactors(artifacts, _connection, name.value, namespace.value, lifecycle, this.eventLog, reactorResultHandler);
|
|
103
103
|
this.reducers = new Reducers(artifacts, _connection, name.value, namespace.value, lifecycle, defaultSinkTypeId);
|
|
104
|
-
this.readModels = new ReadModels(name.value, namespace.value, _connection, artifacts, defaultSinkTypeId);
|
|
104
|
+
this.readModels = new ReadModels(name.value, namespace.value, _connection, artifacts, defaultSinkTypeId, readModelType => this.projections.hasForModel(readModelType));
|
|
105
105
|
this.jobs = new Jobs(name.value, namespace.value, _connection);
|
|
106
106
|
this.webhooks = new Webhooks(name.value, _connection, this.eventTypes, artifacts);
|
|
107
107
|
this.subscriptions = new EventStoreSubscriptions(this.eventTypes, name.value, _connection);
|
|
@@ -157,6 +157,12 @@ export class EventStore implements IEventStore {
|
|
|
157
157
|
});
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/** Stops the store's long-lived observations when the client is disposed. */
|
|
161
|
+
disposeObservations(): void {
|
|
162
|
+
(this.reactors as Reactors).dispose();
|
|
163
|
+
(this.reducers as Reducers).dispose();
|
|
164
|
+
}
|
|
165
|
+
|
|
160
166
|
/** @inheritdoc */
|
|
161
167
|
getEventSequence(id: EventSequenceId): IEventSequence {
|
|
162
168
|
const existing = this._sequences.get(id.value);
|
package/README.md
CHANGED
|
@@ -59,8 +59,7 @@ class EmployeeHired {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
const client = new ChronicleClient(ChronicleOptions.development({ discoveryPatterns: [] }));
|
|
62
|
+
const client = new ChronicleClient(ChronicleOptions.development());
|
|
64
63
|
const store = await client.getEventStore('MyStore');
|
|
65
64
|
const result = await store.eventLog.append('employee-123', new EmployeeHired('Jane', 'Doe'));
|
|
66
65
|
console.log(`Appended at sequence number ${result.sequenceNumber.value}`);
|
|
@@ -73,7 +72,7 @@ Chronicle decorators work with both TC39 standard decorators (TypeScript 5.2+; d
|
|
|
73
72
|
|
|
74
73
|
Read models are inferred from `@projection('id', Model)`, `@reducer('id', sequenceId, Model)`, or an exported model with `@fromEvent(Event)` or other model-bound property mappings. Their schema comes from the model type; the default identifier is its class name. See [read models](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/read-models.md) for preserving existing custom identifiers.
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
Decorated artifacts register when their modules are imported. When the entry file is TypeScript (run through a loader such as `tsx`), the client also scans `**/*.ts` files by default; compiled JavaScript scans nothing unless you set `discoveryPatterns`. See [artifact discovery](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/getting-started.md#artifact-discovery).
|
|
77
76
|
|
|
78
77
|
The client skips TLS certificate validation unless the connection string sets `skipTlsValidation=false`. Set it for every server other than a local development kernel; see [Connect to Chronicle](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/connecting.md).
|
|
79
78
|
|
|
@@ -28,7 +28,10 @@ export class DefaultClientArtifactsProvider implements IClientArtifactsProvider
|
|
|
28
28
|
|
|
29
29
|
/** @inheritdoc */
|
|
30
30
|
get readModels(): Constructor[] {
|
|
31
|
-
const types = new Set(
|
|
31
|
+
const types = new Set([
|
|
32
|
+
...this.discoverer.getTypesByDecoratorType(DecoratorType.ReadModel),
|
|
33
|
+
...this.discoverer.getPropertyOnlyModels()
|
|
34
|
+
]);
|
|
32
35
|
for (const projection of this.projections) {
|
|
33
36
|
const type = getProjectionMetadata(projection)?.readModelType;
|
|
34
37
|
if (type) types.add(type);
|
|
@@ -0,0 +1,219 @@
|
|
|
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 { createServer, ServerError, type ServiceImplementation } from 'nice-grpc';
|
|
6
|
+
import { ConnectionServiceDefinition, EventStoresDefinition, ServerDefinition } from '@cratis/chronicle.contracts';
|
|
7
|
+
import { status } from '@grpc/grpc-js';
|
|
8
|
+
import { ChronicleConnection } from './ChronicleConnection.js';
|
|
9
|
+
import { ChronicleClient } from '../ChronicleClient.js';
|
|
10
|
+
import { ChronicleOptions } from '../ChronicleOptions.js';
|
|
11
|
+
import { OAuthTokenHttpError } from './fetchOAuthAccessToken.js';
|
|
12
|
+
import { RejectedChronicleCredentials } from './RejectedChronicleCredentials.js';
|
|
13
|
+
import { ClientError } from 'nice-grpc-common';
|
|
14
|
+
|
|
15
|
+
const fetchToken = vi.hoisted(() => vi.fn());
|
|
16
|
+
vi.mock('./fetchOAuthAccessToken.js', async importOriginal => ({ ...await importOriginal<typeof import('./fetchOAuthAccessToken.js')>(), fetchOAuthAccessToken: fetchToken }));
|
|
17
|
+
vi.mock('./KernelKeepAlive.js', () => ({ KernelKeepAlive: class { async start() {} } }));
|
|
18
|
+
|
|
19
|
+
const servers: Array<ReturnType<typeof createServer>> = [];
|
|
20
|
+
const connections: ChronicleConnection[] = [];
|
|
21
|
+
const clients: ChronicleClient[] = [];
|
|
22
|
+
|
|
23
|
+
async function listen(rejectAnonymous = false): Promise<number> {
|
|
24
|
+
const server = createServer();
|
|
25
|
+
const unsupported = async () => { throw new Error('Unexpected RPC'); };
|
|
26
|
+
const methods = Object.fromEntries(Object.keys(ConnectionServiceDefinition.methods).map(name => [name, unsupported]));
|
|
27
|
+
server.add(ConnectionServiceDefinition, {
|
|
28
|
+
...methods, checkCompatibility: async () => {
|
|
29
|
+
if (rejectAnonymous) throw new ServerError(status.UNAUTHENTICATED, 'Unauthenticated');
|
|
30
|
+
return { IsCompatible: true, Incompatibilities: [], ServerVersion: 'test' };
|
|
31
|
+
}
|
|
32
|
+
} as ServiceImplementation<typeof ConnectionServiceDefinition>);
|
|
33
|
+
servers.push(server);
|
|
34
|
+
return server.listen('127.0.0.1:0');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function listenAuthenticated(onCheck?: () => void): Promise<number> {
|
|
38
|
+
const server = createServer();
|
|
39
|
+
const authenticate = async (_request: unknown, context: { metadata: { get(key: string): string | undefined } }) => {
|
|
40
|
+
if (context.metadata.get('authorization') !== 'Bearer fresh' &&
|
|
41
|
+
context.metadata.get('api-key') !== 'valid') throw new ServerError(status.UNAUTHENTICATED, 'Unauthenticated');
|
|
42
|
+
};
|
|
43
|
+
const unsupported = async () => { throw new Error('Unexpected RPC'); };
|
|
44
|
+
server.add(ConnectionServiceDefinition, {
|
|
45
|
+
...Object.fromEntries(Object.keys(ConnectionServiceDefinition.methods).map(name => [name, unsupported])),
|
|
46
|
+
checkCompatibility: async (request, context) => {
|
|
47
|
+
onCheck?.();
|
|
48
|
+
await authenticate(request, context);
|
|
49
|
+
return { IsCompatible: true, Incompatibilities: [], ServerVersion: 'test' };
|
|
50
|
+
}
|
|
51
|
+
} as ServiceImplementation<typeof ConnectionServiceDefinition>);
|
|
52
|
+
server.add(ServerDefinition, {
|
|
53
|
+
getVersionInfo: async () => ({}), resetKernelState: unsupported
|
|
54
|
+
} as ServiceImplementation<typeof ServerDefinition>);
|
|
55
|
+
server.add(EventStoresDefinition, {
|
|
56
|
+
allEventStores: async () => ({ IsAuthorized: true, Data: [] }),
|
|
57
|
+
ensureEventStore: unsupported, observeEventStores: unsupported
|
|
58
|
+
} as ServiceImplementation<typeof EventStoresDefinition>);
|
|
59
|
+
servers.push(server);
|
|
60
|
+
return server.listen('127.0.0.1:0');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
afterEach(async () => {
|
|
64
|
+
for (const client of clients) client.dispose();
|
|
65
|
+
for (const connection of connections) connection.dispose();
|
|
66
|
+
for (const server of servers) await server.shutdown();
|
|
67
|
+
connections.length = 0;
|
|
68
|
+
clients.length = 0;
|
|
69
|
+
servers.length = 0;
|
|
70
|
+
fetchToken.mockReset();
|
|
71
|
+
vi.useRealTimers();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('ChronicleConnection authentication', () => {
|
|
75
|
+
it('uses the selected server for credentials and reuses a token when returning to that server', async () => {
|
|
76
|
+
const first = await listen();
|
|
77
|
+
const second = await listen();
|
|
78
|
+
fetchToken.mockImplementation(async (endpoint: string) => ({ access_token: endpoint, expires_in: 3600 }));
|
|
79
|
+
const connection = new ChronicleConnection({ connectionString: `chronicle://user:secret@127.0.0.1:${first},127.0.0.1:${second}?disableTls=true&loadBalancer=round-robin` });
|
|
80
|
+
connections.push(connection);
|
|
81
|
+
|
|
82
|
+
await connection.connect();
|
|
83
|
+
await connection.reconnect();
|
|
84
|
+
await connection.reconnect();
|
|
85
|
+
|
|
86
|
+
expect(fetchToken.mock.calls.map(call => call[0]).sort()).toEqual([
|
|
87
|
+
`http://127.0.0.1:${first}/connect/token`,
|
|
88
|
+
`http://127.0.0.1:${second}/connect/token`
|
|
89
|
+
].sort());
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('keeps an explicit authority across server selections', async () => {
|
|
93
|
+
const first = await listen();
|
|
94
|
+
const second = await listen();
|
|
95
|
+
fetchToken.mockResolvedValue({ access_token: 'token', expires_in: 3600 });
|
|
96
|
+
const connection = new ChronicleConnection({
|
|
97
|
+
connectionString: `chronicle://user:secret@127.0.0.1:${first},127.0.0.1:${second}?disableTls=true&loadBalancer=round-robin`,
|
|
98
|
+
authority: 'http://identity.example:1234'
|
|
99
|
+
});
|
|
100
|
+
connections.push(connection);
|
|
101
|
+
|
|
102
|
+
await connection.connect();
|
|
103
|
+
await connection.reconnect();
|
|
104
|
+
expect(fetchToken).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(fetchToken.mock.calls[0][0]).toBe('http://identity.example:1234/connect/token');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('includes the token endpoint failure when the unauthenticated RPC is rejected', async () => {
|
|
109
|
+
const port = await listen(true);
|
|
110
|
+
fetchToken.mockRejectedValue(new Error('invalid_client'));
|
|
111
|
+
const connection = new ChronicleConnection({ connectionString: `chronicle://wrong:wrong@127.0.0.1:${port}?disableTls=true` });
|
|
112
|
+
connections.push(connection);
|
|
113
|
+
await expect(connection.connect()).rejects.toThrow(`http://127.0.0.1:${port}/connect/token: invalid_client`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('preserves the gRPC code, path and original error on a token failure', async () => {
|
|
117
|
+
const port = await listen(true);
|
|
118
|
+
fetchToken.mockRejectedValue(new OAuthTokenHttpError(401, '{"error":"invalid_client"}'));
|
|
119
|
+
const connection = new ChronicleConnection({ connectionString: `chronicle://wrong:wrong@127.0.0.1:${port}?disableTls=true` });
|
|
120
|
+
connections.push(connection);
|
|
121
|
+
const error = await connection.connect().catch(error => error as ClientError);
|
|
122
|
+
expect(error).toBeInstanceOf(ClientError);
|
|
123
|
+
expect(error.code).toBe(status.UNAUTHENTICATED);
|
|
124
|
+
expect(error.path).toContain('CheckCompatibility');
|
|
125
|
+
expect(error.details).toContain('invalid_client');
|
|
126
|
+
expect(error.cause).toBeInstanceOf(ClientError);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('refreshes a rejected cached token and retries one unary call', async () => {
|
|
130
|
+
const port = await listenAuthenticated();
|
|
131
|
+
fetchToken.mockResolvedValueOnce({ access_token: 'stale', expires_in: 3600 })
|
|
132
|
+
.mockResolvedValueOnce({ access_token: 'fresh', expires_in: 3600 });
|
|
133
|
+
const connection = new ChronicleConnection({ connectionString: `chronicle://user:secret@127.0.0.1:${port}?disableTls=true` });
|
|
134
|
+
connections.push(connection);
|
|
135
|
+
await expect(connection.connect()).resolves.toBeUndefined();
|
|
136
|
+
expect(fetchToken).toHaveBeenCalledTimes(2);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('does not retry streaming calls after an unauthenticated response', async () => {
|
|
140
|
+
const server = createServer();
|
|
141
|
+
const attempts = vi.fn();
|
|
142
|
+
const unsupported = async () => { throw new Error('Unexpected RPC'); };
|
|
143
|
+
server.add(ConnectionServiceDefinition, {
|
|
144
|
+
...Object.fromEntries(Object.keys(ConnectionServiceDefinition.methods).map(name => [name, unsupported])),
|
|
145
|
+
checkCompatibility: async () => ({ IsCompatible: true, Incompatibilities: [], ServerVersion: 'test' }),
|
|
146
|
+
observeConnectedClients: async function* () {
|
|
147
|
+
attempts();
|
|
148
|
+
throw new ServerError(status.UNAUTHENTICATED, 'Token expired');
|
|
149
|
+
}
|
|
150
|
+
} as ServiceImplementation<typeof ConnectionServiceDefinition>);
|
|
151
|
+
servers.push(server);
|
|
152
|
+
const port = await server.listen('127.0.0.1:0');
|
|
153
|
+
fetchToken.mockResolvedValue({ access_token: 'stale', expires_in: 3600 });
|
|
154
|
+
const connection = new ChronicleConnection({ connectionString: `chronicle://user:secret@127.0.0.1:${port}?disableTls=true` });
|
|
155
|
+
connections.push(connection);
|
|
156
|
+
await connection.connect();
|
|
157
|
+
const observe = async () => { for await (const _ of connection.connections.observeConnectedClients({})) { /* drain */ } };
|
|
158
|
+
await expect(observe()).rejects.toMatchObject({ code: status.UNAUTHENTICATED });
|
|
159
|
+
expect(attempts).toHaveBeenCalledTimes(1);
|
|
160
|
+
expect(fetchToken).toHaveBeenCalledTimes(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('connects after two startup invalid_client responses', async () => {
|
|
164
|
+
const port = await listenAuthenticated();
|
|
165
|
+
fetchToken.mockRejectedValueOnce(new OAuthTokenHttpError(401, '{"error":"invalid_client"}'))
|
|
166
|
+
.mockRejectedValueOnce(new OAuthTokenHttpError(401, '{"error":"invalid_client"}'))
|
|
167
|
+
.mockResolvedValue({ access_token: 'fresh', expires_in: 3600 });
|
|
168
|
+
const client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://user:secret@127.0.0.1:${port}?disableTls=true`, { discoveryPatterns: [] }));
|
|
169
|
+
clients.push(client);
|
|
170
|
+
await expect(client.getEventStores()).resolves.toEqual([]);
|
|
171
|
+
expect(fetchToken).toHaveBeenCalledTimes(3);
|
|
172
|
+
}, 10000);
|
|
173
|
+
|
|
174
|
+
it('rejects persistent OAuth credential rejections after three connection attempts', async () => {
|
|
175
|
+
const port = await listenAuthenticated();
|
|
176
|
+
fetchToken.mockRejectedValue(new OAuthTokenHttpError(400, '{"error":"invalid_client"}'));
|
|
177
|
+
const client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://user:secret@127.0.0.1:${port}?disableTls=true`, { discoveryPatterns: [] }));
|
|
178
|
+
clients.push(client);
|
|
179
|
+
await expect(client.getEventStores()).rejects.toBeInstanceOf(RejectedChronicleCredentials);
|
|
180
|
+
expect(fetchToken).toHaveBeenCalledTimes(3);
|
|
181
|
+
}, 10000);
|
|
182
|
+
|
|
183
|
+
it('keeps retrying proxy 403 responses without an OAuth error code', async () => {
|
|
184
|
+
const random = vi.spyOn(Math, 'random').mockReturnValue(0);
|
|
185
|
+
const checks = vi.fn();
|
|
186
|
+
const port = await listenAuthenticated(checks);
|
|
187
|
+
fetchToken.mockRejectedValue(new OAuthTokenHttpError(403, '<html>blocked</html>'));
|
|
188
|
+
const client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://user:secret@127.0.0.1:${port}?disableTls=true`, { discoveryPatterns: [] }));
|
|
189
|
+
clients.push(client);
|
|
190
|
+
const pending = client.getEventStores().then(() => undefined, error => error as Error);
|
|
191
|
+
try {
|
|
192
|
+
await new Promise(resolve => setTimeout(resolve, 3900));
|
|
193
|
+
expect(checks.mock.calls.length).toBeGreaterThanOrEqual(4);
|
|
194
|
+
client.dispose();
|
|
195
|
+
expect((await pending).message).toMatch(/disposed/);
|
|
196
|
+
} finally {
|
|
197
|
+
random.mockRestore();
|
|
198
|
+
}
|
|
199
|
+
}, 15000);
|
|
200
|
+
|
|
201
|
+
it('rejects an API key after three unauthenticated kernel responses', async () => {
|
|
202
|
+
const checks = vi.fn();
|
|
203
|
+
const port = await listenAuthenticated(checks);
|
|
204
|
+
const client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://127.0.0.1:${port}?disableTls=true&apiKey=wrong`, { discoveryPatterns: [] }));
|
|
205
|
+
clients.push(client);
|
|
206
|
+
await expect(client.getEventStores()).rejects.toBeInstanceOf(RejectedChronicleCredentials);
|
|
207
|
+
expect(checks).toHaveBeenCalledTimes(3);
|
|
208
|
+
}, 10000);
|
|
209
|
+
|
|
210
|
+
it('reports the selected endpoint and underlying failure for development credentials', async () => {
|
|
211
|
+
const port = await listen();
|
|
212
|
+
fetchToken.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
213
|
+
const connection = new ChronicleConnection({ connectionString: `chronicle://127.0.0.1:${port}?disableTls=true` });
|
|
214
|
+
connections.push(connection);
|
|
215
|
+
|
|
216
|
+
await expect(connection.connect()).resolves.toBeUndefined();
|
|
217
|
+
expect(fetchToken.mock.calls[0][1]).toBe('chronicle-dev-client');
|
|
218
|
+
});
|
|
219
|
+
});
|