@tstdl/base 0.93.172 → 0.93.174

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.
@@ -6,7 +6,7 @@ import { CircuitBreakerProvider } from '../../../circuit-breaker/provider.js';
6
6
  import { Logger } from '../../../logger/logger.js';
7
7
  import { setupIntegrationTest } from '../../../testing/index.js';
8
8
  import { vertexAiMultiLocation } from '../multi-region.plugin.js';
9
- vi.mock('#/utils/array/index.js', async (importOriginal) => {
9
+ vi.mock('../../../utils/array/index.js', async (importOriginal) => {
10
10
  const actual = await importOriginal();
11
11
  return {
12
12
  ...actual,
@@ -7,7 +7,7 @@ import { Logger } from '../../../logger/logger.js';
7
7
  import { setupIntegrationTest } from '../../../testing/index.js';
8
8
  import { timeout } from '../../../utils/timing.js';
9
9
  import { vertexAiMultiLocation } from '../multi-region.plugin.js';
10
- vi.mock('#/utils/array/index.js', async (importOriginal) => {
10
+ vi.mock('../../../utils/array/index.js', async (importOriginal) => {
11
11
  const actual = await importOriginal();
12
12
  return {
13
13
  ...actual,
@@ -1,53 +1,91 @@
1
1
  import { resolveApiEndpointDataProvider } from '../../../api/types.js';
2
- import { toArray } from '../../../utils/array/array.js';
3
- import { isDefined } from '../../../utils/type-guards.js';
2
+ import { distinct, toArray } from '../../../utils/array/array.js';
3
+ import { isDefined, isString } from '../../../utils/type-guards.js';
4
+ const defaultAllowedHeaders = ['Content-Type', 'Authorization', 'Cache-Control', 'If-None-Match', 'Range'];
5
+ const defaultExposedHeaders = ['ETag', 'Content-Disposition', 'Content-Range'];
6
+ const headerSplitPattern = /,\s*/u;
4
7
  export function corsMiddleware(options = {}) {
5
8
  return async function corsMiddleware(context, next) {
6
- try {
7
- await next();
8
- }
9
- finally {
10
- const { request, response } = context;
11
- const requestMethod = request.headers.tryGetSingle('Access-Control-Request-Method') ?? request.method;
12
- const isOptions = (request.method == 'OPTIONS');
13
- const endpointDefinition = context.api.endpoints.get(requestMethod)?.definition;
14
- const cors = { ...options.default, ...endpointDefinition?.cors };
15
- if (isOptions) {
16
- const allowMethods = (await resolveApiEndpointDataProvider(request, context, cors.accessControlAllowMethods)) ?? [...context.api.endpoints.keys()].join(', ');
17
- response.headers.set('Access-Control-Allow-Methods', allowMethods);
18
- if (isDefined(cors.accessControlAllowHeaders) && !request.headers.has('Access-Control-Allow-Headers')) {
19
- const value = await resolveApiEndpointDataProvider(request, context, cors.accessControlAllowHeaders);
20
- response.headers.set('Access-Control-Allow-Headers', value);
21
- }
22
- if (isDefined(cors.accessControlExposeHeaders) && !request.headers.has('Access-Control-Expose-Headers')) {
23
- const value = await resolveApiEndpointDataProvider(request, context, cors.accessControlExposeHeaders);
24
- response.headers.set('Access-Control-Expose-Headers', value);
25
- }
26
- if (isDefined(cors.accessControlMaxAge) && !request.headers.has('Access-Control-Max-Age')) {
27
- const value = await resolveApiEndpointDataProvider(request, context, cors.accessControlMaxAge);
28
- response.headers.set('Access-Control-Max-Age', value);
29
- }
30
- }
31
- if (!request.headers.has('Access-Control-Allow-Credentials')) {
32
- const allowCredentials = isDefined(cors.accessControlAllowCredentials)
33
- ? await resolveApiEndpointDataProvider(request, context, cors.accessControlAllowCredentials)
34
- : endpointDefinition?.credentials;
35
- if (allowCredentials == true) {
36
- response.headers.set('Access-Control-Allow-Credentials', 'true');
37
- }
38
- }
9
+ const { request, response } = context;
10
+ const requestMethod = request.headers.tryGetSingle('Access-Control-Request-Method') ?? request.method;
11
+ const isOptions = (request.method == 'OPTIONS');
12
+ const endpointDefinition = context.api.endpoints.get(requestMethod)?.definition;
13
+ const cors = { ...options.default, ...endpointDefinition?.cors };
14
+ const origin = request.headers.tryGetSingle('Origin');
15
+ const allowCredentials = (await resolveApiEndpointDataProvider(request, context, cors.accessControlAllowCredentials)) ?? endpointDefinition?.credentials;
16
+ if (isDefined(origin)) {
17
+ let allowOriginValidationFailed = false;
39
18
  if (isDefined(cors.accessControlAllowOrigin) && !response.headers.has('Access-Control-Allow-Origin')) {
40
- const value = await resolveApiEndpointDataProvider(request, context, cors.accessControlAllowOrigin);
41
- response.headers.set('Access-Control-Allow-Origin', value);
19
+ const allowedOrigin = await resolveApiEndpointDataProvider(request, context, cors.accessControlAllowOrigin);
20
+ if (isDefined(allowedOrigin)) {
21
+ const isWildcard = (allowedOrigin == '*');
22
+ const isAllowed = isWildcard || (origin == allowedOrigin);
23
+ if (isAllowed) {
24
+ const value = (isWildcard && (allowCredentials == true)) ? origin : allowedOrigin;
25
+ response.headers.set('Access-Control-Allow-Origin', value);
26
+ if (value == origin) {
27
+ response.headers.append('Vary', 'Origin');
28
+ }
29
+ }
30
+ else {
31
+ allowOriginValidationFailed = true;
32
+ }
33
+ }
42
34
  }
43
35
  if (isDefined(cors.autoAccessControlAllowOrigin) && !response.headers.has('Access-Control-Allow-Origin')) {
44
- const value = await resolveApiEndpointDataProvider(request, context, cors.autoAccessControlAllowOrigin);
45
- const origin = request.headers.tryGetSingle('Origin');
46
- const allowed = isDefined(value) && toArray(value).includes(origin);
36
+ const allowedOrigins = await resolveApiEndpointDataProvider(request, context, cors.autoAccessControlAllowOrigin);
37
+ const allowed = allowedOrigins?.includes(origin) ?? false;
47
38
  if (allowed) {
48
39
  response.headers.set('Access-Control-Allow-Origin', origin);
40
+ response.headers.append('Vary', 'Origin');
41
+ }
42
+ else {
43
+ allowOriginValidationFailed = true;
49
44
  }
50
45
  }
46
+ if (isOptions && allowOriginValidationFailed && !response.headers.has('Access-Control-Allow-Origin')) {
47
+ response.statusCode = 403;
48
+ return;
49
+ }
50
+ }
51
+ if (isOptions) {
52
+ const customAllowMethods = await resolveApiEndpointDataProvider(request, context, cors.accessControlAllowMethods);
53
+ const allowMethods = customAllowMethods?.join(', ') ?? [...context.api.endpoints.keys()].join(', ');
54
+ response.headers.set('Access-Control-Allow-Methods', allowMethods);
55
+ const customAllowHeaders = await resolveApiEndpointDataProvider(request, context, cors.accessControlAllowHeaders);
56
+ const allowHeaders = distinct([
57
+ ...defaultAllowedHeaders,
58
+ ...(customAllowHeaders ?? []),
59
+ ...toArray(response.headers.tryGet('Access-Control-Allow-Headers') ?? []).filter(isString).flatMap((h) => h.split(headerSplitPattern)),
60
+ ]).filter((header) => (header.length > 0)).join(', ');
61
+ response.headers.set('Access-Control-Allow-Headers', allowHeaders);
62
+ if (isDefined(cors.accessControlMaxAge) && !response.headers.has('Access-Control-Max-Age')) {
63
+ const value = await resolveApiEndpointDataProvider(request, context, cors.accessControlMaxAge);
64
+ response.headers.set('Access-Control-Max-Age', value);
65
+ }
66
+ else if (!response.headers.has('Access-Control-Max-Age')) {
67
+ response.headers.set('Access-Control-Max-Age', '86400');
68
+ }
69
+ if (allowCredentials == true) {
70
+ response.headers.set('Access-Control-Allow-Credentials', 'true');
71
+ }
72
+ response.statusCode = 204;
73
+ return;
74
+ }
75
+ try {
76
+ await next();
77
+ }
78
+ finally {
79
+ const customExposeHeaders = await resolveApiEndpointDataProvider(request, context, cors.accessControlExposeHeaders);
80
+ const exposeHeaders = [...new Set([
81
+ ...defaultExposedHeaders,
82
+ ...(customExposeHeaders ?? []),
83
+ ...toArray(response.headers.tryGet('Access-Control-Expose-Headers') ?? []).filter(isString).flatMap((h) => h.split(headerSplitPattern)),
84
+ ])].filter((header) => (header.length > 0)).join(', ');
85
+ response.headers.set('Access-Control-Expose-Headers', exposeHeaders);
86
+ if (allowCredentials == true) {
87
+ response.headers.setIfMissing('Access-Control-Allow-Credentials', 'true');
88
+ }
51
89
  }
52
90
  };
53
91
  }
package/api/types.d.ts CHANGED
@@ -33,11 +33,11 @@ export type ApiEndpointDefinitionResult = SchemaTestable | typeof String | typeo
33
33
  export type ApiEndpointDataProvider<T> = T | ((request: HttpServerRequest, context: ApiGatewayMiddlewareContext) => T | Promise<T>);
34
34
  export type ApiEndpointDefinitionCors = {
35
35
  accessControlAllowCredentials?: ApiEndpointDataProvider<boolean | undefined>;
36
- accessControlAllowHeaders?: ApiEndpointDataProvider<OneOrMany<string> | undefined>;
37
- accessControlAllowMethods?: ApiEndpointDataProvider<OneOrMany<HttpMethod> | undefined>;
36
+ accessControlAllowHeaders?: ApiEndpointDataProvider<string[] | undefined>;
37
+ accessControlAllowMethods?: ApiEndpointDataProvider<HttpMethod[] | undefined>;
38
38
  accessControlAllowOrigin?: ApiEndpointDataProvider<string | undefined>;
39
- autoAccessControlAllowOrigin?: ApiEndpointDataProvider<OneOrMany<string> | undefined>;
40
- accessControlExposeHeaders?: ApiEndpointDataProvider<OneOrMany<string> | undefined>;
39
+ autoAccessControlAllowOrigin?: ApiEndpointDataProvider<string[] | undefined>;
40
+ accessControlExposeHeaders?: ApiEndpointDataProvider<string[] | undefined>;
41
41
  accessControlMaxAge?: ApiEndpointDataProvider<number | undefined>;
42
42
  };
43
43
  export type ApiEndpointDefinition = {
@@ -26,7 +26,7 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
26
26
  private readonly logger;
27
27
  private readonly disposeSignal;
28
28
  private readonly forceRefreshRequested;
29
- private readonly forceRefreshRequested$;
29
+ private readonly forceRefreshSubject;
30
30
  private clockOffset;
31
31
  private initialized;
32
32
  private refreshLoopPromise;
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
- import { Subject, filter, firstValueFrom, from, race, skip, takeUntil, timer } from 'rxjs';
10
+ import { Subject, filter, firstValueFrom, from, race, takeUntil, timer } from 'rxjs';
11
11
  import { CancellationSignal } from '../../cancellation/token.js';
12
12
  import { BadRequestError } from '../../errors/bad-request.error.js';
13
13
  import { ForbiddenError } from '../../errors/forbidden.error.js';
@@ -72,7 +72,7 @@ let AuthenticationClientService = class AuthenticationClientService {
72
72
  logger = inject(Logger, 'AuthenticationService');
73
73
  disposeSignal = inject(CancellationSignal).fork();
74
74
  forceRefreshRequested = signal(false);
75
- forceRefreshRequested$ = toObservable(this.forceRefreshRequested);
75
+ forceRefreshSubject = new Subject();
76
76
  clockOffset = 0;
77
77
  initialized = false;
78
78
  refreshLoopPromise;
@@ -267,6 +267,7 @@ let AuthenticationClientService = class AuthenticationClientService {
267
267
  this.setAdditionalData(data);
268
268
  }
269
269
  this.forceRefreshRequested.set(true);
270
+ this.forceRefreshSubject.next();
270
271
  }
271
272
  /**
272
273
  * Refresh the token.
@@ -404,7 +405,7 @@ let AuthenticationClientService = class AuthenticationClientService {
404
405
  timer(Math.max(10, delayMs)),
405
406
  from(this.disposeSignal),
406
407
  this.token$.pipe(filter((t) => t?.exp !== referenceExp)),
407
- this.forceRefreshRequested$.pipe(filter(Boolean), skip(1)),
408
+ this.forceRefreshSubject,
408
409
  ]), { defaultValue: undefined });
409
410
  while (this.disposeSignal.isUnset) {
410
411
  const token = this.token();
@@ -428,9 +429,9 @@ let AuthenticationClientService = class AuthenticationClientService {
428
429
  const currentNow = this.estimatedServerTimestampSeconds();
429
430
  const currentBuffer = calculateRefreshBufferSeconds(currentToken);
430
431
  const stillNeedsRefresh = this.forceRefreshRequested() || (currentNow >= currentToken.exp - currentBuffer);
431
- this.forceRefreshRequested.set(false);
432
432
  if (stillNeedsRefresh) {
433
433
  await this.refresh();
434
+ this.forceRefreshRequested.set(false);
434
435
  }
435
436
  });
436
437
  // If lock is held by another instance/tab, wait briefly for it to finish (passive sync)
@@ -90,10 +90,10 @@ describe('DocumentManagementAiService Integration', () => {
90
90
  await runInInjectionContext(injector, async () => {
91
91
  await aiService.extractData(tenantId, document.id);
92
92
  const lastCall = mockGenerate.mock.calls[0][0];
93
- const systemPrompt = lastCall.system;
94
- const systemPromptText = systemPrompt.map((p) => p.text).join('\n');
95
- expect(systemPromptText).toContain('Format the title of the document exactly as follows: INV-{{Date}}');
96
- expect(systemPromptText).toContain('Focus on items.');
93
+ const userPrompt = lastCall.prompt;
94
+ const userPromptText = userPrompt.map((p) => p.text).join('\n');
95
+ expect(userPromptText).toContain('Format the title of the document exactly as follows: INV-{{Date}}');
96
+ expect(userPromptText).toContain('Focus on items.');
97
97
  });
98
98
  });
99
99
  test('classifyDocumentType should include granular instructions in prompt', async () => {
@@ -6,6 +6,7 @@ import { object, string } from '../../schema/index.js';
6
6
  import { setupIntegrationTest } from '../../testing/index.js';
7
7
  import { isString } from '../../utils/type-guards.js';
8
8
  import { DocumentManagementConfiguration } from '../server/module.js';
9
+ import { DocumentFileService } from '../server/services/document-file.service.js';
9
10
  import { DocumentManagementAiProviderService, DocumentManagementAncillaryService } from '../server/services/index.js';
10
11
  import { AiValidationExecutor } from '../server/validators/ai-validation-executor.js';
11
12
  import { TestDocumentManagementAncillaryService } from './helper.js';
@@ -41,6 +42,7 @@ describe('AiValidationExecutor Overrides', () => {
41
42
  injector.register(DocumentManagementAiProviderService, { useValue: mockAiProvider });
42
43
  injector.register(DocumentManagementAncillaryService, { useToken: TestDocumentManagementAncillaryService });
43
44
  injector.register(DocumentManagementConfiguration, { useValue: { fileObjectStorageModule: 'docs', fileUploadObjectStorageModule: 'uploads', filePreviewObjectStorageModule: 'previews' } });
45
+ injector.register(DocumentFileService, { useValue: { getContent: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])) } });
44
46
  });
45
47
  afterAll(async () => {
46
48
  await injector?.dispose();
@@ -66,6 +68,7 @@ describe('AiValidationExecutor Overrides', () => {
66
68
  const context = {
67
69
  execution: { tenantId },
68
70
  definition: { identifier: 'test-validation' },
71
+ document: { id: 'test-doc', mimeType: 'application/pdf' },
69
72
  };
70
73
  await executor.publicExecute(context);
71
74
  const call = generateSpy.mock.calls[0][0];
@@ -40,7 +40,7 @@ const streamingApiDefinition = defineApi({
40
40
  result: ServerSentEvents,
41
41
  cors: {
42
42
  accessControlAllowOrigin: '*',
43
- accessControlAllowMethods: 'GET',
43
+ accessControlAllowMethods: ['GET'],
44
44
  },
45
45
  },
46
46
  },
@@ -132,8 +132,8 @@ function bootstrap() {
132
132
  prefix: null,
133
133
  cors: {
134
134
  default: {
135
- autoAccessControlAllowOrigin: 'http://localhost:4200',
136
- accessControlAllowHeaders: 'Content-Type, Authorization',
135
+ autoAccessControlAllowOrigin: ['http://localhost:4200'],
136
+ accessControlAllowHeaders: ['Content-Type', 'Authorization'],
137
137
  },
138
138
  },
139
139
  },
@@ -23,9 +23,9 @@ export type HttpServerResponseOptions = {
23
23
  };
24
24
  export declare class HttpServerResponse {
25
25
  #private;
26
+ readonly headers: HttpHeaders;
26
27
  statusCode: number | undefined;
27
28
  statusMessage: string | undefined;
28
- headers: HttpHeaders;
29
29
  get body(): HttpServerResponseBody | undefined;
30
30
  set body(value: HttpServerResponseBody | undefined);
31
31
  get bodyType(): HttpServerResponseBodyType;
@@ -5,9 +5,9 @@ import { HttpHeaders } from '../http-headers.js';
5
5
  export class HttpServerResponse {
6
6
  #body;
7
7
  #bodyType;
8
+ headers = new HttpHeaders();
8
9
  statusCode;
9
10
  statusMessage;
10
- headers;
11
11
  get body() {
12
12
  return this.#body;
13
13
  }
@@ -35,10 +35,18 @@ export class HttpServerResponse {
35
35
  });
36
36
  }
37
37
  update(options) {
38
- this.statusCode = options.statusCode;
39
- this.statusMessage = options.statusMessage;
40
- this.headers = new HttpHeaders(options.headers);
41
- this.body = options.body;
38
+ if (isDefined(options.statusCode)) {
39
+ this.statusCode = options.statusCode;
40
+ }
41
+ if (isDefined(options.statusMessage)) {
42
+ this.statusMessage = options.statusMessage;
43
+ }
44
+ if (isDefined(options.headers)) {
45
+ this.headers.setMany(options.headers);
46
+ }
47
+ if (isDefined(options.body)) {
48
+ this.body = options.body;
49
+ }
42
50
  if (isDefined(options.cookies)) {
43
51
  for (const [name, cookie] of objectEntries(options.cookies)) {
44
52
  this.headers.append('Set-Cookie', formatSetCookie(name, cookie.value, cookie));
@@ -1,11 +1,11 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  // Mock supports before importing PrettyPrintLogFormatter
3
- vi.mock('#/supports.js', () => ({
3
+ vi.mock('../../supports.js', () => ({
4
4
  supportsColoredStdout: true,
5
5
  supportsColoredStderr: true,
6
6
  }));
7
- import { LogLevel } from '../level.js';
8
7
  import { PrettyPrintLogFormatter } from '../formatters/pretty-print.js';
8
+ import { LogLevel } from '../level.js';
9
9
  describe('PrettyPrintLogFormatter', () => {
10
10
  const formatter = new PrettyPrintLogFormatter();
11
11
  it('should format a log entry with an error and no context', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.172",
3
+ "version": "0.93.174",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -203,7 +203,6 @@
203
203
  "typedoc-plugin-missing-exports": "4.1",
204
204
  "typescript": "5.9",
205
205
  "typescript-eslint": "8.57",
206
- "vite-tsconfig-paths": "6.1",
207
206
  "vitest": "4.1"
208
207
  }
209
208
  }