@zintrust/trace 0.9.2 → 0.9.4

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,215 @@
1
+ import { Env, ErrorFactory, Router, SignedRequest, useDatabase, } from '@zintrust/core';
2
+ import { TraceConfig } from '../config.js';
3
+ import { TraceStorage } from '../storage/index.js';
4
+ const nonces = new Map();
5
+ const nowMs = () => Date.now();
6
+ const normalizePath = (value) => {
7
+ const trimmed = value.trim();
8
+ if (trimmed === '')
9
+ return '/zin/trace/write';
10
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
11
+ };
12
+ const parseMiddleware = (value) => value
13
+ .split(',')
14
+ .map((entry) => entry.trim())
15
+ .filter((entry) => entry.length > 0);
16
+ const appendSuffix = (path, suffix) => {
17
+ const base = normalizePath(path).replace(/\/+$/, '');
18
+ const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
19
+ return `${base}${tail}`;
20
+ };
21
+ const cleanupExpiredNonces = () => {
22
+ const current = nowMs();
23
+ for (const [nonceKey, expiresAt] of nonces.entries()) {
24
+ if (expiresAt <= current) {
25
+ nonces.delete(nonceKey);
26
+ }
27
+ }
28
+ };
29
+ const storeNonce = async (keyId, nonce, ttlMs) => {
30
+ cleanupExpiredNonces();
31
+ const nonceKey = `${keyId}:${nonce}`;
32
+ if (nonces.has(nonceKey))
33
+ return false;
34
+ nonces.set(nonceKey, nowMs() + Math.max(ttlMs, 1));
35
+ return true;
36
+ };
37
+ const getBodyRecord = (req) => {
38
+ const body = req.getBody?.() ?? req.body;
39
+ if (typeof body === 'object' && body !== null && !Array.isArray(body)) {
40
+ return body;
41
+ }
42
+ return {};
43
+ };
44
+ const getRawBody = (req) => {
45
+ const rawText = req.context['rawBodyText'];
46
+ if (typeof rawText === 'string')
47
+ return rawText;
48
+ return JSON.stringify(getBodyRecord(req));
49
+ };
50
+ const toIncomingHeaders = (req) => {
51
+ const headers = req.getHeaders();
52
+ const normalize = (value) => {
53
+ if (Array.isArray(value))
54
+ return value.join(',');
55
+ return value;
56
+ };
57
+ return {
58
+ 'x-zt-key-id': normalize(headers['x-zt-key-id']),
59
+ 'x-zt-timestamp': normalize(headers['x-zt-timestamp']),
60
+ 'x-zt-nonce': normalize(headers['x-zt-nonce']),
61
+ 'x-zt-body-sha256': normalize(headers['x-zt-body-sha256']),
62
+ 'x-zt-signature': normalize(headers['x-zt-signature']),
63
+ };
64
+ };
65
+ const sendFailure = (res, status, code, message, details) => {
66
+ const payload = {
67
+ ok: false,
68
+ error: { code, message, details },
69
+ };
70
+ res.status(status).json(payload);
71
+ };
72
+ const sendSuccess = (res) => {
73
+ const payload = { ok: true };
74
+ res.status(200).json(payload);
75
+ };
76
+ const verifyRequest = async (req, bodyText, settings, path) => {
77
+ if (settings.keyId.trim() === '' || settings.secret.trim() === '') {
78
+ return {
79
+ ok: false,
80
+ code: 'CONFIG_ERROR',
81
+ status: 500,
82
+ message: 'Trace ingest signing credentials are not configured',
83
+ };
84
+ }
85
+ const verifyResult = await SignedRequest.verify({
86
+ method: req.getMethod(),
87
+ url: new URL(path, 'http://localhost'),
88
+ body: bodyText,
89
+ headers: toIncomingHeaders(req),
90
+ nowMs: nowMs(),
91
+ windowMs: settings.signingWindowMs,
92
+ verifyNonce: async (keyId, nonce) => storeNonce(keyId, nonce, settings.nonceTtlMs),
93
+ getSecretForKeyId: async (keyId) => {
94
+ if (keyId === settings.keyId)
95
+ return settings.secret;
96
+ return undefined;
97
+ },
98
+ });
99
+ if (verifyResult.ok === true)
100
+ return { ok: true };
101
+ return {
102
+ ok: false,
103
+ code: verifyResult.code,
104
+ status: verifyResult.code === 'EXPIRED' || verifyResult.code === 'REPLAYED' ? 401 : 403,
105
+ message: verifyResult.message,
106
+ };
107
+ };
108
+ const createWriteHandler = (settings, path) => {
109
+ return async (req, res) => {
110
+ const body = getBodyRecord(req);
111
+ const auth = await verifyRequest(req, getRawBody(req), settings, path);
112
+ if (auth.ok === false) {
113
+ sendFailure(res, auth.status, auth.code, auth.message);
114
+ return;
115
+ }
116
+ const entry = body['entry'];
117
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
118
+ sendFailure(res, 400, 'VALIDATION_ERROR', 'entry must be an object');
119
+ return;
120
+ }
121
+ await settings.storage.writeEntry(entry);
122
+ sendSuccess(res);
123
+ };
124
+ };
125
+ const createUpdateHandler = (settings, path) => {
126
+ return async (req, res) => {
127
+ const body = getBodyRecord(req);
128
+ const auth = await verifyRequest(req, getRawBody(req), settings, path);
129
+ if (auth.ok === false) {
130
+ sendFailure(res, auth.status, auth.code, auth.message);
131
+ return;
132
+ }
133
+ const uuid = body['uuid'];
134
+ const patch = body['patch'];
135
+ if (typeof uuid !== 'string' || uuid.trim() === '') {
136
+ sendFailure(res, 400, 'VALIDATION_ERROR', 'uuid is required');
137
+ return;
138
+ }
139
+ if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {
140
+ sendFailure(res, 400, 'VALIDATION_ERROR', 'patch must be an object');
141
+ return;
142
+ }
143
+ await settings.storage.updateEntry(uuid, patch);
144
+ sendSuccess(res);
145
+ };
146
+ };
147
+ const createMarkFamilyStaleHandler = (settings, path) => {
148
+ return async (req, res) => {
149
+ const body = getBodyRecord(req);
150
+ const auth = await verifyRequest(req, getRawBody(req), settings, path);
151
+ if (auth.ok === false) {
152
+ sendFailure(res, auth.status, auth.code, auth.message);
153
+ return;
154
+ }
155
+ const familyHash = body['familyHash'];
156
+ const exceptUuid = body['exceptUuid'];
157
+ if (typeof familyHash !== 'string' || familyHash.trim() === '') {
158
+ sendFailure(res, 400, 'VALIDATION_ERROR', 'familyHash is required');
159
+ return;
160
+ }
161
+ if (typeof exceptUuid !== 'string' || exceptUuid.trim() === '') {
162
+ sendFailure(res, 400, 'VALIDATION_ERROR', 'exceptUuid is required');
163
+ return;
164
+ }
165
+ await settings.storage.markFamilyStale(familyHash, exceptUuid);
166
+ sendSuccess(res);
167
+ };
168
+ };
169
+ const resolveStorage = (overrides) => {
170
+ if (overrides?.storage !== undefined)
171
+ return overrides.storage;
172
+ const connectionName = overrides?.connectionName ?? TraceConfig.merge().connection;
173
+ const db = useDatabase(undefined, connectionName);
174
+ if (db === undefined) {
175
+ throw ErrorFactory.createConfigError('Trace ingest connection is not configured.', {
176
+ connectionName,
177
+ envKey: 'TRACE_DB_CONNECTION',
178
+ });
179
+ }
180
+ return TraceStorage.resolveStorage(db);
181
+ };
182
+ const readSettings = (overrides) => {
183
+ const configuredSecret = (overrides?.secret ?? Env.get('TRACE_PROXY_SECRET', '')).trim();
184
+ const configuredKeyId = (overrides?.keyId ?? Env.get('TRACE_PROXY_KEY_ID', '')).trim();
185
+ return {
186
+ basePath: normalizePath(overrides?.basePath ?? Env.get('TRACE_PROXY_PATH', '/zin/trace/write')),
187
+ keyId: configuredKeyId === '' ? (Env.APP_NAME || 'zintrust').trim() : configuredKeyId,
188
+ secret: configuredSecret === '' ? Env.APP_KEY : configuredSecret,
189
+ signingWindowMs: overrides?.signingWindowMs ?? Env.getInt('TRACE_PROXY_SIGNING_WINDOW_MS', 60000),
190
+ nonceTtlMs: overrides?.nonceTtlMs ?? Env.getInt('TRACE_PROXY_NONCE_TTL_MS', 120000),
191
+ middleware: overrides?.middleware ?? parseMiddleware(Env.get('TRACE_PROXY_MIDDLEWARE', '')),
192
+ storage: resolveStorage(overrides),
193
+ };
194
+ };
195
+ export const TraceIngestGateway = Object.freeze({
196
+ create(overrides) {
197
+ const settings = readSettings(overrides);
198
+ const routeOptions = settings.middleware.length > 0
199
+ ? { middleware: settings.middleware }
200
+ : undefined;
201
+ const updatePath = appendSuffix(settings.basePath, '/update');
202
+ const markFamilyStalePath = appendSuffix(settings.basePath, '/mark-family-stale');
203
+ return {
204
+ registerRoutes(router) {
205
+ Router.post(router, settings.basePath, createWriteHandler(settings, settings.basePath), routeOptions);
206
+ Router.post(router, updatePath, createUpdateHandler(settings, updatePath), routeOptions);
207
+ Router.post(router, markFamilyStalePath, createMarkFamilyStaleHandler(settings, markFamilyStalePath), routeOptions);
208
+ },
209
+ };
210
+ },
211
+ });
212
+ export const registerTraceIngestGateway = (router, overrides) => {
213
+ TraceIngestGateway.create(overrides).registerRoutes(router);
214
+ };
215
+ export default TraceIngestGateway;
package/dist/register.js CHANGED
@@ -21,13 +21,13 @@
21
21
  */
