@solidxai/core 0.1.15 → 0.1.16-beta.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/CHANGELOG.md +642 -0
- package/dist/commands/run-tests.command.d.ts +3 -1
- package/dist/commands/run-tests.command.d.ts.map +1 -1
- package/dist/commands/run-tests.command.js +6 -2
- package/dist/commands/run-tests.command.js.map +1 -1
- package/dist/controllers/test-run.controller.d.ts +12 -0
- package/dist/controllers/test-run.controller.d.ts.map +1 -0
- package/dist/controllers/test-run.controller.js +54 -0
- package/dist/controllers/test-run.controller.js.map +1 -0
- package/dist/dtos/test-run-request.dto.d.ts +35 -0
- package/dist/dtos/test-run-request.dto.d.ts.map +1 -0
- package/dist/dtos/test-run-request.dto.js +120 -0
- package/dist/dtos/test-run-request.dto.js.map +1 -0
- package/dist/guards/access-token.guard.d.ts +5 -1
- package/dist/guards/access-token.guard.d.ts.map +1 -1
- package/dist/guards/access-token.guard.js +20 -1
- package/dist/guards/access-token.guard.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/active-user-data.interface.d.ts +1 -0
- package/dist/interfaces/active-user-data.interface.d.ts.map +1 -1
- package/dist/interfaces/active-user-data.interface.js.map +1 -1
- package/dist/jobs/database/test-run-queue-options-database.d.ts +8 -0
- package/dist/jobs/database/test-run-queue-options-database.d.ts.map +1 -0
- package/dist/jobs/database/test-run-queue-options-database.js +10 -0
- package/dist/jobs/database/test-run-queue-options-database.js.map +1 -0
- package/dist/jobs/database/test-run-queue-publisher-database.service.d.ts +12 -0
- package/dist/jobs/database/test-run-queue-publisher-database.service.d.ts.map +1 -0
- package/dist/jobs/database/test-run-queue-publisher-database.service.js +39 -0
- package/dist/jobs/database/test-run-queue-publisher-database.service.js.map +1 -0
- package/dist/jobs/database/test-run-queue-subscriber-database.service.d.ts +26 -0
- package/dist/jobs/database/test-run-queue-subscriber-database.service.d.ts.map +1 -0
- package/dist/jobs/database/test-run-queue-subscriber-database.service.js +120 -0
- package/dist/jobs/database/test-run-queue-subscriber-database.service.js.map +1 -0
- package/dist/seeders/seed-data/solid-core-metadata.json +3 -1
- package/dist/services/access-token-denylist.service.d.ts +16 -0
- package/dist/services/access-token-denylist.service.d.ts.map +1 -0
- package/dist/services/access-token-denylist.service.js +74 -0
- package/dist/services/access-token-denylist.service.js.map +1 -0
- package/dist/services/authentication.service.d.ts +9 -1
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +39 -4
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/import-transaction.service.d.ts +1 -1
- package/dist/services/import-transaction.service.d.ts.map +1 -1
- package/dist/services/import-transaction.service.js +13 -23
- package/dist/services/import-transaction.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +20 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +11 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/services/workflow-secret.service.d.ts +3 -0
- package/dist/services/workflow-secret.service.d.ts.map +1 -1
- package/dist/services/workflow-secret.service.js +25 -0
- package/dist/services/workflow-secret.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +9 -0
- package/dist/solid-core.module.js.map +1 -1
- package/dist/testing/adapters/ui/browser-provisioner.d.ts +5 -0
- package/dist/testing/adapters/ui/browser-provisioner.d.ts.map +1 -0
- package/dist/testing/adapters/ui/browser-provisioner.js +102 -0
- package/dist/testing/adapters/ui/browser-provisioner.js.map +1 -0
- package/dist/testing/adapters/ui/playwright-adapter.d.ts +40 -1
- package/dist/testing/adapters/ui/playwright-adapter.d.ts.map +1 -1
- package/dist/testing/adapters/ui/playwright-adapter.js +185 -3
- package/dist/testing/adapters/ui/playwright-adapter.js.map +1 -1
- package/dist/testing/adapters/ui/ui.types.d.ts +6 -0
- package/dist/testing/adapters/ui/ui.types.d.ts.map +1 -1
- package/dist/testing/adapters/ui/ui.types.js.map +1 -1
- package/dist/testing/contracts/runtime-context.types.d.ts +2 -0
- package/dist/testing/contracts/runtime-context.types.d.ts.map +1 -1
- package/dist/testing/contracts/runtime-context.types.js.map +1 -1
- package/dist/testing/core/interpolation.d.ts +1 -0
- package/dist/testing/core/interpolation.d.ts.map +1 -1
- package/dist/testing/core/interpolation.js +28 -68
- package/dist/testing/core/interpolation.js.map +1 -1
- package/dist/testing/core/path-resolution.spec.d.ts +2 -0
- package/dist/testing/core/path-resolution.spec.d.ts.map +1 -0
- package/dist/testing/core/path-resolution.spec.js +106 -0
- package/dist/testing/core/path-resolution.spec.js.map +1 -0
- package/dist/testing/core/path-segments.d.ts +2 -0
- package/dist/testing/core/path-segments.d.ts.map +1 -0
- package/dist/testing/core/path-segments.js +70 -0
- package/dist/testing/core/path-segments.js.map +1 -0
- package/dist/testing/core/resource-store.d.ts.map +1 -1
- package/dist/testing/core/resource-store.js +5 -2
- package/dist/testing/core/resource-store.js.map +1 -1
- package/dist/testing/core/testing-engine.d.ts.map +1 -1
- package/dist/testing/core/testing-engine.js +16 -0
- package/dist/testing/core/testing-engine.js.map +1 -1
- package/dist/testing/reporter/artifact-sink.d.ts +14 -0
- package/dist/testing/reporter/artifact-sink.d.ts.map +1 -0
- package/dist/testing/reporter/artifact-sink.js +3 -0
- package/dist/testing/reporter/artifact-sink.js.map +1 -0
- package/dist/testing/reporter/console-reporter.d.ts.map +1 -1
- package/dist/testing/reporter/console-reporter.js +7 -0
- package/dist/testing/reporter/console-reporter.js.map +1 -1
- package/dist/testing/reporter/file-service-artifact-sink.d.ts +22 -0
- package/dist/testing/reporter/file-service-artifact-sink.d.ts.map +1 -0
- package/dist/testing/reporter/file-service-artifact-sink.js +31 -0
- package/dist/testing/reporter/file-service-artifact-sink.js.map +1 -0
- package/dist/testing/reporter/lifecycle-events.types.d.ts +67 -0
- package/dist/testing/reporter/lifecycle-events.types.d.ts.map +1 -0
- package/dist/testing/reporter/lifecycle-events.types.js +3 -0
- package/dist/testing/reporter/lifecycle-events.types.js.map +1 -0
- package/dist/testing/reporter/lifecycle-webhook-reporter.d.ts +77 -0
- package/dist/testing/reporter/lifecycle-webhook-reporter.d.ts.map +1 -0
- package/dist/testing/reporter/lifecycle-webhook-reporter.js +275 -0
- package/dist/testing/reporter/lifecycle-webhook-reporter.js.map +1 -0
- package/dist/testing/reporter/progress-reporter.d.ts +3 -3
- package/dist/testing/reporter/progress-reporter.d.ts.map +1 -1
- package/dist/testing/reporter/progress-reporter.js.map +1 -1
- package/dist/testing/reporter/reporter.types.d.ts +13 -1
- package/dist/testing/reporter/reporter.types.d.ts.map +1 -1
- package/dist/testing/reporter/reporter.types.js.map +1 -1
- package/dist/testing/runner/read-scenarios-file.d.ts +9 -0
- package/dist/testing/runner/read-scenarios-file.d.ts.map +1 -0
- package/dist/testing/runner/read-scenarios-file.js +56 -0
- package/dist/testing/runner/read-scenarios-file.js.map +1 -0
- package/dist/testing/runner/run-from-metadata.d.ts +7 -2
- package/dist/testing/runner/run-from-metadata.d.ts.map +1 -1
- package/dist/testing/runner/run-from-metadata.js +57 -6
- package/dist/testing/runner/run-from-metadata.js.map +1 -1
- package/dist/testing/steps/ui/navigation.step.d.ts.map +1 -1
- package/dist/testing/steps/ui/navigation.step.js +12 -12
- package/dist/testing/steps/ui/navigation.step.js.map +1 -1
- package/package.json +1 -1
- package/postman/instant-logout.postman_collection.json +1493 -0
- package/src/commands/run-tests.command.ts +3 -0
- package/src/controllers/test-run.controller.ts +44 -0
- package/src/dtos/test-run-request.dto.ts +130 -0
- package/src/guards/access-token.guard.ts +48 -0
- package/src/index.ts +21 -1
- package/src/interfaces/active-user-data.interface.ts +8 -0
- package/src/jobs/database/test-run-queue-options-database.ts +9 -0
- package/src/jobs/database/test-run-queue-publisher-database.service.ts +24 -0
- package/src/jobs/database/test-run-queue-subscriber-database.service.ts +122 -0
- package/src/seeders/seed-data/solid-core-metadata.json +3 -1
- package/src/services/access-token-denylist.service.ts +145 -0
- package/src/services/authentication.service.ts +118 -2
- package/src/services/import-transaction.service.ts +19 -36
- package/src/services/settings/default-settings-provider.service.ts +12 -0
- package/src/services/workflow-secret.service.ts +61 -2
- package/src/solid-core.module.ts +11 -0
- package/src/testing/README.md +44 -1
- package/src/testing/adapters/ui/browser-provisioner.ts +115 -0
- package/src/testing/adapters/ui/playwright-adapter.ts +255 -5
- package/src/testing/adapters/ui/ui.types.ts +14 -0
- package/src/testing/contracts/runtime-context.types.ts +6 -0
- package/src/testing/core/interpolation.ts +39 -69
- package/src/testing/core/path-resolution.spec.ts +133 -0
- package/src/testing/core/path-segments.ts +81 -0
- package/src/testing/core/resource-store.ts +8 -2
- package/src/testing/core/testing-engine.ts +22 -0
- package/src/testing/reporter/artifact-sink.ts +16 -0
- package/src/testing/reporter/console-reporter.ts +9 -0
- package/src/testing/reporter/file-service-artifact-sink.ts +46 -0
- package/src/testing/reporter/lifecycle-events.types.ts +103 -0
- package/src/testing/reporter/lifecycle-webhook-reporter.ts +385 -0
- package/src/testing/reporter/progress-reporter.ts +3 -1
- package/src/testing/reporter/reporter.types.ts +10 -1
- package/src/testing/runner/read-scenarios-file.ts +44 -0
- package/src/testing/runner/run-from-metadata.ts +102 -9
- package/src/testing/steps/ui/navigation.step.ts +24 -20
- package/dist/services/access-token-storage.service.d.ts +0 -1
- package/dist/services/access-token-storage.service.d.ts.map +0 -1
- package/dist/services/access-token-storage.service.js +0 -1
- package/dist/services/access-token-storage.service.js.map +0 -1
- package/src/services/access-token-storage.service.ts +0 -0
|
@@ -9,6 +9,7 @@ import { runFromMetadata } from 'src/testing/runner/run-from-metadata';
|
|
|
9
9
|
import type { TestingMetadata } from 'src/testing/contracts/testing-metadata.types';
|
|
10
10
|
import { SpecRegistry } from 'src/testing/core/spec-registry';
|
|
11
11
|
import { SettingService } from 'src/services/setting.service';
|
|
12
|
+
import { WorkflowSecretService } from 'src/services/workflow-secret.service';
|
|
12
13
|
import type { SolidCoreSetting } from 'src/services/settings/default-settings-provider.service';
|
|
13
14
|
|
|
14
15
|
interface TestRunCommandOptions {
|
|
@@ -41,6 +42,7 @@ export class TestRunCommand extends CommandRunner {
|
|
|
41
42
|
constructor(
|
|
42
43
|
private readonly moduleMetadataHelperService: ModuleMetadataHelperService,
|
|
43
44
|
private readonly settingService: SettingService,
|
|
45
|
+
private readonly workflowSecretService: WorkflowSecretService,
|
|
44
46
|
) {
|
|
45
47
|
super();
|
|
46
48
|
}
|
|
@@ -134,6 +136,7 @@ export class TestRunCommand extends CommandRunner {
|
|
|
134
136
|
retries: options?.retries,
|
|
135
137
|
},
|
|
136
138
|
options: { printApiLogs },
|
|
139
|
+
resolveSecrets: (keys) => this.workflowSecretService.resolveAvailable(keys),
|
|
137
140
|
specs: specEntries.length
|
|
138
141
|
? (registry) => loadSpecRegistrations(specEntries, metadataPath, registry)
|
|
139
142
|
: undefined,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
|
3
|
+
import { ApiTags } from '@nestjs/swagger';
|
|
4
|
+
|
|
5
|
+
import { Auth } from 'src/decorators/auth.decorator';
|
|
6
|
+
import { Public } from 'src/decorators/public.decorator';
|
|
7
|
+
import { AuthType } from 'src/enums/auth-type.enum';
|
|
8
|
+
import { PublisherFactory } from 'src/services/queues/publisher-factory.service';
|
|
9
|
+
import {
|
|
10
|
+
TestRunJobPayload,
|
|
11
|
+
TestRunRequestDto,
|
|
12
|
+
} from 'src/dtos/test-run-request.dto';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Triggers a BDD test run from CUSTOM INPUT (inline scenarios). Enqueues the run and
|
|
16
|
+
* returns immediately; a worker (QUEUES_SERVICE_ROLE=subscriber) executes it and
|
|
17
|
+
* streams lifecycle webhooks to `dto.webhookUrl`. Reusable by any Solid app
|
|
18
|
+
* (testingHub calls this from its StartRunController).
|
|
19
|
+
*/
|
|
20
|
+
@Auth(AuthType.None)
|
|
21
|
+
@Controller('test-runs')
|
|
22
|
+
@ApiTags('Solid Core')
|
|
23
|
+
export class TestRunController {
|
|
24
|
+
constructor(private readonly publisherFactory: PublisherFactory<TestRunJobPayload>) {}
|
|
25
|
+
|
|
26
|
+
@Public()
|
|
27
|
+
@Post()
|
|
28
|
+
@HttpCode(HttpStatus.ACCEPTED)
|
|
29
|
+
// Accept the raw body (not the typed DTO) so the global ValidationPipe's
|
|
30
|
+
// implicit-conversion does not coerce inline scenario objects into empty arrays.
|
|
31
|
+
async trigger(@Body() dto: TestRunRequestDto & Record<string, any>) {
|
|
32
|
+
const runId = randomUUID();
|
|
33
|
+
const jobId = await this.publisherFactory.publish(
|
|
34
|
+
{
|
|
35
|
+
payload: { ...dto, runId },
|
|
36
|
+
parentEntity: 'testRun',
|
|
37
|
+
},
|
|
38
|
+
'TestRunQueuePublisher',
|
|
39
|
+
dto.broker,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return { runId, jobId, status: 'queued' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IsArray,
|
|
3
|
+
IsBoolean,
|
|
4
|
+
IsObject,
|
|
5
|
+
IsOptional,
|
|
6
|
+
IsString,
|
|
7
|
+
} from 'class-validator';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ScenarioSpec,
|
|
11
|
+
ScenarioType,
|
|
12
|
+
TestingDataRecord,
|
|
13
|
+
TestingRoleSpec,
|
|
14
|
+
TestingUserSpec,
|
|
15
|
+
} from '../testing/contracts/testing-metadata.types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Per-scenario capture toggles. Structurally compatible with the typed
|
|
19
|
+
* `CaptureOptions` introduced in Phase 2 (A3, in adapters/ui/ui.types.ts).
|
|
20
|
+
*/
|
|
21
|
+
export interface CaptureOptionsInput {
|
|
22
|
+
console?: boolean;
|
|
23
|
+
network?: boolean;
|
|
24
|
+
screenshotOnFailure?: boolean;
|
|
25
|
+
screenshotAtEnd?: boolean;
|
|
26
|
+
video?: boolean;
|
|
27
|
+
har?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Body of `POST /test-runs`. Carries the FULL test definition inline (scenarios +
|
|
32
|
+
* optional data/roles/users — the `testing` block minus `specs`), so the runner is
|
|
33
|
+
* location-agnostic and never reads a fixed module/path. `specs` (custom step code)
|
|
34
|
+
* cannot travel inline and must be deployed on the worker.
|
|
35
|
+
*/
|
|
36
|
+
export class TestRunRequestDto {
|
|
37
|
+
@IsOptional()
|
|
38
|
+
@IsArray()
|
|
39
|
+
scenarios?: ScenarioSpec[];
|
|
40
|
+
|
|
41
|
+
@IsOptional()
|
|
42
|
+
@IsString()
|
|
43
|
+
scenariosPath?: string;
|
|
44
|
+
|
|
45
|
+
@IsOptional()
|
|
46
|
+
@IsArray()
|
|
47
|
+
data?: TestingDataRecord[];
|
|
48
|
+
|
|
49
|
+
@IsOptional()
|
|
50
|
+
@IsArray()
|
|
51
|
+
roles?: TestingRoleSpec[];
|
|
52
|
+
|
|
53
|
+
@IsOptional()
|
|
54
|
+
@IsArray()
|
|
55
|
+
users?: TestingUserSpec[];
|
|
56
|
+
|
|
57
|
+
/** API base URL for relative request URLs. */
|
|
58
|
+
@IsOptional()
|
|
59
|
+
@IsString()
|
|
60
|
+
baseUrl?: string;
|
|
61
|
+
|
|
62
|
+
/** UI base URL for relative navigation (Playwright). */
|
|
63
|
+
@IsOptional()
|
|
64
|
+
@IsString()
|
|
65
|
+
uiBaseUrl?: string;
|
|
66
|
+
|
|
67
|
+
@IsOptional()
|
|
68
|
+
@IsString()
|
|
69
|
+
type?: ScenarioType;
|
|
70
|
+
|
|
71
|
+
@IsOptional()
|
|
72
|
+
@IsString()
|
|
73
|
+
env?: string;
|
|
74
|
+
|
|
75
|
+
@IsOptional()
|
|
76
|
+
@IsBoolean()
|
|
77
|
+
headless?: boolean;
|
|
78
|
+
|
|
79
|
+
@IsOptional()
|
|
80
|
+
@IsBoolean()
|
|
81
|
+
recordVideo?: boolean;
|
|
82
|
+
|
|
83
|
+
@IsOptional()
|
|
84
|
+
@IsArray()
|
|
85
|
+
includeTags?: string[];
|
|
86
|
+
|
|
87
|
+
@IsOptional()
|
|
88
|
+
@IsArray()
|
|
89
|
+
scenarioIds?: string[];
|
|
90
|
+
|
|
91
|
+
@IsOptional()
|
|
92
|
+
@IsArray()
|
|
93
|
+
skipScenarioIds?: string[];
|
|
94
|
+
|
|
95
|
+
/** Where lifecycle events are POSTed (e.g. testingHub ingest endpoint). */
|
|
96
|
+
@IsString()
|
|
97
|
+
webhookUrl!: string;
|
|
98
|
+
|
|
99
|
+
/** Correlation id supplied by the caller (e.g. testingHub TestRun id). */
|
|
100
|
+
@IsOptional()
|
|
101
|
+
@IsString()
|
|
102
|
+
externalRunId?: string;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Per-run env variables for `${env:X}` interpolation — e.g. { BASE_URL: app.url }
|
|
106
|
+
* so scenarios target the application under test rather than the runner itself.
|
|
107
|
+
* (Distinct from `env` above, which is the dev/uat/prod environment label.)
|
|
108
|
+
*/
|
|
109
|
+
@IsOptional()
|
|
110
|
+
@IsObject()
|
|
111
|
+
variables?: Record<string, string>;
|
|
112
|
+
|
|
113
|
+
@IsOptional()
|
|
114
|
+
@IsObject()
|
|
115
|
+
capture?: CaptureOptionsInput;
|
|
116
|
+
|
|
117
|
+
@IsOptional()
|
|
118
|
+
@IsBoolean()
|
|
119
|
+
printApiLogs?: boolean;
|
|
120
|
+
|
|
121
|
+
/** Queue broker override: database | redis | rabbitmq (default: database). */
|
|
122
|
+
@IsOptional()
|
|
123
|
+
@IsString()
|
|
124
|
+
broker?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Queue payload = the request plus the runner-generated run id. */
|
|
128
|
+
export interface TestRunJobPayload extends TestRunRequestDto {
|
|
129
|
+
runId: string;
|
|
130
|
+
}
|
|
@@ -13,6 +13,7 @@ import { REQUEST_USER_KEY } from "../constants";
|
|
|
13
13
|
import { PermissionMetadataService } from '../services/permission-metadata.service';
|
|
14
14
|
import { ClsService } from 'nestjs-cls';
|
|
15
15
|
import { ActiveSessionStorageService } from "../services/active-session-storage.service";
|
|
16
|
+
import { AccessTokenDenylistService } from "../services/access-token-denylist.service";
|
|
16
17
|
import { SettingService } from '../services/setting.service';
|
|
17
18
|
import { createHash } from "crypto";
|
|
18
19
|
|
|
@@ -22,6 +23,7 @@ export class AccessTokenGuard implements CanActivate {
|
|
|
22
23
|
private readonly jwtService: JwtService,
|
|
23
24
|
private readonly permissionsService: PermissionMetadataService,
|
|
24
25
|
private readonly activeSessionStorage: ActiveSessionStorageService,
|
|
26
|
+
private readonly accessTokenDenylist: AccessTokenDenylistService,
|
|
25
27
|
private readonly settingService: SettingService,
|
|
26
28
|
private readonly cls: ClsService
|
|
27
29
|
) { }
|
|
@@ -47,6 +49,13 @@ export class AccessTokenGuard implements CanActivate {
|
|
|
47
49
|
jwtConfiguration
|
|
48
50
|
);
|
|
49
51
|
|
|
52
|
+
// Immediate access-token revocation. Runs BEFORE the concurrent-login
|
|
53
|
+
// check on purpose: rejecting a revoked token first stops it reaching the
|
|
54
|
+
// adopt-on-empty branch below and reclaiming the active session slot.
|
|
55
|
+
// No setting check here - isRevoked short-circuits when the feature is
|
|
56
|
+
// off, without touching the cache.
|
|
57
|
+
await this.validateNotRevoked(payload, token);
|
|
58
|
+
|
|
50
59
|
// Prevent Concurrent Login Feature
|
|
51
60
|
await this.validateConcurrentLoginSession(payload, token);
|
|
52
61
|
|
|
@@ -83,6 +92,45 @@ export class AccessTokenGuard implements CanActivate {
|
|
|
83
92
|
return createHash('sha256').update(token).digest('hex');
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The key this token's session is denylisted under.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately NOT merged with resolveSessionId above, which differs only by
|
|
99
|
+
* the deviceKey step. Merging them would change validateConcurrentLoginSession
|
|
100
|
+
* for tokens issued while preventConcurrentLogins was off that are still live
|
|
101
|
+
* when it is switched on: those resolve to sha256(token) today and would
|
|
102
|
+
* resolve to deviceKey instead. The divergence is invisible in normal
|
|
103
|
+
* operation - that check only runs while the setting is on, and tokens issued
|
|
104
|
+
* in that state never carry a deviceKey - so the two formulas already agree
|
|
105
|
+
* for every token it sees. Only toggle-crossover tokens tell them apart.
|
|
106
|
+
*/
|
|
107
|
+
private resolveRevocationKey(payload: ActiveUserData, token: string): string {
|
|
108
|
+
return (
|
|
109
|
+
payload.sessionId ??
|
|
110
|
+
payload.deviceKey ??
|
|
111
|
+
createHash('sha256').update(token).digest('hex')
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async validateNotRevoked(payload: ActiveUserData, token: string): Promise<void> {
|
|
116
|
+
// isRevoked would short-circuit on its own, but resolving the key first can
|
|
117
|
+
// cost a sha256 for a token carrying neither claim. Checking here keeps the
|
|
118
|
+
// opted-out path genuinely free.
|
|
119
|
+
if (!this.accessTokenDenylist.isEnabled()) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const revocationKey = this.resolveRevocationKey(payload, token);
|
|
124
|
+
const revoked = await this.accessTokenDenylist.isRevoked(
|
|
125
|
+
payload.sub,
|
|
126
|
+
revocationKey,
|
|
127
|
+
payload.iat,
|
|
128
|
+
);
|
|
129
|
+
if (revoked) {
|
|
130
|
+
throw new UnauthorizedException(ERROR_MESSAGES.SESSION_INVALID);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
86
134
|
private async validateConcurrentLoginSession(payload: ActiveUserData, token: string,): Promise<void> {
|
|
87
135
|
if (!this.settingService.getConfigValue<SolidCoreSetting>("preventConcurrentLogins",)) {
|
|
88
136
|
return;
|
package/src/index.ts
CHANGED
|
@@ -319,6 +319,15 @@ export * from './jobs/redis/smtp-email-subscriber-redis.service'
|
|
|
319
319
|
export * from './jobs/redis/test-queue-publisher-redis.service'
|
|
320
320
|
export * from './jobs/redis/test-queue-options-redis'
|
|
321
321
|
export * from './jobs/redis/test-queue-subscriber-redis.service'
|
|
322
|
+
export * from './jobs/database/test-run-queue-publisher-database.service'
|
|
323
|
+
export * from './jobs/database/test-run-queue-subscriber-database.service'
|
|
324
|
+
export * from './controllers/test-run.controller'
|
|
325
|
+
export * from './dtos/test-run-request.dto'
|
|
326
|
+
export * from './testing/reporter/lifecycle-events.types'
|
|
327
|
+
export * from './testing/reporter/lifecycle-webhook-reporter'
|
|
328
|
+
export * from './testing/reporter/artifact-sink'
|
|
329
|
+
export * from './testing/reporter/file-service-artifact-sink'
|
|
330
|
+
export * from './services/file/file-service.interface'
|
|
322
331
|
export * from './jobs/redis/three60-whatsapp-publisher-redis.service'
|
|
323
332
|
export * from './jobs/redis/three60-whatsapp-queue-options-redis'
|
|
324
333
|
export * from './jobs/redis/three60-whatsapp-subscriber-redis.service'
|
|
@@ -339,7 +348,7 @@ export * from './services/selection-providers/list-of-values-selection-providers
|
|
|
339
348
|
export * from './seeders/permission-metadata-seeder.service'
|
|
340
349
|
export * from './seeders/module-metadata-seeder.service'
|
|
341
350
|
|
|
342
|
-
|
|
351
|
+
export * from './services/access-token-denylist.service'
|
|
343
352
|
export * from './services/action-metadata.service'
|
|
344
353
|
export * from './services/authentication.service'
|
|
345
354
|
export * from './services/bcrypt.service'
|
|
@@ -471,3 +480,14 @@ export * from './solid-core.module'
|
|
|
471
480
|
export * from './winston.logger'
|
|
472
481
|
|
|
473
482
|
export { ERROR_MESSAGES } from './constants/error-messages'
|
|
483
|
+
// Needed by consuming projects composing their own logout, so they can return
|
|
484
|
+
// the same response body as the built-in one.
|
|
485
|
+
export { SUCCESS_MESSAGES } from './constants/success-messages'
|
|
486
|
+
|
|
487
|
+
// Named rather than `export *` so the testing internals stay private: consumers only need
|
|
488
|
+
// to provision the browser, which Test Hub does on module init.
|
|
489
|
+
export {
|
|
490
|
+
ensureChromiumInstalled,
|
|
491
|
+
isChromiumInstalled,
|
|
492
|
+
cancelChromiumInstall,
|
|
493
|
+
} from './testing/adapters/ui/browser-provisioner'
|
|
@@ -37,6 +37,14 @@ export interface ActiveUserData {
|
|
|
37
37
|
*/
|
|
38
38
|
roles: string[];
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Standard JWT "issued at", in whole seconds. Stamped automatically by
|
|
42
|
+
* signAsync on every token - declared here only because it was never typed.
|
|
43
|
+
* Compared against a revocation instant to decide whether this token predates
|
|
44
|
+
* a logout.
|
|
45
|
+
*/
|
|
46
|
+
iat?: number;
|
|
47
|
+
|
|
40
48
|
/**
|
|
41
49
|
* The subject's (user) permissions.
|
|
42
50
|
* These are not part of the JWT token, we query them from the database each time the access-token guard is run.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import testRunQueueConfig from './test-run-queue-options-database';
|
|
4
|
+
import { MqMessageQueueService } from '../../services/mq-message-queue.service';
|
|
5
|
+
import { MqMessageService } from '../../services/mq-message.service';
|
|
6
|
+
import { QueuesModuleOptions } from "../../interfaces";
|
|
7
|
+
import { DatabasePublisher } from 'src/services/queues/database-publisher.service';
|
|
8
|
+
import { TestRunJobPayload } from '../../dtos/test-run-request.dto';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class TestRunQueuePublisherDatabase extends DatabasePublisher<TestRunJobPayload> {
|
|
12
|
+
constructor(
|
|
13
|
+
protected readonly mqMessageService: MqMessageService,
|
|
14
|
+
protected readonly mqMessageQueueService: MqMessageQueueService,
|
|
15
|
+
) {
|
|
16
|
+
super(mqMessageService, mqMessageQueueService);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
options(): QueuesModuleOptions {
|
|
20
|
+
return {
|
|
21
|
+
...testRunQueueConfig
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { HttpService } from '@nestjs/axios';
|
|
3
|
+
|
|
4
|
+
import { QueueMessage } from 'src/interfaces/mq';
|
|
5
|
+
import testRunQueueConfig from './test-run-queue-options-database';
|
|
6
|
+
import { MqMessageService } from '../../services/mq-message.service';
|
|
7
|
+
import { MqMessageQueueService } from '../../services/mq-message-queue.service';
|
|
8
|
+
import { QueuesModuleOptions } from "../../interfaces";
|
|
9
|
+
import { DatabaseSubscriber } from 'src/services/queues/database-subscriber.service';
|
|
10
|
+
import { PollerService } from 'src/services/poller.service';
|
|
11
|
+
import { runFromMetadata } from '../../testing/runner/run-from-metadata';
|
|
12
|
+
import { readScenariosFile } from '../../testing/runner/read-scenarios-file';
|
|
13
|
+
import {
|
|
14
|
+
LifecycleWebhookReporter,
|
|
15
|
+
WebhookPostFn,
|
|
16
|
+
} from '../../testing/reporter/lifecycle-webhook-reporter';
|
|
17
|
+
import { FileServiceArtifactSink } from '../../testing/reporter/file-service-artifact-sink';
|
|
18
|
+
import { FILE_SERVICE, IFileService } from '../../services/file/file-service.interface';
|
|
19
|
+
import { TestRunJobPayload } from '../../dtos/test-run-request.dto';
|
|
20
|
+
import { WorkflowSecretService } from '../../services/workflow-secret.service';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Executes a queued test run on the worker tier (QUEUES_SERVICE_ROLE=subscriber).
|
|
24
|
+
* Resolves scenarios from `scenariosPath` (read from disk here, at execution time —
|
|
25
|
+
* the worker shares the filesystem with the caller) or from inline `scenarios`, then
|
|
26
|
+
* runs them via {@link runFromMetadata} with a {@link LifecycleWebhookReporter} that
|
|
27
|
+
* streams run.start → scenario.* → run.end to the caller's webhookUrl. A failing TEST
|
|
28
|
+
* is NOT a failing JOB: failures (including an unreadable scenario file) are reported
|
|
29
|
+
* over the webhook (run.end ok:false) and the job still completes, so the queue does
|
|
30
|
+
* not retry a deterministically-failing test run.
|
|
31
|
+
*/
|
|
32
|
+
@Injectable()
|
|
33
|
+
export class TestRunQueueSubscriberDatabase extends DatabaseSubscriber<TestRunJobPayload> {
|
|
34
|
+
private readonly testRunLogger = new Logger(TestRunQueueSubscriberDatabase.name);
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
readonly mqMessageService: MqMessageService,
|
|
38
|
+
readonly mqMessageQueueService: MqMessageQueueService,
|
|
39
|
+
readonly poller: PollerService,
|
|
40
|
+
private readonly httpService: HttpService,
|
|
41
|
+
@Inject(FILE_SERVICE) private readonly fileService: IFileService,
|
|
42
|
+
private readonly workflowSecretService: WorkflowSecretService,
|
|
43
|
+
) {
|
|
44
|
+
super(mqMessageService, mqMessageQueueService, poller);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
options(): QueuesModuleOptions {
|
|
48
|
+
return {
|
|
49
|
+
...testRunQueueConfig
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async subscribe(message: QueueMessage<TestRunJobPayload>) {
|
|
54
|
+
const p = message.payload;
|
|
55
|
+
this.testRunLogger.debug(`Starting test run ${p.runId} (externalRunId=${p.externalRunId})`);
|
|
56
|
+
|
|
57
|
+
const post: WebhookPostFn = async (url, body, opts) => {
|
|
58
|
+
const res = await this.httpService.axiosRef.post(url, body, {
|
|
59
|
+
headers: { 'Content-Type': 'application/json', ...(opts.headers ?? {}) },
|
|
60
|
+
timeout: opts.timeoutMs ?? 10_000,
|
|
61
|
+
validateStatus: () => true,
|
|
62
|
+
});
|
|
63
|
+
return { ok: res.status >= 200 && res.status < 300, status: res.status };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Persist binary artifacts (screenshots, whole-run video) to shared storage via the
|
|
67
|
+
// core FILE_SERVICE (disk by default, S3 when DEFAULT_FILE_SERVICE=s3) instead of
|
|
68
|
+
// shipping them inline / storing blobs in the DB.
|
|
69
|
+
const artifactSink = new FileServiceArtifactSink(this.fileService, { prefix: 'testing-hub' });
|
|
70
|
+
|
|
71
|
+
const reporter = new LifecycleWebhookReporter({
|
|
72
|
+
webhookUrl: p.webhookUrl,
|
|
73
|
+
runName: p.externalRunId ?? p.runId,
|
|
74
|
+
externalRunId: p.externalRunId,
|
|
75
|
+
runId: p.runId,
|
|
76
|
+
post,
|
|
77
|
+
artifactSink,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let scenarios = p.scenarios;
|
|
81
|
+
let data = p.data;
|
|
82
|
+
if (p.scenariosPath) {
|
|
83
|
+
const read = readScenariosFile(p.scenariosPath);
|
|
84
|
+
if (!read.ok) {
|
|
85
|
+
const reason = `Scenario file unreadable: ${p.scenariosPath} (${read.error})`;
|
|
86
|
+
this.testRunLogger.warn(`Test run ${p.runId} — ${reason}`);
|
|
87
|
+
reporter.onRunStart({ total: 0, startedAt: new Date().toISOString(), scenarioIds: [] });
|
|
88
|
+
await reporter.flushPending(1, reason);
|
|
89
|
+
return { runId: p.runId, exitCode: 1 };
|
|
90
|
+
}
|
|
91
|
+
scenarios = read.scenarios;
|
|
92
|
+
data = read.data ?? data;
|
|
93
|
+
this.testRunLogger.debug(`Test run ${p.runId} loaded ${scenarios.length} scenario(s) from ${p.scenariosPath}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let exitCode = 0;
|
|
97
|
+
try {
|
|
98
|
+
await runFromMetadata({
|
|
99
|
+
scenarios,
|
|
100
|
+
data,
|
|
101
|
+
includeTags: p.includeTags,
|
|
102
|
+
scenarioIds: p.scenarioIds,
|
|
103
|
+
skipScenarioIds: p.skipScenarioIds,
|
|
104
|
+
reporter,
|
|
105
|
+
env: p.variables,
|
|
106
|
+
api: { baseUrl: p.baseUrl },
|
|
107
|
+
ui: { baseUrl: p.uiBaseUrl, headless: p.headless ?? true, capture: p.capture, recordVideo: p.recordVideo ?? true },
|
|
108
|
+
options: { printApiLogs: p.printApiLogs ?? true },
|
|
109
|
+
externalRunId: p.externalRunId,
|
|
110
|
+
resolveSecrets: (keys) => this.workflowSecretService.resolveAvailable(keys),
|
|
111
|
+
});
|
|
112
|
+
} catch (err: any) {
|
|
113
|
+
// A failed scenario is surfaced via the run.end webhook, not as a job failure.
|
|
114
|
+
exitCode = 1;
|
|
115
|
+
this.testRunLogger.warn(`Test run ${p.runId} completed with failures: ${err?.message ?? err}`);
|
|
116
|
+
} finally {
|
|
117
|
+
await reporter.flushPending(exitCode);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { runId: p.runId, exitCode };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -14958,6 +14958,7 @@
|
|
|
14958
14958
|
"enableGlobalSearch": true,
|
|
14959
14959
|
"create": false,
|
|
14960
14960
|
"edit": false,
|
|
14961
|
+
"delete": false,
|
|
14961
14962
|
"import": false,
|
|
14962
14963
|
"export": false,
|
|
14963
14964
|
"allowedViews": [
|
|
@@ -15089,7 +15090,8 @@
|
|
|
15089
15090
|
"label": "User Activity History",
|
|
15090
15091
|
"className": "grid",
|
|
15091
15092
|
"showAddFormButton": false,
|
|
15092
|
-
"showEditFormButton": false
|
|
15093
|
+
"showEditFormButton": false,
|
|
15094
|
+
"showDeleteFormButton": false
|
|
15093
15095
|
},
|
|
15094
15096
|
"children": [
|
|
15095
15097
|
{
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { CACHE_MANAGER } from "@nestjs/cache-manager";
|
|
2
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
3
|
+
import { Cache } from "cache-manager";
|
|
4
|
+
import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service";
|
|
5
|
+
import { SettingService } from "./setting.service";
|
|
6
|
+
|
|
7
|
+
// The denylist entry must outlive the longest-lived access token it could
|
|
8
|
+
// match. Any token an entry rejects was issued before the revocation, so it
|
|
9
|
+
// dies by revokedAt + accessTokenTtl at the latest; after that `exp` rejects it
|
|
10
|
+
// and the entry is dead weight. Mirrors STATE_TTL_BUFFER_SECONDS in
|
|
11
|
+
// refresh-token-ids-storage.service.ts.
|
|
12
|
+
const ENTRY_TTL_BUFFER_SECONDS = 60;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Immediate access-token revocation, opt-in via `invalidateAccessTokenOnLogout`.
|
|
16
|
+
*
|
|
17
|
+
* Entries record WHEN a session was revoked rather than merely that it was, and
|
|
18
|
+
* the guard compares that instant against the token's own `iat`. The timestamp
|
|
19
|
+
* is what makes a session-scoped key safe: some keys are reused across logins -
|
|
20
|
+
* API-key auth passes a stable `apikey-<id>` deviceKey, and the per-user key
|
|
21
|
+
* below is shared by every login that user will ever make - so a boolean entry
|
|
22
|
+
* would have to either reject the newly minted token too, or be cleared on
|
|
23
|
+
* re-issue and thereby revive the still-unexpired token the logout killed.
|
|
24
|
+
* Comparing against `iat` needs neither: tokens minted after the revocation
|
|
25
|
+
* pass, tokens minted before it fail, and nothing is ever cleared.
|
|
26
|
+
*
|
|
27
|
+
* This service owns the setting check - callers never branch on it. One source
|
|
28
|
+
* of truth instead of three call sites that can drift, no `if (enabled)` nesting
|
|
29
|
+
* in logout(), and a consuming project using the public seam cannot write
|
|
30
|
+
* entries that nothing will ever read. `getConfigValue` is a synchronous
|
|
31
|
+
* in-memory lookup, so a deployment that has not opted in pays nothing and
|
|
32
|
+
* issues no I/O.
|
|
33
|
+
*/
|
|
34
|
+
@Injectable()
|
|
35
|
+
export class AccessTokenDenylistService {
|
|
36
|
+
constructor(
|
|
37
|
+
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
|
38
|
+
private readonly settingService: SettingService,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
isEnabled(): boolean {
|
|
42
|
+
return (
|
|
43
|
+
this.settingService.getConfigValue<SolidCoreSetting>(
|
|
44
|
+
"invalidateAccessTokenOnLogout",
|
|
45
|
+
) === true
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Revoke every access token belonging to one session. A no-op when the
|
|
51
|
+
* feature is off, and when no session key could be resolved - which spares
|
|
52
|
+
* the caller a null check.
|
|
53
|
+
*/
|
|
54
|
+
async revokeSession(userId: number, sessionKey?: string): Promise<void> {
|
|
55
|
+
if (!this.isEnabled() || !sessionKey) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await this.cacheManager.set(
|
|
59
|
+
this.getSessionKey(userId, sessionKey),
|
|
60
|
+
this.revokedAt(),
|
|
61
|
+
this.getEntryTtlMs(),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Revoke every access token for a user, across all sessions - `allDevices`.
|
|
67
|
+
*
|
|
68
|
+
* Needs no claim on the token and no enumeration of buckets: every
|
|
69
|
+
* outstanding token was necessarily issued before the logout that writes
|
|
70
|
+
* this, so one entry sweeps them all.
|
|
71
|
+
*/
|
|
72
|
+
async revokeAllForUser(userId: number): Promise<void> {
|
|
73
|
+
if (!this.isEnabled()) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await this.cacheManager.set(
|
|
77
|
+
this.getUserKey(userId),
|
|
78
|
+
this.revokedAt(),
|
|
79
|
+
this.getEntryTtlMs(),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* True when the token was issued before either revocation instant that could
|
|
85
|
+
* apply to it - its own session's, or its user's.
|
|
86
|
+
*
|
|
87
|
+
* Returns false without touching the cache when the feature is off, which is
|
|
88
|
+
* why the guard needs no setting check of its own.
|
|
89
|
+
*/
|
|
90
|
+
async isRevoked(
|
|
91
|
+
userId: number,
|
|
92
|
+
sessionKey: string,
|
|
93
|
+
issuedAtSeconds?: number,
|
|
94
|
+
): Promise<boolean> {
|
|
95
|
+
if (!this.isEnabled()) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A token with no `iat` cannot be placed relative to a revocation. Tokens
|
|
100
|
+
// signed by @nestjs/jwt always carry one, so this is a malformed-payload
|
|
101
|
+
// guard rather than a real case; refuse rather than fail open.
|
|
102
|
+
if (typeof issuedAtSeconds !== "number") {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const [sessionRevokedAt, userRevokedAt] = await Promise.all([
|
|
107
|
+
this.cacheManager.get<number>(this.getSessionKey(userId, sessionKey)),
|
|
108
|
+
this.cacheManager.get<number>(this.getUserKey(userId)),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const revokedAt = Math.max(sessionRevokedAt ?? 0, userRevokedAt ?? 0);
|
|
112
|
+
return issuedAtSeconds * 1000 < revokedAt;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* `iat` has whole-second resolution, so a token minted in the same second as
|
|
117
|
+
* the revocation is indistinguishable from one minted just before it.
|
|
118
|
+
* Rounding up resolves that ambiguity toward revoking: an automated client
|
|
119
|
+
* re-authenticating inside the window gets one 401 and succeeds on retry,
|
|
120
|
+
* whereas rounding down would leave a genuinely revoked token valid for up to
|
|
121
|
+
* a second. No human path can reach the window at all - logout invalidates
|
|
122
|
+
* the refresh token, so the next access token only exists after a fresh login.
|
|
123
|
+
*/
|
|
124
|
+
private revokedAt(): number {
|
|
125
|
+
return (Math.floor(Date.now() / 1000) + 1) * 1000;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private getSessionKey(userId: number, sessionKey: string): string {
|
|
129
|
+
return `revoked-session-${userId}-${sessionKey}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private getUserKey(userId: number): string {
|
|
133
|
+
return `revoked-user-${userId}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// cache-manager v5 expects milliseconds; accessTokenTtl is configured in
|
|
137
|
+
// seconds. Read from the setting rather than hardcoded so that raising
|
|
138
|
+
// accessTokenTtl keeps entries long enough to outlive the tokens they match.
|
|
139
|
+
private getEntryTtlMs(): number {
|
|
140
|
+
const accessTokenTtlSeconds = Number(
|
|
141
|
+
this.settingService.getConfigValue<SolidCoreSetting>("accessTokenTtl"),
|
|
142
|
+
);
|
|
143
|
+
return (accessTokenTtlSeconds + ENTRY_TTL_BUFFER_SECONDS) * 1000;
|
|
144
|
+
}
|
|
145
|
+
}
|