@zintrust/trace 0.9.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ import { ErrorFactory, RemoteSignedJson } from '@zintrust/core';
2
+ const ensureConfigured = (settings) => {
3
+ if (settings.baseUrl.trim() === '') {
4
+ throw ErrorFactory.createConfigError('TRACE_PROXY_URL is required when TRACE_PROXY=true');
5
+ }
6
+ if (settings.keyId.trim() === '' || settings.secret.trim() === '') {
7
+ throw ErrorFactory.createConfigError('TRACE_PROXY signing credentials are required when TRACE_PROXY=true');
8
+ }
9
+ };
10
+ const normalizePath = (value) => {
11
+ const trimmed = value.trim();
12
+ if (trimmed === '')
13
+ return '/zin/trace/write';
14
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
15
+ };
16
+ const trimTrailingSlashes = (value) => {
17
+ let trimmed = value;
18
+ while (trimmed.endsWith('/')) {
19
+ trimmed = trimmed.slice(0, -1);
20
+ }
21
+ return trimmed;
22
+ };
23
+ const createUnsupportedReadError = () => ErrorFactory.createConfigError('Trace proxy sender storage does not expose dashboard/query operations. Use the trace server for reads.');
24
+ const buildSettings = (settings) => {
25
+ ensureConfigured(settings);
26
+ const normalizedPath = normalizePath(settings.path);
27
+ return {
28
+ baseUrl: settings.baseUrl,
29
+ keyId: settings.keyId,
30
+ secret: settings.secret,
31
+ timeoutMs: settings.timeoutMs,
32
+ signaturePathPrefixToStrip: new URL(settings.baseUrl).pathname,
33
+ missingUrlMessage: 'TRACE_PROXY_URL is required when TRACE_PROXY=true',
34
+ missingCredentialsMessage: 'TRACE_PROXY signing credentials are required when TRACE_PROXY=true',
35
+ messages: {
36
+ unauthorized: 'Trace proxy rejected the request credentials',
37
+ forbidden: 'Trace proxy rejected the request signature',
38
+ rateLimited: 'Trace proxy rate-limited the request',
39
+ rejected: 'Trace proxy rejected the request payload',
40
+ error: 'Trace proxy request failed',
41
+ timedOut: 'Trace proxy request timed out',
42
+ },
43
+ normalizedPath,
44
+ };
45
+ };
46
+ const appendSuffix = (path, suffix) => {
47
+ const base = trimTrailingSlashes(normalizePath(path));
48
+ const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
49
+ return `${base}${tail}`;
50
+ };
51
+ const unsupportedQueryEntries = async () => {
52
+ throw createUnsupportedReadError();
53
+ };
54
+ const unsupportedGetEntry = async () => {
55
+ throw createUnsupportedReadError();
56
+ };
57
+ const unsupportedGetBatch = async () => {
58
+ throw createUnsupportedReadError();
59
+ };
60
+ const unsupportedQueryBatchEntries = async () => {
61
+ throw createUnsupportedReadError();
62
+ };
63
+ const unsupportedPrune = async () => {
64
+ throw createUnsupportedReadError();
65
+ };
66
+ const unsupportedClear = async () => {
67
+ throw createUnsupportedReadError();
68
+ };
69
+ const unsupportedGetMonitoring = async () => {
70
+ throw createUnsupportedReadError();
71
+ };
72
+ const unsupportedAddMonitoring = async () => {
73
+ throw createUnsupportedReadError();
74
+ };
75
+ const unsupportedRemoveMonitoring = async () => {
76
+ throw createUnsupportedReadError();
77
+ };
78
+ const unsupportedStats = async () => {
79
+ throw createUnsupportedReadError();
80
+ };
81
+ export const ProxyTraceStorage = Object.freeze({
82
+ create(settings) {
83
+ const normalized = buildSettings(settings);
84
+ return Object.freeze({
85
+ async writeEntry(entry) {
86
+ await RemoteSignedJson.request(normalized, normalized.normalizedPath, {
87
+ entry,
88
+ });
89
+ },
90
+ async updateEntry(uuid, patch) {
91
+ await RemoteSignedJson.request(normalized, appendSuffix(normalized.normalizedPath, '/update'), { uuid, patch });
92
+ },
93
+ async markFamilyStale(familyHash, exceptUuid) {
94
+ await RemoteSignedJson.request(normalized, appendSuffix(normalized.normalizedPath, '/mark-family-stale'), { familyHash, exceptUuid });
95
+ },
96
+ queryEntries: unsupportedQueryEntries,
97
+ getEntry: unsupportedGetEntry,
98
+ getBatch: unsupportedGetBatch,
99
+ queryBatchEntries: unsupportedQueryBatchEntries,
100
+ prune: unsupportedPrune,
101
+ clear: unsupportedClear,
102
+ getMonitoring: unsupportedGetMonitoring,
103
+ addMonitoring: unsupportedAddMonitoring,
104
+ removeMonitoring: unsupportedRemoveMonitoring,
105
+ stats: unsupportedStats,
106
+ });
107
+ },
108
+ });
109
+ export default ProxyTraceStorage;
@@ -179,6 +179,7 @@ const getCoreRuntime = async () => {
179
179
  };