22
22
  import { TraceConfig } from './config.js';
23
23
  import { TraceContext } from './context.js';
24
- import { TraceStorage } from './storage/index.js';
24
+ import { ProxyTraceStorage, TraceServiceTag, TraceStorage } from './storage/index.js';
25
25
  import { TraceContentBudget } from './storage/TraceContentBudget.js';
26
26
  import { TraceContentRedaction } from './storage/TraceContentRedaction.js';
27
27
  import { TraceEntryFiltering } from './storage/TraceEntryFiltering.js';
28
28
  import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics.js';
29
+ import { assertTraceConnectionResolved, assertTraceStorageReady, resolveObservedConnectionName, resolveTraceConnectionName, } from './TraceConnection.js';
29
30
  const globalTraceRegisterState = globalThis;
30
- globalTraceRegisterState.__zintrust_system_trace_plugin_requested__ = true;
31
31
  const traceAlreadyInitialized = globalTraceRegisterState.__zintrust_system_trace_register_initialized__ === true;
32
32
  if (!traceAlreadyInitialized) {
33
33
  globalTraceRegisterState.__zintrust_system_trace_register_initialized__ = true;
@@ -40,11 +40,6 @@ const importCore = async () => {
40
40
  return {};
41
41
  }
42
42
  };
43
- const TRACE_REQUIRED_TABLES = [
44
- 'zin_trace_entries',
45
- 'zin_trace_entries_tags',
46
- 'zin_trace_monitoring',
47
- ];
48
43
  const resolveRegisterMiddleware = () => {
49
44
  const globalMiddlewareRegistrarState = globalThis;
50
45
  return (middleware) => {
@@ -57,30 +52,6 @@ const resolveRegisterMiddleware = () => {
57
52
  globalMiddlewareRegistrarState.__zintrust_pending_global_middlewares__.push(middleware);
58
53
  };
59
54
  };
60
- const resolveTraceConnectionName = (env, configuredConnection) => {
61
- const resolveDefaultConnection = () => {
62
- const defaultConnection = env?.get('DB_CONNECTION', '').trim() ?? '';
63
- if (defaultConnection === '' || defaultConnection === 'default')
64
- return 'default';
65
- return defaultConnection;
66
- };
67
- const explicitConnection = configuredConnection?.trim();
68
- if (explicitConnection !== undefined && explicitConnection !== '') {
69
- return explicitConnection === 'default' ? resolveDefaultConnection() : explicitConnection;
70
- }
71
- return resolveDefaultConnection();
72
- };
73
- const resolveObservedConnectionName = (env, configuredObservedConnection, storageConnectionName) => {
74
- if (typeof configuredObservedConnection === 'string' &&
75
- configuredObservedConnection.trim() !== '') {
76
- return resolveTraceConnectionName(env, configuredObservedConnection);
77
- }
78
- const defaultConnectionName = resolveTraceConnectionName(env);
79
- if (storageConnectionName !== defaultConnectionName) {
80
- return defaultConnectionName;
81
- }
82
- return storageConnectionName;
83
- };
84
55
  const isObjectValue = (value) => {
85
56
  return typeof value === 'object' && value !== null && !Array.isArray(value);
86
57
  };
@@ -160,43 +131,6 @@ const buildTraceRedactionOverrides = (input) => {
160
131
  ? redaction
161
132
  : undefined;
162
133
  };
163
- const createTraceConfigError = (coreApi, message, details) => {
164
- if (coreApi.ErrorFactory?.createConfigError !== undefined) {
165
- return coreApi.ErrorFactory.createConfigError(message, details);
166
- }
167
- const error = new globalThis.Error(message);
168
- error.name = 'ConfigError';
169
- error.code = 'CONFIG_ERROR';
170
- error.statusCode = 500;
171
- error.details = details;
172
- return error;
173
- };
174
- function assertTraceConnectionResolved(coreApi, db, params) {
175
- if (db !== undefined) {
176
- return;
177
- }
178
- throw createTraceConfigError(coreApi, `Trace connection "${params.connectionName}" could not be resolved.`, {
179
- connectionName: params.connectionName,
180
- envKey: params.envKey,
181
- hint: params.envKey === 'TRACE_DB_CONNECTION'
182
- ? 'Configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.'
183
- : 'Configure TRACE_QUERY_CONNECTION, or ensure DB_CONNECTION resolves to an existing database connection.',
184
- });
185
- }
186
- const assertTraceStorageReady = async (coreApi, db, connectionName) => {
187
- try {
188
- await Promise.all(TRACE_REQUIRED_TABLES.map(async (table) => {
189
- await db.queryOne(`SELECT 1 AS ok FROM ${table} LIMIT 1`, []);
190
- }));
191
- }
192
- catch (error) {
193
- throw createTraceConfigError(coreApi, `Trace storage connection "${connectionName}" is not ready. Create the database if needed and run \`zin migrate:trace\` before enabling TRACE_ENABLED.`, {
194
- connectionName,
195
- error,
196
- requiredTables: [...TRACE_REQUIRED_TABLES],
197
- });
198
- }
199
- };
200
134
  const core = (await importCore());
201
135
  const Env = core.Env;
202
136
  const startupOverrides = await resolveTraceStartupOverrides(core);
@@ -208,6 +142,15 @@ if (!traceAlreadyInitialized && Env) {
208
142
  const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
209
143
  const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
210
144
  const logMinLevelRaw = Env.get('TRACE_LOG_LEVEL', '').trim();
145
+ const traceProxyRaw = Env.get('TRACE_PROXY', '').trim();
146
+ const traceProxyUrlRaw = Env.get('TRACE_PROXY_URL', '').trim();
147
+ const traceProxyPathRaw = Env.get('TRACE_PROXY_PATH', '').trim();
148
+ const traceProxyKeyIdRaw = Env.get('TRACE_PROXY_KEY_ID', '').trim();
149
+ const traceProxySecretRaw = Env.get('TRACE_PROXY_SECRET', '').trim();
150
+ const traceProxyTimeoutRaw = Env.get('TRACE_PROXY_TIMEOUT_MS', '').trim();
151
+ const traceServiceTagRaw = Env.get('TRACE_SERVICE_TAG', '').trim();
152
+ const appNameRaw = Env.get('APP_NAME', '').trim();
153
+ const appKeyRaw = Env.get('APP_KEY', '').trim();
211
154
  const captureCachePayloadsRaw = Env.get('TRACE_CACHE_PAYLOADS', '').trim();
212
155
  const captureQueryBindingsRaw = Env.get('TRACE_QUERY_BINDINGS', '').trim();
213
156
  const contentDispatchDriverRaw = Env.get('TRACE_CONTENT_QUEUE_DRIVER', '').trim();
@@ -232,6 +175,21 @@ if (!traceAlreadyInitialized && Env) {
232
175
  const logMinLevel = (logMinLevelRaw === '' ? startupOverrides?.logMinLevel : logMinLevelRaw);
233
176
  const captureCachePayloads = parseEnvBool(captureCachePayloadsRaw) ?? startupOverrides?.captureCachePayloads;
234
177
  const captureQueryBindings = parseEnvBool(captureQueryBindingsRaw) ?? startupOverrides?.captureQueryBindings;
178
+ const traceProxyEnabled = parseEnvBool(traceProxyRaw) ?? startupOverrides?.proxy?.enabled;
179
+ const traceProxyUrl = traceProxyUrlRaw === '' ? startupOverrides?.proxy?.url : traceProxyUrlRaw;
180
+ const traceProxyPath = traceProxyPathRaw === '' ? startupOverrides?.proxy?.path : traceProxyPathRaw;
181
+ const traceProxyKeyId = traceProxyKeyIdRaw === ''
182
+ ? (startupOverrides?.proxy?.keyId ?? appNameRaw)
183
+ : traceProxyKeyIdRaw;
184
+ const traceProxySecret = traceProxySecretRaw === ''
185
+ ? (startupOverrides?.proxy?.secret ?? appKeyRaw)
186
+ : traceProxySecretRaw;
187
+ const traceProxyTimeout = traceProxyTimeoutRaw === ''
188
+ ? startupOverrides?.proxy?.timeoutMs
189
+ : Number.parseInt(traceProxyTimeoutRaw, 10);
190
+ const traceServiceTag = traceServiceTagRaw === ''
191
+ ? (startupOverrides?.serviceTag ?? appNameRaw).trim() || undefined
192
+ : traceServiceTagRaw;
235
193
  const contentDispatchDriver = contentDispatchDriverRaw === ''
236
194
  ? startupOverrides?.contentDispatch?.driver
237
195
  : contentDispatchDriverRaw;
@@ -267,6 +225,29 @@ if (!traceAlreadyInitialized && Env) {
267
225
  enabled,
268
226
  connection,
269
227
  observeConnection,
228
+ ...(typeof traceServiceTag === 'string' && traceServiceTag !== ''
229
+ ? { serviceTag: traceServiceTag }
230
+ : {}),
231
+ proxy: {
232
+ ...TraceConfig.defaults().proxy,
233
+ ...startupOverrides?.proxy,
234
+ ...(typeof traceProxyEnabled === 'boolean' ? { enabled: traceProxyEnabled } : {}),
235
+ ...(typeof traceProxyUrl === 'string' && traceProxyUrl !== ''
236
+ ? { url: traceProxyUrl }
237
+ : {}),
238
+ ...(typeof traceProxyPath === 'string' && traceProxyPath !== ''
239
+ ? { path: traceProxyPath }
240
+ : {}),
241
+ ...(typeof traceProxyKeyId === 'string' && traceProxyKeyId !== ''
242
+ ? { keyId: traceProxyKeyId }
243
+ : {}),
244
+ ...(typeof traceProxySecret === 'string' && traceProxySecret !== ''
245
+ ? { secret: traceProxySecret }
246
+ : {}),
247
+ ...(typeof traceProxyTimeout === 'number' && Number.isFinite(traceProxyTimeout)
248
+ ? { timeoutMs: traceProxyTimeout }
249
+ : {}),
250
+ },
270
251
  ...(typeof pruneAfterHours === 'number' && Number.isFinite(pruneAfterHours)
271
252
  ? { pruneAfterHours }
272
253
  : {}),
@@ -330,7 +311,16 @@ if (!traceAlreadyInitialized && Env) {
330
311
  envKey: 'TRACE_QUERY_CONNECTION',
331
312
  });
332
313
  await assertTraceStorageReady(core, storageDb, resolvedConnectionName);
333
- const storage = TraceWriteDiagnostics.wrapStorage(TraceContentBudget.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config), config.redaction), config), {
314
+ const resolvedStorage = config.proxy.enabled
315
+ ? ProxyTraceStorage.create({
316
+ baseUrl: config.proxy.url ?? '',
317
+ path: config.proxy.path,
318
+ keyId: config.proxy.keyId ?? '',
319
+ secret: config.proxy.secret ?? '',
320
+ timeoutMs: config.proxy.timeoutMs,
321
+ })
322
+ : TraceStorage.resolveStorage(storageDb);
323
+ const storage = TraceWriteDiagnostics.wrapStorage(TraceContentBudget.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceServiceTag.wrapStorage(resolvedStorage, config), config), config.redaction), config), {
334
324
  connectionName: resolvedConnectionName,
335
325
  logger: core.Logger,
336
326
  });
