stitchkit 0.10.0 → 0.12.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/dist/cli.js +2 -2
- package/dist/{index-f39j6twc.js → index-eyc38rgc.js} +1 -1
- package/dist/{index-jgpsd7dy.js → index-fq89tdex.js} +8 -1
- package/dist/index-fwqnkc90.js +151 -0
- package/dist/{index-s8gtzt8w.js → index-kb7wxdq0.js} +15 -12
- package/dist/{index-9zrq8x5z.js → index-msmy8ydw.js} +1 -1
- package/dist/internal/errors.d.ts +7 -0
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/node.js +3 -3
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/context.d.ts +39 -6
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/event.d.ts +26 -4
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +21 -6
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/index.js +5 -7
- package/dist/tools.js +3 -3
- package/llms-full.txt +38 -13
- package/package.json +1 -1
- package/dist/index-031q8xmx.js +0 -87
- package/dist/index-p9m9c0jw.js +0 -58
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
emitResult,
|
|
5
5
|
parseCliArgs,
|
|
6
6
|
pollUntilDone
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-eyc38rgc.js";
|
|
8
8
|
import"./index-0ed3bx43.js";
|
|
9
|
-
import"./index-
|
|
9
|
+
import"./index-fq89tdex.js";
|
|
10
10
|
import"./index-tm7dqzxc.js";
|
|
11
11
|
export {
|
|
12
12
|
pollUntilDone,
|
|
@@ -76,6 +76,13 @@ function formatZodError(error) {
|
|
|
76
76
|
return lines.join(`
|
|
77
77
|
`) + suffix;
|
|
78
78
|
}
|
|
79
|
+
function errorCode(err) {
|
|
80
|
+
if (AppError.is(err))
|
|
81
|
+
return err.code;
|
|
82
|
+
if (err instanceof z2.ZodError)
|
|
83
|
+
return "VALIDATION_ERROR";
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
79
86
|
function normalizeError(err) {
|
|
80
87
|
if (AppError.is(err))
|
|
81
88
|
return err;
|
|
@@ -102,4 +109,4 @@ function isWithinDir(root, target) {
|
|
|
102
109
|
return target === root || target === base || target.startsWith(base + sep);
|
|
103
110
|
}
|
|
104
111
|
|
|
105
|
-
export { AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, formatZodError, normalizeError, validateHandlerOutput, isWithinDir };
|
|
112
|
+
export { AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, formatZodError, errorCode, normalizeError, validateHandlerOutput, isWithinDir };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isRecord,
|
|
3
|
+
isUnsafeKey
|
|
4
|
+
} from "./index-tm7dqzxc.js";
|
|
5
|
+
|
|
6
|
+
// src/server/request.ts
|
|
7
|
+
function generateTraceId() {
|
|
8
|
+
return `${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
9
|
+
}
|
|
10
|
+
function resolveTraceId(req) {
|
|
11
|
+
const header = req.headers.get("x-request-id") ?? req.headers.get("x-trace-id");
|
|
12
|
+
const trimmed = header?.trim();
|
|
13
|
+
if (trimmed && trimmed.length <= 128 && /^[\w.-]+$/.test(trimmed)) {
|
|
14
|
+
return trimmed;
|
|
15
|
+
}
|
|
16
|
+
return generateTraceId();
|
|
17
|
+
}
|
|
18
|
+
function resolveSocketIp(req, server) {
|
|
19
|
+
if (typeof server === "object" && server !== null && "requestIP" in server && typeof server.requestIP === "function") {
|
|
20
|
+
const addr = server.requestIP(req);
|
|
21
|
+
if (isRecord(addr) && typeof addr.address === "string" && addr.address) {
|
|
22
|
+
return addr.address;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if ("ip" in req && typeof req.ip === "string" && req.ip)
|
|
26
|
+
return req.ip;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
function extractIp(req, options = {}) {
|
|
30
|
+
if (options.trustProxy) {
|
|
31
|
+
const forwarded = req.headers.get("x-forwarded-for");
|
|
32
|
+
if (forwarded)
|
|
33
|
+
return (forwarded.split(",")[0] ?? "").trim().replace(/^::ffff:/, "");
|
|
34
|
+
const realIp = req.headers.get("x-real-ip");
|
|
35
|
+
if (realIp)
|
|
36
|
+
return realIp.trim().replace(/^::ffff:/, "");
|
|
37
|
+
}
|
|
38
|
+
return (options.socketIp ?? "").replace(/^::ffff:/, "");
|
|
39
|
+
}
|
|
40
|
+
function getClientInfo(req, options = {}) {
|
|
41
|
+
return {
|
|
42
|
+
ipAddress: extractIp(req, options) || undefined,
|
|
43
|
+
userAgent: req.headers.get("user-agent") ?? undefined
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function parseQueryParams(url) {
|
|
47
|
+
const query = {};
|
|
48
|
+
for (const key of new Set(url.searchParams.keys())) {
|
|
49
|
+
if (isUnsafeKey(key))
|
|
50
|
+
continue;
|
|
51
|
+
const values = url.searchParams.getAll(key);
|
|
52
|
+
const [first] = values;
|
|
53
|
+
query[key] = values.length === 1 && first !== undefined ? first : values;
|
|
54
|
+
}
|
|
55
|
+
return query;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/observability/trace.ts
|
|
59
|
+
var TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/i;
|
|
60
|
+
function randomHex(bytes) {
|
|
61
|
+
const arr = new Uint8Array(bytes);
|
|
62
|
+
crypto.getRandomValues(arr);
|
|
63
|
+
let hex = "";
|
|
64
|
+
for (const byte of arr)
|
|
65
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
66
|
+
return hex;
|
|
67
|
+
}
|
|
68
|
+
function createTraceContext() {
|
|
69
|
+
return { traceId: randomHex(16), spanId: randomHex(8) };
|
|
70
|
+
}
|
|
71
|
+
function parseTraceparent(header) {
|
|
72
|
+
if (!header)
|
|
73
|
+
return null;
|
|
74
|
+
const match = TRACEPARENT_RE.exec(header.trim());
|
|
75
|
+
if (!match?.[1] || !match[2])
|
|
76
|
+
return null;
|
|
77
|
+
const traceId = match[1].toLowerCase();
|
|
78
|
+
const parentSpanId = match[2].toLowerCase();
|
|
79
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(parentSpanId))
|
|
80
|
+
return null;
|
|
81
|
+
return { traceId, spanId: randomHex(8), parentSpanId };
|
|
82
|
+
}
|
|
83
|
+
function formatTraceparent(ctx) {
|
|
84
|
+
return `00-${ctx.traceId}-${ctx.spanId}-01`;
|
|
85
|
+
}
|
|
86
|
+
function resolveTraceContext(req) {
|
|
87
|
+
return parseTraceparent(req.headers.get("traceparent")) ?? createTraceContext();
|
|
88
|
+
}
|
|
89
|
+
function childSpan(parent) {
|
|
90
|
+
return {
|
|
91
|
+
traceId: parent.traceId,
|
|
92
|
+
spanId: randomHex(8),
|
|
93
|
+
parentSpanId: parent.spanId
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/observability/context.ts
|
|
98
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
99
|
+
var storage = new AsyncLocalStorage;
|
|
100
|
+
function runWithRequestContext(ctx, fn) {
|
|
101
|
+
return storage.run(ctx, fn);
|
|
102
|
+
}
|
|
103
|
+
function getRequestContext() {
|
|
104
|
+
return storage.getStore();
|
|
105
|
+
}
|
|
106
|
+
function getTraceId() {
|
|
107
|
+
return storage.getStore()?.trace.traceId;
|
|
108
|
+
}
|
|
109
|
+
function getUserId() {
|
|
110
|
+
return storage.getStore()?.userId;
|
|
111
|
+
}
|
|
112
|
+
function setRequestUser(userId) {
|
|
113
|
+
const ctx = storage.getStore();
|
|
114
|
+
if (ctx)
|
|
115
|
+
ctx.userId = userId;
|
|
116
|
+
}
|
|
117
|
+
function setRequestEndpoint(serviceName, action) {
|
|
118
|
+
const ctx = storage.getStore();
|
|
119
|
+
if (ctx) {
|
|
120
|
+
ctx.serviceName = serviceName;
|
|
121
|
+
ctx.action = action;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function setRequestDimensions(dimensions) {
|
|
125
|
+
const ctx = storage.getStore();
|
|
126
|
+
if (ctx)
|
|
127
|
+
ctx.dimensions = { ...ctx.dimensions, ...dimensions };
|
|
128
|
+
}
|
|
129
|
+
function setRequestError(error) {
|
|
130
|
+
const ctx = storage.getStore();
|
|
131
|
+
if (ctx)
|
|
132
|
+
ctx.error = error;
|
|
133
|
+
}
|
|
134
|
+
function wrapInRequestContext(handler, options = {}) {
|
|
135
|
+
return (req, server) => {
|
|
136
|
+
const ctx = {
|
|
137
|
+
trace: resolveTraceContext(req),
|
|
138
|
+
source: "http",
|
|
139
|
+
method: req.method,
|
|
140
|
+
path: new URL(req.url, "http://localhost").pathname,
|
|
141
|
+
startedAt: process.hrtime.bigint(),
|
|
142
|
+
...getClientInfo(req, {
|
|
143
|
+
trustProxy: options.trustProxy,
|
|
144
|
+
socketIp: resolveSocketIp(req, server)
|
|
145
|
+
})
|
|
146
|
+
};
|
|
147
|
+
return runWithRequestContext(ctx, () => handler(req, server));
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { generateTraceId, resolveTraceId, resolveSocketIp, extractIp, getClientInfo, parseQueryParams, createTraceContext, parseTraceparent, formatTraceparent, resolveTraceContext, childSpan, runWithRequestContext, getRequestContext, getTraceId, getUserId, setRequestUser, setRequestEndpoint, setRequestDimensions, setRequestError, wrapInRequestContext };
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AppError,
|
|
3
3
|
badRequest,
|
|
4
|
+
errorCode,
|
|
4
5
|
isWithinDir,
|
|
5
6
|
normalizeError,
|
|
6
7
|
validateHandlerOutput
|
|
7
|
-
} from "./index-
|
|
8
|
+
} from "./index-fq89tdex.js";
|
|
8
9
|
import {
|
|
9
10
|
extractIp,
|
|
10
11
|
getClientInfo,
|
|
11
12
|
parseQueryParams,
|
|
12
13
|
resolveSocketIp,
|
|
13
|
-
resolveTraceId
|
|
14
|
-
|
|
14
|
+
resolveTraceId,
|
|
15
|
+
setRequestEndpoint
|
|
16
|
+
} from "./index-fwqnkc90.js";
|
|
15
17
|
import {
|
|
16
18
|
__require,
|
|
17
19
|
isUnsafeKey,
|
|
@@ -445,8 +447,8 @@ function levelForStatus(status) {
|
|
|
445
447
|
return "warn";
|
|
446
448
|
return "info";
|
|
447
449
|
}
|
|
448
|
-
function buildLogFields(method, path, status, durationMs, traceId,
|
|
449
|
-
return { traceId, method, path, status, durationMs, ...
|
|
450
|
+
function buildLogFields(method, path, status, durationMs, traceId, errorCode2) {
|
|
451
|
+
return { traceId, method, path, status, durationMs, ...errorCode2 && { errorCode: errorCode2 } };
|
|
450
452
|
}
|
|
451
453
|
function formatMs(ms) {
|
|
452
454
|
if (ms >= 1000)
|
|
@@ -490,20 +492,20 @@ function logIncoming(req, pathname, traceId, ipAddress) {
|
|
|
490
492
|
}
|
|
491
493
|
return log;
|
|
492
494
|
}
|
|
493
|
-
function logOutgoing(req, pathname, status, log, ipAddress,
|
|
495
|
+
function logOutgoing(req, pathname, status, log, ipAddress, errorCode2) {
|
|
494
496
|
const ms = elapsedMs(log.startTime);
|
|
495
497
|
if (isProd) {
|
|
496
498
|
console.log(JSON.stringify({
|
|
497
499
|
ts: new Date().toISOString(),
|
|
498
500
|
level: levelForStatus(status),
|
|
499
501
|
msg: `${req.method} ${pathname} ${status}`,
|
|
500
|
-
...buildLogFields(req.method, pathname, status, Math.round(ms), log.traceId,
|
|
502
|
+
...buildLogFields(req.method, pathname, status, Math.round(ms), log.traceId, errorCode2),
|
|
501
503
|
ip: ipAddress
|
|
502
504
|
}));
|
|
503
505
|
return;
|
|
504
506
|
}
|
|
505
507
|
const mc = METHOD_COLOR[req.method] ?? c.dim;
|
|
506
|
-
const code =
|
|
508
|
+
const code = errorCode2 ? ` ${c.red}${errorCode2}${c.reset}` : "";
|
|
507
509
|
console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${log.traceId}${c.reset} ${c.cyan}←${c.reset} ${safePath(pathname)} ${statusColor(status)}${status}${c.reset}${code} ${durationColor(ms)}${formatMs(ms)}${c.reset} ${ipLabel(ipAddress ?? "")}`);
|
|
508
510
|
}
|
|
509
511
|
|
|
@@ -533,15 +535,15 @@ function createHandler(config) {
|
|
|
533
535
|
});
|
|
534
536
|
reqLog = { traceId, startTime: performance.now() };
|
|
535
537
|
}
|
|
536
|
-
const logDone = (status,
|
|
538
|
+
const logDone = (status, errorCode2) => {
|
|
537
539
|
if (!reqLog)
|
|
538
540
|
return;
|
|
539
541
|
if (useDefaultLog)
|
|
540
|
-
logOutgoing(req, url.pathname, status, reqLog, ipAddress,
|
|
542
|
+
logOutgoing(req, url.pathname, status, reqLog, ipAddress, errorCode2);
|
|
541
543
|
if (customLogger) {
|
|
542
544
|
const durationMs = Math.round(elapsedMs(reqLog.startTime));
|
|
543
545
|
const level = levelForStatus(status);
|
|
544
|
-
customLogger[level](`${req.method} ${url.pathname} ${status}${
|
|
546
|
+
customLogger[level](`${req.method} ${url.pathname} ${status}${errorCode2 ? ` ${errorCode2}` : ""} ${durationMs}ms`, buildLogFields(req.method, url.pathname, status, durationMs, reqLog.traceId, errorCode2));
|
|
545
547
|
}
|
|
546
548
|
};
|
|
547
549
|
const respondError = async (err, errCtx, endpoint) => {
|
|
@@ -550,7 +552,7 @@ function createHandler(config) {
|
|
|
550
552
|
const response = await hooks.onError(errCtx ?? buildErrorContext(req, url, traceId, clientIp), err, endpoint);
|
|
551
553
|
if (response instanceof Response) {
|
|
552
554
|
const withCors = applyCors(response, cors, req);
|
|
553
|
-
logDone(withCors.status);
|
|
555
|
+
logDone(withCors.status, errorCode(err));
|
|
554
556
|
return withCors;
|
|
555
557
|
}
|
|
556
558
|
} catch {}
|
|
@@ -600,6 +602,7 @@ function createHandler(config) {
|
|
|
600
602
|
}
|
|
601
603
|
const { method, pathParams, groupHooks } = match;
|
|
602
604
|
const ctx = buildBaseContext(req, url, pathParams, traceId, clientIp);
|
|
605
|
+
setRequestEndpoint(method.serviceName, method.key);
|
|
603
606
|
try {
|
|
604
607
|
await parseRequestInto(ctx, req, url, method, config.maxUploadBytes);
|
|
605
608
|
if (hooks?.beforeHandle) {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type ZodType, z } from 'zod';
|
|
2
2
|
import { AppError } from '../contract';
|
|
3
3
|
export declare function formatZodError(error: z.ZodError): string;
|
|
4
|
+
/**
|
|
5
|
+
* The stable error code for a thrown value — `AppError.code`, `VALIDATION_ERROR`
|
|
6
|
+
* for a `ZodError`, else `undefined`. Side-effect-free (unlike `normalizeError`,
|
|
7
|
+
* it never logs): for access-log attribution on a path where the response is
|
|
8
|
+
* produced elsewhere — a custom `onError` hook that returns its own `Response`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function errorCode(err: unknown): string | undefined;
|
|
4
11
|
export declare function normalizeError(err: unknown): AppError;
|
|
5
12
|
/**
|
|
6
13
|
* Validate a handler's return value against the contract `output` schema. A
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CASxD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAYrD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,OAAO,GACZ;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAO9D"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CASxD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAI1D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAYrD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,OAAO,GACZ;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAO9D"}
|
package/dist/node.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
createImplement,
|
|
4
4
|
createSocketIOServer,
|
|
5
5
|
implement
|
|
6
|
-
} from "./index-
|
|
6
|
+
} from "./index-kb7wxdq0.js";
|
|
7
7
|
import {
|
|
8
8
|
AppError,
|
|
9
9
|
appError,
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
notFound,
|
|
14
14
|
rateLimited,
|
|
15
15
|
unauthorized
|
|
16
|
-
} from "./index-
|
|
17
|
-
import"./index-
|
|
16
|
+
} from "./index-fq89tdex.js";
|
|
17
|
+
import"./index-fwqnkc90.js";
|
|
18
18
|
import"./index-tm7dqzxc.js";
|
|
19
19
|
// src/server/node.ts
|
|
20
20
|
import { serve } from "srvx";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/observability/audit.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,kBAAkB,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAe,KAAK,eAAe,EAAmB,MAAM,YAAY,CAAC;AAGhF,oCAAoC;AACpC,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,IAAI,EAAE,CAAC,CAAC,EACN,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,KACpD,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpD;;;OAGG;IACH,QAAQ,EAAE,aAAa,CAAC;CACzB;AAmBD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/observability/audit.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,kBAAkB,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAe,KAAK,eAAe,EAAmB,MAAM,YAAY,CAAC;AAGhF,oCAAoC;AACpC,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,IAAI,EAAE,CAAC,CAAC,EACN,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,KACpD,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpD;;;OAGG;IACH,QAAQ,EAAE,aAAa,CAAC;CACzB;AAmBD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CA6G9D"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { TransportSource } from '../contract';
|
|
2
|
-
import type { JsonValue } from './sanitize';
|
|
3
2
|
import { type TraceContext } from './trace';
|
|
4
3
|
/** Everything known about the request in flight. */
|
|
5
4
|
export interface RequestContext {
|
|
@@ -19,11 +18,26 @@ export interface RequestContext {
|
|
|
19
18
|
userAgent?: string;
|
|
20
19
|
/** Resolved user id — set late, once auth has run. */
|
|
21
20
|
userId?: string;
|
|
22
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Stable endpoint identity — `(serviceName, action)` of the matched contract
|
|
23
|
+
* route. Written by the HTTP pipeline when the route matches, *before*
|
|
24
|
+
* validation, so even a failed request is attributed to the operation it
|
|
25
|
+
* targeted. → ADR 0022.
|
|
26
|
+
*/
|
|
27
|
+
serviceName?: string;
|
|
28
|
+
action?: string;
|
|
29
|
+
/**
|
|
30
|
+
* App-defined domain dimensions (a tenant / project / entity id, …) — an
|
|
31
|
+
* opaque bag the core attaches no meaning to (→ ADR 0021), surfaced on
|
|
32
|
+
* `RequestEvent.dimensions`. Set via `setRequestDimensions`.
|
|
33
|
+
*/
|
|
34
|
+
dimensions?: Record<string, string>;
|
|
35
|
+
/** Error outcome — set late, by the error handler. `details` is sanitised into
|
|
36
|
+
* `RequestEvent.errorDetail` at emit, so it is accepted loosely here. */
|
|
23
37
|
error?: {
|
|
24
38
|
code?: string;
|
|
25
39
|
message?: string;
|
|
26
|
-
details?:
|
|
40
|
+
details?: unknown;
|
|
27
41
|
};
|
|
28
42
|
}
|
|
29
43
|
/** Run `fn` with `ctx` as the active request context. */
|
|
@@ -40,16 +54,35 @@ export declare function getUserId(): string | undefined;
|
|
|
40
54
|
* from the auth hook.
|
|
41
55
|
*/
|
|
42
56
|
export declare function setRequestUser(userId: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Attach the matched endpoint's stable `(serviceName, action)` identity to the
|
|
59
|
+
* active context. The framework's HTTP pipeline calls this when a contract route
|
|
60
|
+
* matches — *before* validation — so the audit event for a request carries the
|
|
61
|
+
* operation it targeted even when the request fails pre-handler. No-op outside a
|
|
62
|
+
* request context. → ADR 0022.
|
|
63
|
+
*/
|
|
64
|
+
export declare function setRequestEndpoint(serviceName: string, action: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Merge app-defined domain dimensions (a tenant / project / entity id, …) onto
|
|
67
|
+
* the active context — an opaque bag the core gives no meaning to (→ ADR 0021),
|
|
68
|
+
* surfaced on `RequestEvent.dimensions`. Resolve them cheaply from `ctx.params` /
|
|
69
|
+
* headers in `beforeHandle` (success) or `onError` (a pre-handler failure) and
|
|
70
|
+
* they land on the audit event for the request, success or failure alike. Merges
|
|
71
|
+
* across calls; no-op outside a request context.
|
|
72
|
+
*/
|
|
73
|
+
export declare function setRequestDimensions(dimensions: Record<string, string>): void;
|
|
43
74
|
/**
|
|
44
75
|
* Record the error outcome on the active context. Call this from the error
|
|
45
76
|
* handler — the audit hook reads it when the request completes. Optional
|
|
46
|
-
* `details` carries structure the message string flattens (e.g. the failing
|
|
47
|
-
* Zod issues)
|
|
77
|
+
* `details` carries the structure the message string flattens (e.g. the failing
|
|
78
|
+
* Zod issues, or an `AppError.details`); it is accepted as `unknown` and
|
|
79
|
+
* **sanitised** into `RequestEvent.errorDetail` at emit — pass it raw, no need to
|
|
80
|
+
* pre-launder the type.
|
|
48
81
|
*/
|
|
49
82
|
export declare function setRequestError(error: {
|
|
50
83
|
code?: string;
|
|
51
84
|
message?: string;
|
|
52
|
-
details?:
|
|
85
|
+
details?: unknown;
|
|
53
86
|
}): void;
|
|
54
87
|
/** Options for `wrapInRequestContext`. */
|
|
55
88
|
export interface WrapRequestContextOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/observability/context.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/observability/context.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AAEjE,oDAAoD;AACpD,MAAM,WAAW,cAAc;IAC7B,sCAAsC;IACtC,KAAK,EAAE,YAAY,CAAC;IACpB,4CAA4C;IAC5C,MAAM,EAAE,eAAe,CAAC;IACxB,iBAAiB;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC;8EAC0E;IAC1E,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAChE;AAID,yDAAyD;AACzD,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAE5E;AAED,sEAAsE;AACtE,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,SAAS,CAE9D;AAED,mEAAmE;AACnE,wBAAgB,UAAU,IAAI,MAAM,GAAG,SAAS,CAE/C;AAED,qDAAqD;AACrD,wBAAgB,SAAS,IAAI,MAAM,GAAG,SAAS,CAE9C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAGnD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAM5E;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAG7E;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,IAAI,CAGP;AAED,0CAA0C;AAC1C,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,EACvD,OAAO,GAAE,yBAA8B,GACtC,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiBhD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TransportSource } from '../contract';
|
|
1
|
+
import type { HttpMethod, TransportSource } from '../contract';
|
|
2
2
|
import type { JsonValue } from './sanitize';
|
|
3
3
|
/**
|
|
4
4
|
* A normalised audit event — one shape for a completed call on any surface
|
|
@@ -11,8 +11,29 @@ export interface RequestEvent {
|
|
|
11
11
|
source: TransportSource;
|
|
12
12
|
/** HTTP verb, or `TOOL` for a tool call. */
|
|
13
13
|
method: string;
|
|
14
|
+
/**
|
|
15
|
+
* The operation's contract verb (`GET` / `POST` / …). Set on **tool** events
|
|
16
|
+
* (whose `method` is `TOOL`) so a single filter can tell a read from a write
|
|
17
|
+
* across HTTP and tool calls — `(event.httpMethod ?? event.method) !== 'GET'`.
|
|
18
|
+
* Omitted on HTTP events, where `method` already is the verb. → ADR 0030.
|
|
19
|
+
*/
|
|
20
|
+
httpMethod?: HttpMethod;
|
|
14
21
|
/** Request path — `/api/...` for HTTP, `/{source}/{tool}` for a tool call. */
|
|
15
22
|
path: string;
|
|
23
|
+
/**
|
|
24
|
+
* Stable owning-contract identity of the matched operation — the "service"
|
|
25
|
+
* (contract prefix) and "action" (endpoint key) halves. Set on every surface
|
|
26
|
+
* (HTTP, MCP, agent) from the contract, not parsed from `path`. → ADR 0022.
|
|
27
|
+
*/
|
|
28
|
+
serviceName?: string;
|
|
29
|
+
action?: string;
|
|
30
|
+
/**
|
|
31
|
+
* App-defined domain dimensions for the call — e.g. a tenant / project /
|
|
32
|
+
* entity id. An opaque bag the core attaches no meaning to (→ ADR 0021);
|
|
33
|
+
* populated by `setRequestDimensions`. The sink maps it onto its own columns
|
|
34
|
+
* instead of re-deriving identity from the path.
|
|
35
|
+
*/
|
|
36
|
+
dimensions?: Record<string, string>;
|
|
16
37
|
/** Tool name — tool calls only. */
|
|
17
38
|
toolName?: string;
|
|
18
39
|
/** W3C trace id — correlates every span of one logical request. */
|
|
@@ -32,9 +53,10 @@ export interface RequestEvent {
|
|
|
32
53
|
/** Error message — failures only. */
|
|
33
54
|
errorMessage?: string;
|
|
34
55
|
/**
|
|
35
|
-
* Structured error detail — failures only,
|
|
36
|
-
* via `setRequestError({ details })` (e.g. the failing validation
|
|
37
|
-
* `errorMessage` string flattens)
|
|
56
|
+
* Structured error detail — failures only. On HTTP, what the error handler
|
|
57
|
+
* recorded via `setRequestError({ details })` (e.g. the failing validation
|
|
58
|
+
* issues the `errorMessage` string flattens); on a tool call, the failed
|
|
59
|
+
* `ToolResult.details` (sanitised).
|
|
38
60
|
*/
|
|
39
61
|
errorDetail?: JsonValue;
|
|
40
62
|
/** Sanitised request payload — the HTTP body or the tool arguments. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/observability/event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/observability/event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,mCAAmC;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kCAAkC;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,uEAAuE;IACvE,OAAO,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1B,mDAAmD;IACnD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,4CAA4C;IAC5C,aAAa,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6BAA6B;IAC7B,SAAS,EAAE,IAAI,CAAC;CACjB"}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* function — nothing else.
|
|
8
8
|
*/
|
|
9
9
|
export { type AuditConfig, type AuditHook, createAuditHook } from './audit';
|
|
10
|
-
export { getRequestContext, getTraceId, getUserId, type RequestContext, runWithRequestContext, setRequestError, setRequestUser, wrapInRequestContext, } from './context';
|
|
10
|
+
export { getRequestContext, getTraceId, getUserId, type RequestContext, runWithRequestContext, setRequestDimensions, setRequestEndpoint, setRequestError, setRequestUser, wrapInRequestContext, } from './context';
|
|
11
11
|
export type { RequestEvent } from './event';
|
|
12
12
|
export { type JsonValue, measureSize, redact, type SanitizeOptions, type SizeMeasure, sanitizePayload, truncatePreview, } from './sanitize';
|
|
13
13
|
export { childSpan, createTraceContext, formatTraceparent, parseTraceparent, resolveTraceContext, type TraceContext, } from './trace';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,SAAS,EACT,KAAK,cAAc,EACnB,qBAAqB,EACrB,eAAe,EACf,cAAc,EACd,oBAAoB,GACrB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EACL,KAAK,SAAS,EACd,WAAW,EACX,MAAM,EACN,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,SAAS,EACT,KAAK,cAAc,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,oBAAoB,GACrB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EACL,KAAK,SAAS,EACd,WAAW,EACX,MAAM,EACN,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}
|
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
parseTraceparent,
|
|
9
9
|
resolveTraceContext,
|
|
10
10
|
runWithRequestContext,
|
|
11
|
+
setRequestDimensions,
|
|
12
|
+
setRequestEndpoint,
|
|
11
13
|
setRequestError,
|
|
12
14
|
setRequestUser,
|
|
13
15
|
wrapInRequestContext
|
|
14
|
-
} from "../index-
|
|
15
|
-
import"../index-p9m9c0jw.js";
|
|
16
|
+
} from "../index-fwqnkc90.js";
|
|
16
17
|
import {
|
|
17
18
|
isRecord,
|
|
18
19
|
isUnsafeKey
|
|
@@ -140,6 +141,9 @@ function createAuditHook(config) {
|
|
|
140
141
|
source: ctx.source,
|
|
141
142
|
method: ctx.method,
|
|
142
143
|
path: ctx.path,
|
|
144
|
+
...ctx.serviceName !== undefined && { serviceName: ctx.serviceName },
|
|
145
|
+
...ctx.action !== undefined && { action: ctx.action },
|
|
146
|
+
...ctx.dimensions !== undefined && { dimensions: ctx.dimensions },
|
|
143
147
|
traceId: ctx.trace.traceId,
|
|
144
148
|
spanId: ctx.trace.spanId,
|
|
145
149
|
parentSpanId: ctx.trace.parentSpanId,
|
|
@@ -148,7 +152,9 @@ function createAuditHook(config) {
|
|
|
148
152
|
durationMs,
|
|
149
153
|
errorCode: ctx.error?.code,
|
|
150
154
|
errorMessage: ctx.error?.message,
|
|
151
|
-
...ctx.error?.details !== undefined && {
|
|
155
|
+
...ctx.error?.details !== undefined && {
|
|
156
|
+
errorDetail: sanitizePayload(ctx.error.details, sanitize)
|
|
157
|
+
},
|
|
152
158
|
payload: sanitizePayload(body, sanitize),
|
|
153
159
|
resultSize: null,
|
|
154
160
|
responseBytes: 0,
|
|
@@ -163,14 +169,18 @@ function createAuditHook(config) {
|
|
|
163
169
|
};
|
|
164
170
|
};
|
|
165
171
|
const toolCall = {
|
|
166
|
-
afterToolCall: (toolName, args, result, durationMs, context) => {
|
|
167
|
-
const
|
|
168
|
-
const span =
|
|
172
|
+
afterToolCall: (toolName, args, result, durationMs, context, endpoint) => {
|
|
173
|
+
const requestCtx = getRequestContext();
|
|
174
|
+
const span = requestCtx ? childSpan(requestCtx.trace) : createTraceContext();
|
|
169
175
|
const measure = result.ok ? measureSize(result.data) : { resultSize: null, responseBytes: 0 };
|
|
170
176
|
emit({
|
|
171
177
|
source: context.source,
|
|
172
178
|
method: "TOOL",
|
|
179
|
+
httpMethod: endpoint.method,
|
|
173
180
|
path: `/${context.source}/${toolName}`,
|
|
181
|
+
serviceName: endpoint.serviceName,
|
|
182
|
+
action: endpoint.key,
|
|
183
|
+
...requestCtx?.dimensions !== undefined && { dimensions: requestCtx.dimensions },
|
|
174
184
|
toolName,
|
|
175
185
|
traceId: span.traceId,
|
|
176
186
|
spanId: span.spanId,
|
|
@@ -180,6 +190,9 @@ function createAuditHook(config) {
|
|
|
180
190
|
durationMs,
|
|
181
191
|
errorCode: result.ok ? undefined : result.code,
|
|
182
192
|
errorMessage: result.ok ? undefined : toolErrorMessage(result),
|
|
193
|
+
...!result.ok && result.details !== undefined && {
|
|
194
|
+
errorDetail: sanitizePayload(result.details, sanitize)
|
|
195
|
+
},
|
|
183
196
|
payload: sanitizePayload(args, sanitize),
|
|
184
197
|
resultSize: measure.resultSize,
|
|
185
198
|
responseBytes: measure.responseBytes,
|
|
@@ -199,6 +212,8 @@ export {
|
|
|
199
212
|
truncatePreview,
|
|
200
213
|
setRequestUser,
|
|
201
214
|
setRequestError,
|
|
215
|
+
setRequestEndpoint,
|
|
216
|
+
setRequestDimensions,
|
|
202
217
|
sanitizePayload,
|
|
203
218
|
runWithRequestContext,
|
|
204
219
|
resolveTraceContext,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAEV,eAAe,EACf,aAAa,EAGd,MAAM,SAAS,CAAC;AAEjB,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAyNxF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,uBAsBnD"}
|
package/dist/server/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
socketIoLane,
|
|
13
13
|
staticRoute,
|
|
14
14
|
webSocketLane
|
|
15
|
-
} from "../index-
|
|
15
|
+
} from "../index-kb7wxdq0.js";
|
|
16
16
|
import {
|
|
17
17
|
createAuthHook,
|
|
18
18
|
createBearerResolver,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
signJwt,
|
|
26
26
|
verifyJwt,
|
|
27
27
|
verifyPkce
|
|
28
|
-
} from "../index-
|
|
28
|
+
} from "../index-msmy8ydw.js";
|
|
29
29
|
import {
|
|
30
30
|
jsonSchemaFields,
|
|
31
31
|
toJsonSchema
|
|
@@ -42,17 +42,15 @@ import {
|
|
|
42
42
|
notFound,
|
|
43
43
|
rateLimited,
|
|
44
44
|
unauthorized
|
|
45
|
-
} from "../index-
|
|
46
|
-
import {
|
|
47
|
-
getTraceId
|
|
48
|
-
} from "../index-031q8xmx.js";
|
|
45
|
+
} from "../index-fq89tdex.js";
|
|
49
46
|
import {
|
|
50
47
|
extractIp,
|
|
51
48
|
generateTraceId,
|
|
52
49
|
getClientInfo,
|
|
50
|
+
getTraceId,
|
|
53
51
|
resolveSocketIp,
|
|
54
52
|
resolveTraceId
|
|
55
|
-
} from "../index-
|
|
53
|
+
} from "../index-fwqnkc90.js";
|
|
56
54
|
import {
|
|
57
55
|
isRecord
|
|
58
56
|
} from "../index-tm7dqzxc.js";
|
package/dist/tools.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
inputIsQuery,
|
|
3
3
|
signJwt,
|
|
4
4
|
verifyPkce
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-msmy8ydw.js";
|
|
6
6
|
import {
|
|
7
7
|
coerceJsonArgs,
|
|
8
8
|
collectTools,
|
|
@@ -14,14 +14,14 @@ import {
|
|
|
14
14
|
pollUntil,
|
|
15
15
|
readCapped,
|
|
16
16
|
toolResultFromError
|
|
17
|
-
} from "./index-
|
|
17
|
+
} from "./index-eyc38rgc.js";
|
|
18
18
|
import {
|
|
19
19
|
toJsonSchema
|
|
20
20
|
} from "./index-0ed3bx43.js";
|
|
21
21
|
import {
|
|
22
22
|
AppError,
|
|
23
23
|
isWithinDir
|
|
24
|
-
} from "./index-
|
|
24
|
+
} from "./index-fq89tdex.js";
|
|
25
25
|
import {
|
|
26
26
|
isRecord,
|
|
27
27
|
typedEntries
|
package/llms-full.txt
CHANGED
|
@@ -2256,11 +2256,14 @@ queryable across all three:
|
|
|
2256
2256
|
|-------|-------|
|
|
2257
2257
|
| `source` | `http` \| `mcp` \| `agent` |
|
|
2258
2258
|
| `method` / `path` | the verb + path, or `TOOL` + `/{source}/{tool}` |
|
|
2259
|
+
| `serviceName` / `action` | stable contract identity of the operation (→ ADR 0022) — from the contract, not parsed from `path`; set on every surface, present even on a pre-handler 400 |
|
|
2259
2260
|
| `toolName` | tool calls only |
|
|
2261
|
+
| `httpMethod` | the contract verb on **tool** events (their `method` is `TOOL`) — filter reads vs writes across both surfaces with `(event.httpMethod ?? event.method) !== 'GET'` |
|
|
2262
|
+
| `dimensions` | app-defined domain dimensions (tenant / project / entity id) — see [request context](#request-context) |
|
|
2260
2263
|
| `traceId` / `spanId` / `parentSpanId` | [W3C trace context](#trace-context) |
|
|
2261
2264
|
| `ok` / `statusCode` | outcome — real HTTP status, or `200`/`400` for a tool |
|
|
2262
2265
|
| `durationMs` / `startedAt` | timing |
|
|
2263
|
-
| `errorCode` / `errorMessage` | failures only |
|
|
2266
|
+
| `errorCode` / `errorMessage` / `errorDetail` | failures only — `errorDetail` carries the structure the message flattens (e.g. Zod issues) |
|
|
2264
2267
|
| `payload` | the request body / tool arguments — sanitised |
|
|
2265
2268
|
| `resultSize` / `responseBytes` | result item count + serialised size |
|
|
2266
2269
|
| `userId` / `ipAddress` / `userAgent` | identity |
|
|
@@ -2279,15 +2282,36 @@ Bun.serve({
|
|
|
2279
2282
|
})
|
|
2280
2283
|
```
|
|
2281
2284
|
|
|
2282
|
-
|
|
2283
|
-
them from the hooks that know:
|
|
2285
|
+
Some fields are filled in late. Set them from the hooks that know:
|
|
2284
2286
|
|
|
2285
2287
|
```ts
|
|
2286
|
-
import {
|
|
2288
|
+
import {
|
|
2289
|
+
setRequestDimensions,
|
|
2290
|
+
setRequestError,
|
|
2291
|
+
setRequestUser,
|
|
2292
|
+
} from 'stitchkit/observability'
|
|
2287
2293
|
|
|
2288
2294
|
createAuthHook({ /* … */ inject: (ctx, user) => user && setRequestUser(user.id) })
|
|
2289
2295
|
// in your onError hook:
|
|
2290
|
-
setRequestError({ code: err.code, message: err.message })
|
|
2296
|
+
setRequestError({ code: err.code, message: err.message, details: err.issues })
|
|
2297
|
+
```
|
|
2298
|
+
|
|
2299
|
+
**Endpoint identity is automatic.** The framework writes the matched operation's
|
|
2300
|
+
`(serviceName, action)` into the context at route-match, *before* validation — so
|
|
2301
|
+
`event.serviceName` / `event.action` are present on every event, including a
|
|
2302
|
+
pre-handler 400. Nothing to wire.
|
|
2303
|
+
|
|
2304
|
+
**Domain dimensions** — attach your own tenant / project / entity id with
|
|
2305
|
+
`setRequestDimensions`. It is an opaque `Record<string, string>` the core gives no
|
|
2306
|
+
meaning to (→ ADR 0021). Resolve it cheaply from `ctx.params` / headers in
|
|
2307
|
+
`beforeHandle` (success) or `onError` (a pre-handler failure — `ctx.params` /
|
|
2308
|
+
`ctx.req` are available there) and it lands on `event.dimensions` for the request
|
|
2309
|
+
either way, so your sink reads it as a column instead of re-parsing the path:
|
|
2310
|
+
|
|
2311
|
+
```ts
|
|
2312
|
+
// beforeHandle (success) and onError (failure) alike:
|
|
2313
|
+
const projectId = ctx.req?.headers.get('x-project') ?? String(ctx.params?.projectId ?? '')
|
|
2314
|
+
if (projectId) setRequestDimensions({ projectId })
|
|
2291
2315
|
```
|
|
2292
2316
|
|
|
2293
2317
|
Make the framework router share this trace id — so request logs and your
|
|
@@ -2378,14 +2402,15 @@ outcome and the duration, neither of which exists before the handler runs.
|
|
|
2378
2402
|
|
|
2379
2403
|
### Keying a row on (service, action)
|
|
2380
2404
|
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
`
|
|
2386
|
-
`
|
|
2387
|
-
|
|
2388
|
-
|
|
2405
|
+
`createAuditHook` already keys every event by **service** and **action**
|
|
2406
|
+
(`event.serviceName` / `event.action`, → ADR 0029) — reach for the raw hook only
|
|
2407
|
+
when you also need the handler **output**, which the audit wrapper never sees. For
|
|
2408
|
+
that, read the endpoint identity off the `MethodDef` the hook receives —
|
|
2409
|
+
`endpoint.serviceName` (the contract prefix) and `endpoint.key` (the endpoint key,
|
|
2410
|
+
e.g. `updatePartial`). They are stable and always present (→ ADR 0022); the action
|
|
2411
|
+
is not in the URL and `toolName` is absent on HTTP-only endpoints, so this is the
|
|
2412
|
+
only reliable pair. `afterHandle` also gives you the handler `result` — so it is
|
|
2413
|
+
the home for a rich mutation audit that records output:
|
|
2389
2414
|
|
|
2390
2415
|
```ts
|
|
2391
2416
|
hooks: {
|
package/package.json
CHANGED
package/dist/index-031q8xmx.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getClientInfo,
|
|
3
|
-
resolveSocketIp
|
|
4
|
-
} from "./index-p9m9c0jw.js";
|
|
5
|
-
|
|
6
|
-
// src/observability/trace.ts
|
|
7
|
-
var TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/i;
|
|
8
|
-
function randomHex(bytes) {
|
|
9
|
-
const arr = new Uint8Array(bytes);
|
|
10
|
-
crypto.getRandomValues(arr);
|
|
11
|
-
let hex = "";
|
|
12
|
-
for (const byte of arr)
|
|
13
|
-
hex += byte.toString(16).padStart(2, "0");
|
|
14
|
-
return hex;
|
|
15
|
-
}
|
|
16
|
-
function createTraceContext() {
|
|
17
|
-
return { traceId: randomHex(16), spanId: randomHex(8) };
|
|
18
|
-
}
|
|
19
|
-
function parseTraceparent(header) {
|
|
20
|
-
if (!header)
|
|
21
|
-
return null;
|
|
22
|
-
const match = TRACEPARENT_RE.exec(header.trim());
|
|
23
|
-
if (!match?.[1] || !match[2])
|
|
24
|
-
return null;
|
|
25
|
-
const traceId = match[1].toLowerCase();
|
|
26
|
-
const parentSpanId = match[2].toLowerCase();
|
|
27
|
-
if (/^0+$/.test(traceId) || /^0+$/.test(parentSpanId))
|
|
28
|
-
return null;
|
|
29
|
-
return { traceId, spanId: randomHex(8), parentSpanId };
|
|
30
|
-
}
|
|
31
|
-
function formatTraceparent(ctx) {
|
|
32
|
-
return `00-${ctx.traceId}-${ctx.spanId}-01`;
|
|
33
|
-
}
|
|
34
|
-
function resolveTraceContext(req) {
|
|
35
|
-
return parseTraceparent(req.headers.get("traceparent")) ?? createTraceContext();
|
|
36
|
-
}
|
|
37
|
-
function childSpan(parent) {
|
|
38
|
-
return {
|
|
39
|
-
traceId: parent.traceId,
|
|
40
|
-
spanId: randomHex(8),
|
|
41
|
-
parentSpanId: parent.spanId
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// src/observability/context.ts
|
|
46
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
47
|
-
var storage = new AsyncLocalStorage;
|
|
48
|
-
function runWithRequestContext(ctx, fn) {
|
|
49
|
-
return storage.run(ctx, fn);
|
|
50
|
-
}
|
|
51
|
-
function getRequestContext() {
|
|
52
|
-
return storage.getStore();
|
|
53
|
-
}
|
|
54
|
-
function getTraceId() {
|
|
55
|
-
return storage.getStore()?.trace.traceId;
|
|
56
|
-
}
|
|
57
|
-
function getUserId() {
|
|
58
|
-
return storage.getStore()?.userId;
|
|
59
|
-
}
|
|
60
|
-
function setRequestUser(userId) {
|
|
61
|
-
const ctx = storage.getStore();
|
|
62
|
-
if (ctx)
|
|
63
|
-
ctx.userId = userId;
|
|
64
|
-
}
|
|
65
|
-
function setRequestError(error) {
|
|
66
|
-
const ctx = storage.getStore();
|
|
67
|
-
if (ctx)
|
|
68
|
-
ctx.error = error;
|
|
69
|
-
}
|
|
70
|
-
function wrapInRequestContext(handler, options = {}) {
|
|
71
|
-
return (req, server) => {
|
|
72
|
-
const ctx = {
|
|
73
|
-
trace: resolveTraceContext(req),
|
|
74
|
-
source: "http",
|
|
75
|
-
method: req.method,
|
|
76
|
-
path: new URL(req.url, "http://localhost").pathname,
|
|
77
|
-
startedAt: process.hrtime.bigint(),
|
|
78
|
-
...getClientInfo(req, {
|
|
79
|
-
trustProxy: options.trustProxy,
|
|
80
|
-
socketIp: resolveSocketIp(req, server)
|
|
81
|
-
})
|
|
82
|
-
};
|
|
83
|
-
return runWithRequestContext(ctx, () => handler(req, server));
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export { createTraceContext, parseTraceparent, formatTraceparent, resolveTraceContext, childSpan, runWithRequestContext, getRequestContext, getTraceId, getUserId, setRequestUser, setRequestError, wrapInRequestContext };
|
package/dist/index-p9m9c0jw.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isRecord,
|
|
3
|
-
isUnsafeKey
|
|
4
|
-
} from "./index-tm7dqzxc.js";
|
|
5
|
-
|
|
6
|
-
// src/server/request.ts
|
|
7
|
-
function generateTraceId() {
|
|
8
|
-
return `${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
9
|
-
}
|
|
10
|
-
function resolveTraceId(req) {
|
|
11
|
-
const header = req.headers.get("x-request-id") ?? req.headers.get("x-trace-id");
|
|
12
|
-
const trimmed = header?.trim();
|
|
13
|
-
if (trimmed && trimmed.length <= 128 && /^[\w.-]+$/.test(trimmed)) {
|
|
14
|
-
return trimmed;
|
|
15
|
-
}
|
|
16
|
-
return generateTraceId();
|
|
17
|
-
}
|
|
18
|
-
function resolveSocketIp(req, server) {
|
|
19
|
-
if (typeof server === "object" && server !== null && "requestIP" in server && typeof server.requestIP === "function") {
|
|
20
|
-
const addr = server.requestIP(req);
|
|
21
|
-
if (isRecord(addr) && typeof addr.address === "string" && addr.address) {
|
|
22
|
-
return addr.address;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
if ("ip" in req && typeof req.ip === "string" && req.ip)
|
|
26
|
-
return req.ip;
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
function extractIp(req, options = {}) {
|
|
30
|
-
if (options.trustProxy) {
|
|
31
|
-
const forwarded = req.headers.get("x-forwarded-for");
|
|
32
|
-
if (forwarded)
|
|
33
|
-
return (forwarded.split(",")[0] ?? "").trim().replace(/^::ffff:/, "");
|
|
34
|
-
const realIp = req.headers.get("x-real-ip");
|
|
35
|
-
if (realIp)
|
|
36
|
-
return realIp.trim().replace(/^::ffff:/, "");
|
|
37
|
-
}
|
|
38
|
-
return (options.socketIp ?? "").replace(/^::ffff:/, "");
|
|
39
|
-
}
|
|
40
|
-
function getClientInfo(req, options = {}) {
|
|
41
|
-
return {
|
|
42
|
-
ipAddress: extractIp(req, options) || undefined,
|
|
43
|
-
userAgent: req.headers.get("user-agent") ?? undefined
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function parseQueryParams(url) {
|
|
47
|
-
const query = {};
|
|
48
|
-
for (const key of new Set(url.searchParams.keys())) {
|
|
49
|
-
if (isUnsafeKey(key))
|
|
50
|
-
continue;
|
|
51
|
-
const values = url.searchParams.getAll(key);
|
|
52
|
-
const [first] = values;
|
|
53
|
-
query[key] = values.length === 1 && first !== undefined ? first : values;
|
|
54
|
-
}
|
|
55
|
-
return query;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export { generateTraceId, resolveTraceId, resolveSocketIp, extractIp, getClientInfo, parseQueryParams };
|