180
180
  const getQueueWorkerApi = async () => {
181
181
  try {
182
+ // @ts-ignore
182
183
  const mod = (await import('@zintrust/workers'));
183
184
  return typeof mod.createQueueWorker === 'function' ? mod : null;
184
185
  }
@@ -0,0 +1,5 @@
1
+ import type { ITraceConfig, ITraceStorage } from '../types';
2
+ export declare const TraceServiceTag: Readonly<{
3
+ wrapStorage(storage: ITraceStorage, config: ITraceConfig): ITraceStorage;
4
+ }>;
5
+ export default TraceServiceTag;
@@ -0,0 +1,43 @@
1
+ import { ErrorFactory } from '@zintrust/core';
2
+ const appendServiceTag = (entry, serviceTag) => {
3
+ const normalizedTag = serviceTag?.trim() ?? '';
4
+ if (normalizedTag === '' || entry.tags.includes(normalizedTag)) {
5
+ return entry;
6
+ }
7
+ return {
8
+ ...entry,
9
+ tags: [...entry.tags, normalizedTag],
10
+ };
11
+ };
12
+ const unsupportedRead = async () => {
13
+ throw ErrorFactory.createConfigError('Trace proxy mode only supports runtime persistence on the sender. Query the trace server database or dashboard directly.');
14
+ };
15
+ const bindOrUnsupported = (method) => {
16
+ if (method === undefined) {
17
+ return unsupportedRead;
18
+ }
19
+ return method;
20
+ };
21
+ export const TraceServiceTag = Object.freeze({
22
+ wrapStorage(storage, config) {
23
+ const writeEntry = async (entry) => {
24
+ await storage.writeEntry(appendServiceTag(entry, config.serviceTag));
25
+ };
26
+ return Object.freeze({
27
+ writeEntry,
28
+ updateEntry: storage.updateEntry.bind(storage),
29
+ markFamilyStale: storage.markFamilyStale.bind(storage),
30
+ queryEntries: bindOrUnsupported(storage.queryEntries?.bind(storage)),
31
+ getEntry: bindOrUnsupported(storage.getEntry?.bind(storage)),
32
+ getBatch: bindOrUnsupported(storage.getBatch?.bind(storage)),
33
+ queryBatchEntries: bindOrUnsupported(storage.queryBatchEntries?.bind(storage)),
34
+ prune: bindOrUnsupported(storage.prune?.bind(storage)),
35
+ clear: bindOrUnsupported(storage.clear?.bind(storage)),
36
+ getMonitoring: bindOrUnsupported(storage.getMonitoring?.bind(storage)),
37
+ addMonitoring: bindOrUnsupported(storage.addMonitoring?.bind(storage)),
38
+ removeMonitoring: bindOrUnsupported(storage.removeMonitoring?.bind(storage)),
39
+ stats: bindOrUnsupported(storage.stats?.bind(storage)),
40
+ });
41
+ },
42
+ });
43
+ export default TraceServiceTag;
@@ -1,2 +1,4 @@
1
1
  export { TraceStorage } from './TraceStorage';
2
2
  export type { ITraceStorage } from './TraceStorage';
3
+ export { ProxyTraceStorage } from './ProxyTraceStorage';
4
+ export { TraceServiceTag } from './TraceServiceTag';
@@ -1 +1,3 @@
1
1
  export { TraceStorage } from './TraceStorage.js';
2
+ export { ProxyTraceStorage } from './ProxyTraceStorage.js';
3
+ export { TraceServiceTag } from './TraceServiceTag.js';
package/dist/types.d.ts CHANGED
@@ -316,6 +316,14 @@ export type TraceContentDispatchConfig = {
316
316
  enqueueTimeoutMs: number;
317
317
  worker: TraceContentDispatchWorkerConfig;
318
318
  };
319
+ export type TraceProxyConfig = {
320
+ enabled: boolean;
321
+ url?: string;
322
+ path: string;
323
+ keyId?: string;
324
+ secret?: string;
325
+ timeoutMs: number;
326
+ };
319
327
  export type TraceWatcherToggle = boolean | TraceFilterRule;
320
328
  export type TraceRequestWatcherToggle = boolean | TraceRequestWatcherConfig;
321
329
  export type TraceClientRequestWatcherToggle = boolean | TraceClientRequestWatcherConfig;
@@ -345,6 +353,8 @@ export interface ITraceConfig {
345
353
  enabled: boolean;
346
354
  connection?: string;
347
355
  observeConnection?: string;
356
+ serviceTag?: string;
357
+ proxy: TraceProxyConfig;
348
358
  pruneAfterHours: number;
349
359
  ignoreRoutes: string[];
350
360
  ignorePaths: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.9.3",
3
+ "version": "1.2.0",
4
4
  "description": "Trace assistant for ZinTrust: logs requests, queries, exceptions, jobs, and more.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  "node": ">=20.0.0"
41
41
  },
42
42
  "peerDependencies": {
43
- "@zintrust/core": "^0.9.2"
43
+ "@zintrust/core": "^0.9.6"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
package/src/config.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  TraceConfigOverrides,
9
9
  TraceContentDispatchConfig,
10
10
  TraceFilterRule,
11
+ TraceProxyConfig,
11
12
  TraceRequestWatcherConfig,
12
13
  TraceWatcherToggle,
13
14
  } from './types';
@@ -253,10 +254,31 @@ const mergeContentDispatch = (
253
254
  };
254
255
  };