@@ -0,0 +1,12 @@
1
+ import type { ITraceStorage } from '../types';
2
+ type ProxyTraceStorageSettings = {
3
+ baseUrl: string;
4
+ path: string;
5
+ keyId: string;
6
+ secret: string;
7
+ timeoutMs: number;
8
+ };
9
+ export declare const ProxyTraceStorage: Readonly<{
10
+ create(settings: ProxyTraceStorageSettings): ITraceStorage;
11
+ }>;
12
+ export default ProxyTraceStorage;
@@ -0,0 +1,102 @@
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 createUnsupportedReadError = () => ErrorFactory.createConfigError('Trace proxy sender storage does not expose dashboard/query operations. Use the trace server for reads.');
17
+ const buildSettings = (settings) => {
18
+ ensureConfigured(settings);
19
+ const normalizedPath = normalizePath(settings.path);
20
+ return {
21
+ baseUrl: settings.baseUrl,
22
+ keyId: settings.keyId,
23
+ secret: settings.secret,
24
+ timeoutMs: settings.timeoutMs,
25
+ signaturePathPrefixToStrip: new URL(settings.baseUrl).pathname,
26
+ missingUrlMessage: 'TRACE_PROXY_URL is required when TRACE_PROXY=true',
27
+ missingCredentialsMessage: 'TRACE_PROXY signing credentials are required when TRACE_PROXY=true',
28
+ messages: {
29
+ unauthorized: 'Trace proxy rejected the request credentials',
30
+ forbidden: 'Trace proxy rejected the request signature',
31
+ rateLimited: 'Trace proxy rate-limited the request',
32
+ rejected: 'Trace proxy rejected the request payload',
33
+ error: 'Trace proxy request failed',
34
+ timedOut: 'Trace proxy request timed out',
35
+ },
36
+ normalizedPath,
37
+ };
38
+ };
39
+ const appendSuffix = (path, suffix) => {
40
+ const base = normalizePath(path).replace(/\/+$/, '');
41
+ const tail = suffix.startsWith('/') ? suffix : `/${suffix}`;
42
+ return `${base}${tail}`;
43
+ };
44
+ const unsupportedQueryEntries = async () => {
45
+ throw createUnsupportedReadError();
46
+ };
47
+ const unsupportedGetEntry = async () => {
48
+ throw createUnsupportedReadError();
49
+ };
50
+ const unsupportedGetBatch = async () => {
51
+ throw createUnsupportedReadError();
52
+ };
53
+ const unsupportedQueryBatchEntries = async () => {
54
+ throw createUnsupportedReadError();
55
+ };
56
+ const unsupportedPrune = async () => {
57
+ throw createUnsupportedReadError();
58
+ };
59
+ const unsupportedClear = async () => {
60
+ throw createUnsupportedReadError();
61
+ };
62
+ const unsupportedGetMonitoring = async () => {
63
+ throw createUnsupportedReadError();
64
+ };
65
+ const unsupportedAddMonitoring = async () => {
66
+ throw createUnsupportedReadError();
67
+ };
68
+ const unsupportedRemoveMonitoring = async () => {
69
+ throw createUnsupportedReadError();
70
+ };
71
+ const unsupportedStats = async () => {
72
+ throw createUnsupportedReadError();
73
+ };
74
+ export const ProxyTraceStorage = Object.freeze({
75
+ create(settings) {
76
+ const normalized = buildSettings(settings);
77
+ return Object.freeze({
78
+ async writeEntry(entry) {
79
+ await RemoteSignedJson.request(normalized, normalized.normalizedPath, {
80
+ entry,
81
+ });
82
+ },
83
+ async updateEntry(uuid, patch) {
84
+ await RemoteSignedJson.request(normalized, appendSuffix(normalized.normalizedPath, '/update'), { uuid, patch });
85
+ },
86
+ async markFamilyStale(familyHash, exceptUuid) {
87
+ await RemoteSignedJson.request(normalized, appendSuffix(normalized.normalizedPath, '/mark-family-stale'), { familyHash, exceptUuid });
88
+ },
89
+ queryEntries: unsupportedQueryEntries,
90
+ getEntry: unsupportedGetEntry,
91
+ getBatch: unsupportedGetBatch,
92
+ queryBatchEntries: unsupportedQueryBatchEntries,
93
+ prune: unsupportedPrune,
94
+ clear: unsupportedClear,
95
+ getMonitoring: unsupportedGetMonitoring,
96
+ addMonitoring: unsupportedAddMonitoring,
97
+ removeMonitoring: unsupportedRemoveMonitoring,
98
+ stats: unsupportedStats,
99
+ });
100
+ },
101
+ });
102
+ 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.2",
3
+ "version": "0.9.4",
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.4"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
@@ -56,4 +56,4 @@
56
56
  "build": "tsc -p tsconfig.json && tsc -p tsconfig.migrations.json && node ../../scripts/fix-dist-esm-imports.mjs dist",
57
57
  "prepublishOnly": "npm run build"
58
58
  }
59
- }
59
+ }