stitchkit 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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 };
@@ -10,8 +10,9 @@ import {
10
10
  getClientInfo,
11
11
  parseQueryParams,
12
12
  resolveSocketIp,
13
- resolveTraceId
14
- } from "./index-p9m9c0jw.js";
13
+ resolveTraceId,
14
+ setRequestEndpoint
15
+ } from "./index-fwqnkc90.js";
15
16
  import {
16
17
  __require,
17
18
  isUnsafeKey,
@@ -600,6 +601,7 @@ function createHandler(config) {
600
601
  }
601
602
  const { method, pathParams, groupHooks } = match;
602
603
  const ctx = buildBaseContext(req, url, pathParams, traceId, clientIp);
604
+ setRequestEndpoint(method.serviceName, method.key);
603
605
  try {
604
606
  await parseRequestInto(ctx, req, url, method, config.maxUploadBytes);
605
607
  if (hooks?.beforeHandle) {
package/dist/node.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createImplement,
4
4
  createSocketIOServer,
5
5
  implement
6
- } from "./index-s8gtzt8w.js";
6
+ } from "./index-renxz7c2.js";
7
7
  import {
8
8
  AppError,
9
9
  appError,
@@ -14,7 +14,7 @@ import {
14
14
  rateLimited,
15
15
  unauthorized
16
16
  } from "./index-jgpsd7dy.js";
17
- import"./index-p9m9c0jw.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,CAgG9D"}
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,CAsG9D"}
@@ -19,6 +19,20 @@ export interface RequestContext {
19
19
  userAgent?: string;
20
20
  /** Resolved user id — set late, once auth has run. */
21
21
  userId?: string;
22
+ /**
23
+ * Stable endpoint identity — `(serviceName, action)` of the matched contract
24
+ * route. Written by the HTTP pipeline when the route matches, *before*
25
+ * validation, so even a failed request is attributed to the operation it
26
+ * targeted. → ADR 0022.
27
+ */
28
+ serviceName?: string;
29
+ action?: string;
30
+ /**
31
+ * App-defined domain dimensions (a tenant / project / entity id, …) — an
32
+ * opaque bag the core attaches no meaning to (→ ADR 0021), surfaced on
33
+ * `RequestEvent.dimensions`. Set via `setRequestDimensions`.
34
+ */
35
+ dimensions?: Record<string, string>;
22
36
  /** Error outcome — set late, by the error handler. */
23
37
  error?: {
24
38
  code?: string;
@@ -40,6 +54,23 @@ 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
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/observability/context.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,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,sDAAsD;IACtD,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC;CAClE;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;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB,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
+ {"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,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,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,sDAAsD;IACtD,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC;CAClE;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;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB,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"}
@@ -13,6 +13,20 @@ export interface RequestEvent {
13
13
  method: string;
14
14
  /** Request path — `/api/...` for HTTP, `/{source}/{tool}` for a tool call. */
15
15
  path: string;
16
+ /**
17
+ * Stable owning-contract identity of the matched operation — the "service"
18
+ * (contract prefix) and "action" (endpoint key) halves. Set on every surface
19
+ * (HTTP, MCP, agent) from the contract, not parsed from `path`. → ADR 0022.
20
+ */
21
+ serviceName?: string;
22
+ action?: string;
23
+ /**
24
+ * App-defined domain dimensions for the call — e.g. a tenant / project /
25
+ * entity id. An opaque bag the core attaches no meaning to (→ ADR 0021);
26
+ * populated by `setRequestDimensions`. The sink maps it onto its own columns
27
+ * instead of re-deriving identity from the path.
28
+ */
29
+ dimensions?: Record<string, string>;
16
30
  /** Tool name — tool calls only. */
17
31
  toolName?: string;
18
32
  /** W3C trace id — correlates every span of one logical request. */
@@ -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;AACnD,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,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,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;;;;OAIG;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"}
1
+ {"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/observability/event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,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,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;;;;OAIG;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-031q8xmx.js";
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,
@@ -163,14 +167,17 @@ function createAuditHook(config) {
163
167
  };
164
168
  };
165
169
  const toolCall = {
166
- afterToolCall: (toolName, args, result, durationMs, context) => {
167
- const parent = getRequestContext()?.trace;
168
- const span = parent ? childSpan(parent) : createTraceContext();
170
+ afterToolCall: (toolName, args, result, durationMs, context, endpoint) => {
171
+ const requestCtx = getRequestContext();
172
+ const span = requestCtx ? childSpan(requestCtx.trace) : createTraceContext();
169
173
  const measure = result.ok ? measureSize(result.data) : { resultSize: null, responseBytes: 0 };
170
174
  emit({
171
175
  source: context.source,
172
176
  method: "TOOL",
173
177
  path: `/${context.source}/${toolName}`,
178
+ serviceName: endpoint.serviceName,
179
+ action: endpoint.key,
180
+ ...requestCtx?.dimensions !== undefined && { dimensions: requestCtx.dimensions },
174
181
  toolName,
175
182
  traceId: span.traceId,
176
183
  spanId: span.spanId,
@@ -199,6 +206,8 @@ export {
199
206
  truncatePreview,
200
207
  setRequestUser,
201
208
  setRequestError,
209
+ setRequestEndpoint,
210
+ setRequestDimensions,
202
211
  sanitizePayload,
203
212
  runWithRequestContext,
204
213
  resolveTraceContext,
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.ts"],"names":[],"mappings":"AA8BA,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,CAkNxF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,uBAsBnD"}
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,CAsNxF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,uBAsBnD"}
@@ -12,7 +12,7 @@ import {
12
12
  socketIoLane,
13
13
  staticRoute,
14
14
  webSocketLane
15
- } from "../index-s8gtzt8w.js";
15
+ } from "../index-renxz7c2.js";
16
16
  import {
17
17
  createAuthHook,
18
18
  createBearerResolver,
@@ -43,16 +43,14 @@ import {
43
43
  rateLimited,
44
44
  unauthorized
45
45
  } from "../index-jgpsd7dy.js";
46
- import {
47
- getTraceId
48
- } from "../index-031q8xmx.js";
49
46
  import {
50
47
  extractIp,
51
48
  generateTraceId,
52
49
  getClientInfo,
50
+ getTraceId,
53
51
  resolveSocketIp,
54
52
  resolveTraceId
55
- } from "../index-p9m9c0jw.js";
53
+ } from "../index-fwqnkc90.js";
56
54
  import {
57
55
  isRecord
58
56
  } from "../index-tm7dqzxc.js";
package/llms-full.txt CHANGED
@@ -2256,11 +2256,13 @@ 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
+ | `dimensions` | app-defined domain dimensions (tenant / project / entity id) — see [request context](#request-context) |
2260
2262
  | `traceId` / `spanId` / `parentSpanId` | [W3C trace context](#trace-context) |
2261
2263
  | `ok` / `statusCode` | outcome — real HTTP status, or `200`/`400` for a tool |
2262
2264
  | `durationMs` / `startedAt` | timing |
2263
- | `errorCode` / `errorMessage` | failures only |
2265
+ | `errorCode` / `errorMessage` / `errorDetail` | failures only — `errorDetail` carries the structure the message flattens (e.g. Zod issues) |
2264
2266
  | `payload` | the request body / tool arguments — sanitised |
2265
2267
  | `resultSize` / `responseBytes` | result item count + serialised size |
2266
2268
  | `userId` / `ipAddress` / `userAgent` | identity |
@@ -2279,15 +2281,36 @@ Bun.serve({
2279
2281
  })
2280
2282
  ```
2281
2283
 
2282
- Two fields are filled in late the resolved user, and the error outcome. Set
2283
- them from the hooks that know:
2284
+ Some fields are filled in late. Set them from the hooks that know:
2284
2285
 
2285
2286
  ```ts
2286
- import { setRequestError, setRequestUser } from 'stitchkit/observability'
2287
+ import {
2288
+ setRequestDimensions,
2289
+ setRequestError,
2290
+ setRequestUser,
2291
+ } from 'stitchkit/observability'
2287
2292
 
2288
2293
  createAuthHook({ /* … */ inject: (ctx, user) => user && setRequestUser(user.id) })
2289
2294
  // in your onError hook:
2290
- setRequestError({ code: err.code, message: err.message })
2295
+ setRequestError({ code: err.code, message: err.message, details: err.issues })
2296
+ ```
2297
+
2298
+ **Endpoint identity is automatic.** The framework writes the matched operation's
2299
+ `(serviceName, action)` into the context at route-match, *before* validation — so
2300
+ `event.serviceName` / `event.action` are present on every event, including a
2301
+ pre-handler 400. Nothing to wire.
2302
+
2303
+ **Domain dimensions** — attach your own tenant / project / entity id with
2304
+ `setRequestDimensions`. It is an opaque `Record<string, string>` the core gives no
2305
+ meaning to (→ ADR 0021). Resolve it cheaply from `ctx.params` / headers in
2306
+ `beforeHandle` (success) or `onError` (a pre-handler failure — `ctx.params` /
2307
+ `ctx.req` are available there) and it lands on `event.dimensions` for the request
2308
+ either way, so your sink reads it as a column instead of re-parsing the path:
2309
+
2310
+ ```ts
2311
+ // beforeHandle (success) and onError (failure) alike:
2312
+ const projectId = ctx.req?.headers.get('x-project') ?? String(ctx.params?.projectId ?? '')
2313
+ if (projectId) setRequestDimensions({ projectId })
2291
2314
  ```
2292
2315
 
2293
2316
  Make the framework router share this trace id — so request logs and your
@@ -2378,14 +2401,15 @@ outcome and the duration, neither of which exists before the handler runs.
2378
2401
 
2379
2402
  ### Keying a row on (service, action)
2380
2403
 
2381
- For a per-endpoint audit row keyed by **service** and **action**, read the
2382
- endpoint identity off the `MethodDef` the hook receives — `endpoint.serviceName`
2383
- (the contract prefix) and `endpoint.key` (the endpoint key, e.g. `updatePartial`).
2384
- They are stable and always present (→ ADR 0022); the action is not in the URL and
2385
- `toolName` is absent on HTTP-only endpoints, so this is the only reliable pair.
2386
- `afterHandle` also gives you the handler `result` so it, not `createAuditHook`'s
2387
- HTTP wrapper (which never sees the response body), is the home for a rich mutation
2388
- audit that records output:
2404
+ `createAuditHook` already keys every event by **service** and **action**
2405
+ (`event.serviceName` / `event.action`, ADR 0029) — reach for the raw hook only
2406
+ when you also need the handler **output**, which the audit wrapper never sees. For
2407
+ that, read the endpoint identity off the `MethodDef` the hook receives
2408
+ `endpoint.serviceName` (the contract prefix) and `endpoint.key` (the endpoint key,
2409
+ e.g. `updatePartial`). They are stable and always present (→ ADR 0022); the action
2410
+ is not in the URL and `toolName` is absent on HTTP-only endpoints, so this is the
2411
+ only reliable pair. `afterHandle` also gives you the handler `result` — so it is
2412
+ the home for a rich mutation audit that records output:
2389
2413
 
2390
2414
  ```ts
2391
2415
  hooks: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",
@@ -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 };
@@ -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 };