@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.
- package/README.md +31 -0
- package/dist/build-manifest.json +43 -19
- package/dist/config.js +18 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ingest/TraceIngestGateway.d.ts +22 -0
- package/dist/ingest/TraceIngestGateway.js +253 -0
- package/dist/register.js +58 -2
- package/dist/storage/ProxyTraceStorage.d.ts +12 -0
- package/dist/storage/ProxyTraceStorage.js +109 -0
- package/dist/storage/TraceContentBudget.js +1 -0
- package/dist/storage/TraceServiceTag.d.ts +5 -0
- package/dist/storage/TraceServiceTag.js +43 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +2 -0
- package/dist/types.d.ts +10 -0
- package/package.json +2 -2
- package/src/config.ts +23 -0
- package/src/index.ts +1 -0
- package/src/ingest/TraceIngestGateway.ts +360 -0
- package/src/register.ts +67 -2
- package/src/storage/ProxyTraceStorage.ts +190 -0
- package/src/storage/TraceContentBudget.ts +1 -0
- package/src/storage/TraceServiceTag.ts +56 -0
- package/src/storage/index.ts +2 -0
- package/src/types.ts +11 -0
|
@@ -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;
|
|
@@ -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;
|
package/dist/storage/index.d.ts
CHANGED
package/dist/storage/index.js
CHANGED
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": "
|
|
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.
|
|
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;
|