255
256
 
257
+ const mergeProxyConfig = (
258
+ base: TraceProxyConfig,
259
+ override?: TraceConfigOverrides['proxy']
260
+ ): TraceProxyConfig => {
261
+ if (override === undefined) return base;
262
+
263
+ return {
264
+ ...base,
265
+ ...override,
266
+ };
267
+ };
268
+
256
269
  const DEFAULTS: ITraceConfig = Object.freeze({
257
270
  enabled: false,
258
271
  connection: undefined,
259
272
  observeConnection: undefined,
273
+ serviceTag: undefined,
274
+ proxy: {
275
+ enabled: false,
276
+ url: undefined,
277
+ path: '/zin/trace/write',
278
+ keyId: undefined,
279
+ secret: undefined,
280
+ timeoutMs: 30000,
281
+ },
260
282
  pruneAfterHours: 24,
261
283
  ignoreRoutes: ['/trace', '/health', '/ping'],
262
284
  ignorePaths: [],
@@ -342,6 +364,7 @@ export const TraceConfig = Object.freeze({
342
364
  return Object.freeze({
343
365
  ...DEFAULTS,
344
366
  ...overrides,
367
+ proxy: mergeProxyConfig(DEFAULTS.proxy, overrides.proxy),
345
368
  contentDispatch: mergeContentDispatch(DEFAULTS.contentDispatch, overrides.contentDispatch),
346
369
  watchers: mergeWatchers(DEFAULTS.watchers, overrides.watchers),
347
370
  redaction: {
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export { TraceContext } from './context';
31
31
  // ---------------------------------------------------------------------------
32
32
  export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
33
33
  export type { TraceDashboardOptions, TraceDashboardRegistrationOptions } from './dashboard/routes';
34
+ export { registerTraceIngestGateway, TraceIngestGateway } from './ingest/TraceIngestGateway';
34
35
 
35
36
  // ---------------------------------------------------------------------------
36
37
  // Watchers (named re-exports for use with custom wiring)
@@ -0,0 +1,360 @@
1
+ import {
2
+ Env,
3
+ ErrorFactory,
4
+ Router,
5
+ SignedRequest,
6
+ useDatabase,
7
+ type IRequest,
8
+ type IResponse,
9
+ type IRouter,
10
+ type RouteOptions,
11
+ } from '@zintrust/core';
12
+ import { TraceConfig } from '../config';
13
+ import { TraceStorage } from '../storage';
14
+ import type { ITraceEntry, ITraceStorage } from '../types';
15
+
16
+ type TraceIngestGatewaySettings = {
17
+ basePath: string;
18
+ keyId: string;
19
+ secret: string;
20
+ signingWindowMs: number;
21
+ nonceTtlMs: number;
22
+ middleware: ReadonlyArray<string>;
23
+ storage: ITraceStorage;
24
+ };
25
+
26
+ type TraceIngestGatewayOverrides = Partial<
27
+ Omit<TraceIngestGatewaySettings, 'storage'> & { storage: ITraceStorage; connectionName: string }
28
+ >;
29
+
30
+ type TraceGatewayFailure = {
31
+ ok: false;
32
+ error: {
33
+ code: string;
34
+ message: string;
35
+ details?: unknown;
36
+ };
37
+ };
38
+
39
+ type TraceGatewaySuccess = {
40
+ ok: true;
41
+ };
42
+
43
+ const nonces = new Map<string, number>();
44
+
45
+ const nowMs = (): number => Date.now();
46
+
47
+ const normalizePath = (value: string): string => {
48
+ const trimmed = value.trim();
49
+ if (trimmed === '') return '/zin/trace/write';
50
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
51
+ };
52
+
53
+ const parseMiddleware = (value: string): ReadonlyArray<string> =>
54
+ value
55
+ .split(',')
56
+ .map((entry) => entry.trim())
57
+ .filter((entry) => entry.length > 0);
58
+
59
+ const trimTrailingSlashes = (value: string): string => {
60
+ let trimmed = value;
61
+ while (trimmed.endsWith('/')) {
62
+ trimmed = trimmed.slice(0, -1);
63
+ }
64
+ return trimmed;
65
+ };
66
+
67
+ const appendSuffix = (path: string, suffix: string): string => {
68
+ const base = trimTrailingSlashes(normalizePath(path));
69
+ const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
70
+ return `${base}${tail}`;
71
+ };
72
+
73
+ const cleanupExpiredNonces = (): void => {
74
+ const current = nowMs();
75
+ for (const [nonceKey, expiresAt] of nonces.entries()) {
76
+ if (expiresAt <= current) {
77
+ nonces.delete(nonceKey);
78
+ }
79
+ }
80
+ };
81
+
82
+ const storeNonce = async (keyId: string, nonce: string, ttlMs: number): Promise<boolean> => {
83
+ cleanupExpiredNonces();
84
+ const nonceKey = `${keyId}:${nonce}`;
85
+ if (nonces.has(nonceKey)) return false;
86
+ nonces.set(nonceKey, nowMs() + Math.max(ttlMs, 1));
87
+ return true;
88
+ };
89
+
90
+ const getBodyRecord = (req: IRequest): Record<string, unknown> => {
91
+ const body = req.getBody?.() ?? req.body;
92
+ if (typeof body === 'object' && body !== null && !Array.isArray(body)) {
93
+ return body as Record<string, unknown>;
94
+ }
95
+ return {};
96
+ };
97
+
98
+ const getRawBody = (req: IRequest): string => {
99
+ const rawText = req.context['rawBodyText'];
100
+ if (typeof rawText === 'string') return rawText;
101
+ return JSON.stringify(getBodyRecord(req));
102
+ };
103
+
104
+ const toIncomingHeaders = (req: IRequest): Record<string, string | undefined> => {
105
+ const headers = req.getHeaders();
106
+ const normalize = (value: string | string[] | undefined): string | undefined => {
107
+ if (Array.isArray(value)) return value.join(',');
108
+ return value;
109
+ };
110
+
111
+ return {
112
+ 'x-zt-key-id': normalize(headers['x-zt-key-id']),
113
+ 'x-zt-timestamp': normalize(headers['x-zt-timestamp']),
114
+ 'x-zt-nonce': normalize(headers['x-zt-nonce']),
115
+ 'x-zt-body-sha256': normalize(headers['x-zt-body-sha256']),
116
+ 'x-zt-signature': normalize(headers['x-zt-signature']),
117
+ };
118
+ };
119
+
120
+ const sendFailure = (
121
+ res: IResponse,
122
+ status: number,
123
+ code: string,
124
+ message: string,
125
+ details?: unknown
126
+ ): void => {
127
+ const payload: TraceGatewayFailure = {
128
+ ok: false,
129
+ error: { code, message, details },
130
+ };
131
+ res.status(status).json(payload);
132
+ };
133
+
134
+ const sendSuccess = (res: IResponse): void => {
135
+ const payload: TraceGatewaySuccess = { ok: true };
136
+ res.status(200).json(payload);
137
+ };
138
+
139
+ const verifyRequest = async (
140
+ req: IRequest,
141
+ bodyText: string,
142
+ settings: TraceIngestGatewaySettings,
143
+ path: string
144
+ ): Promise<{ ok: true } | { ok: false; code: string; status: number; message: string }> => {
145
+ if (settings.keyId.trim() === '' || settings.secret.trim() === '') {
146
+ return {
147
+ ok: false,
148
+ code: 'CONFIG_ERROR',
149
+ status: 500,
150
+ message: 'Trace ingest signing credentials are not configured',
151
+ };
152
+ }
153
+
154
+ const verifyResult = await SignedRequest.verify({
155
+ method: req.getMethod(),
156
+ url: new URL(path, 'http://localhost'),
157
+ body: bodyText,
158
+ headers: toIncomingHeaders(req),
159
+ nowMs: nowMs(),
160
+ windowMs: settings.signingWindowMs,
161
+ verifyNonce: async (keyId: string, nonce: string) =>
162
+ storeNonce(keyId, nonce, settings.nonceTtlMs),
163
+ getSecretForKeyId: async (keyId: string) => {
164
+ if (keyId === settings.keyId) return settings.secret;
165
+ return undefined;
166
+ },
167
+ });
168
+
169
+ if (verifyResult.ok === true) return { ok: true };
170
+
171
+ return {
172
+ ok: false,
173
+ code: verifyResult.code,
174
+ status: verifyResult.code === 'EXPIRED' || verifyResult.code === 'REPLAYED' ? 401 : 403,
175
+ message: verifyResult.message,
176
+ };
177
+ };
178
+
179
+ const createWriteHandler = (settings: TraceIngestGatewaySettings, path: string) => {
180
+ return async (req: IRequest, res: IResponse): Promise<void> => {
181
+ const body = getBodyRecord(req);
182
+ const auth = await verifyRequest(req, getRawBody(req), settings, path);
183
+ if (auth.ok === false) {
184
+ sendFailure(res, auth.status, auth.code, auth.message);
185
+ return;
186
+ }
187
+
188
+ const entry = body['entry'];
189
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
190
+ sendFailure(res, 400, 'VALIDATION_ERROR', 'entry must be an object');
191
+ return;
192
+ }
193
+
194
+ await settings.storage.writeEntry(entry as ITraceEntry);
195
+ sendSuccess(res);
196
+ };
197
+ };
198
+
199
+ const createUpdateHandler = (settings: TraceIngestGatewaySettings, path: string) => {
200
+ return async (req: IRequest, res: IResponse): Promise<void> => {
201
+ const body = getBodyRecord(req);
202
+ const auth = await verifyRequest(req, getRawBody(req), settings, path);
203
+ if (auth.ok === false) {
204
+ sendFailure(res, auth.status, auth.code, auth.message);
205
+ return;
206
+ }
207
+
208
+ const uuid = body['uuid'];
209
+ const patch = body['patch'];
210
+ if (typeof uuid !== 'string' || uuid.trim() === '') {
211
+ sendFailure(res, 400, 'VALIDATION_ERROR', 'uuid is required');
212
+ return;
213
+ }
214
+
215
+ if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {
216
+ sendFailure(res, 400, 'VALIDATION_ERROR', 'patch must be an object');
217
+ return;
218
+ }
219
+
220
+ await settings.storage.updateEntry(
221
+ uuid,
222
+ patch as Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
223
+ );
224
+ sendSuccess(res);
225
+ };
226
+ };
227
+
228
+ const createMarkFamilyStaleHandler = (settings: TraceIngestGatewaySettings, path: string) => {
229
+ return async (req: IRequest, res: IResponse): Promise<void> => {
230
+ const body = getBodyRecord(req);
231
+ const auth = await verifyRequest(req, getRawBody(req), settings, path);
232
+ if (auth.ok === false) {
233
+ sendFailure(res, auth.status, auth.code, auth.message);
234
+ return;
235
+ }
236
+
237
+ const familyHash = body['familyHash'];
238
+ const exceptUuid = body['exceptUuid'];
239
+
240
+ if (typeof familyHash !== 'string' || familyHash.trim() === '') {
241
+ sendFailure(res, 400, 'VALIDATION_ERROR', 'familyHash is required');
242
+ return;
243
+ }
244
+
245
+ if (typeof exceptUuid !== 'string' || exceptUuid.trim() === '') {
246
+ sendFailure(res, 400, 'VALIDATION_ERROR', 'exceptUuid is required');
247
+ return;
248
+ }
249
+
250
+ await settings.storage.markFamilyStale(familyHash, exceptUuid);
251
+ sendSuccess(res);
252
+ };
253
+ };
254
+
255
+ const resolveStorage = (overrides?: TraceIngestGatewayOverrides): ITraceStorage => {
256
+ if (overrides?.storage !== undefined) return overrides.storage;
257
+
258
+ const connectionName = overrides?.connectionName ?? TraceConfig.merge().connection;
259
+ const db = useDatabase(undefined, connectionName);
260
+ if (db === undefined) {
261
+ throw ErrorFactory.createConfigError('Trace ingest connection is not configured.', {
262
+ connectionName,
263
+ envKey: 'TRACE_DB_CONNECTION',
264
+ });
265
+ }
266
+
267
+ return TraceStorage.resolveStorage(db);
268
+ };
269
+
270
+ const readConfiguredKeyId = (overrides?: TraceIngestGatewayOverrides): string => {
271
+ return (overrides?.keyId ?? Env.get('TRACE_PROXY_KEY_ID', '')).trim();
272
+ };
273
+
274
+ const readConfiguredSecret = (overrides?: TraceIngestGatewayOverrides): string => {
275
+ return (overrides?.secret ?? Env.get('TRACE_PROXY_SECRET', '')).trim();
276
+ };
277
+
278
+ const resolveKeyId = (overrides?: TraceIngestGatewayOverrides): string => {
279
+ const configuredKeyId = readConfiguredKeyId(overrides);
280
+ if (configuredKeyId !== '') return configuredKeyId;
281
+ return (Env.APP_NAME || 'zintrust').trim();
282
+ };
283
+
284
+ const resolveSecret = (overrides?: TraceIngestGatewayOverrides): string => {
285
+ const configuredSecret = readConfiguredSecret(overrides);
286
+ if (configuredSecret !== '') return configuredSecret;
287
+ return Env.APP_KEY;
288
+ };
289
+
290
+ const resolveSigningWindowMs = (overrides?: TraceIngestGatewayOverrides): number => {
291
+ return overrides?.signingWindowMs ?? Env.getInt('TRACE_PROXY_SIGNING_WINDOW_MS', 60000);
292
+ };
293
+
294
+ const resolveNonceTtlMs = (overrides?: TraceIngestGatewayOverrides): number => {
295
+ return overrides?.nonceTtlMs ?? Env.getInt('TRACE_PROXY_NONCE_TTL_MS', 120000);
296
+ };
297
+
298
+ const resolveMiddleware = (overrides?: TraceIngestGatewayOverrides): ReadonlyArray<string> => {
299
+ return overrides?.middleware ?? parseMiddleware(Env.get('TRACE_PROXY_MIDDLEWARE', ''));
300
+ };
301
+
302
+ const readSettings = (overrides?: TraceIngestGatewayOverrides): TraceIngestGatewaySettings => {
303
+ return {
304
+ basePath: normalizePath(overrides?.basePath ?? Env.get('TRACE_PROXY_PATH', '/zin/trace/write')),
305
+ keyId: resolveKeyId(overrides),
306
+ secret: resolveSecret(overrides),
307
+ signingWindowMs: resolveSigningWindowMs(overrides),
308
+ nonceTtlMs: resolveNonceTtlMs(overrides),
309
+ middleware: resolveMiddleware(overrides),
310
+ storage: resolveStorage(overrides),
311
+ };
312
+ };
313
+
314
+ const getRouteOptions = (settings: TraceIngestGatewaySettings): RouteOptions | undefined => {
315
+ if (settings.middleware.length === 0) return undefined;
316
+ return { middleware: settings.middleware } as RouteOptions;
317
+ };
318
+
319
+ const registerRoutes = (router: IRouter, settings: TraceIngestGatewaySettings): void => {
320
+ const routeOptions = getRouteOptions(settings);
321
+ const updatePath = appendSuffix(settings.basePath, '/update');
322
+ const markFamilyStalePath = appendSuffix(settings.basePath, '/mark-family-stale');
323
+
324
+ Router.post(
325
+ router,
326
+ settings.basePath,
327
+ createWriteHandler(settings, settings.basePath),
328
+ routeOptions
329
+ );
330
+ Router.post(router, updatePath, createUpdateHandler(settings, updatePath), routeOptions);
331
+ Router.post(
332
+ router,
333
+ markFamilyStalePath,
334
+ createMarkFamilyStaleHandler(settings, markFamilyStalePath),
335
+ routeOptions
336
+ );
337
+ };
338
+
339
+ export const TraceIngestGateway = Object.freeze({
340
+ create(overrides?: TraceIngestGatewayOverrides): {
341
+ registerRoutes: (router: IRouter) => void;
342
+ } {
343
+ const settings = readSettings(overrides);
344
+
345
+ return {
346
+ registerRoutes(router: IRouter): void {
347
+ registerRoutes(router, settings);
348
+ },
349
+ };
350
+ },
351
+ });
352
+
353
+ export const registerTraceIngestGateway = (
354
+ router: IRouter,
355
+ overrides?: TraceIngestGatewayOverrides
356
+ ): void => {
357
+ TraceIngestGateway.create(overrides).registerRoutes(router);
358
+ };
359
+
360
+ export default TraceIngestGateway;