@pikku/core 0.12.150 → 0.12.151

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.
Files changed (139) hide show
  1. package/dist/function/function-runner.js +23 -0
  2. package/dist/function/trace-queue-service.d.ts +26 -0
  3. package/dist/function/trace-queue-service.js +48 -0
  4. package/dist/mesh.d.ts +2 -2
  5. package/dist/mesh.js +3 -3
  6. package/dist/middleware/index.d.ts +1 -1
  7. package/dist/middleware/index.js +1 -1
  8. package/dist/middleware/mesh-auth.d.ts +3 -2
  9. package/dist/middleware/mesh-auth.js +7 -2
  10. package/dist/middleware/remote-jobs-secret.d.ts +1 -1
  11. package/dist/middleware/require-origin.d.ts +19 -0
  12. package/dist/middleware/require-origin.js +36 -2
  13. package/dist/services/deployment-service.d.ts +1 -1
  14. package/dist/services/incoming-webhook-service.d.ts +4 -2
  15. package/dist/services/incoming-webhook-service.js +4 -3
  16. package/dist/services/queue-webhook-service.js +7 -1
  17. package/dist/services/remote-jobs.d.ts +6 -1
  18. package/dist/services/scheduler-service.d.ts +3 -1
  19. package/dist/services/webhook-service.d.ts +2 -0
  20. package/dist/services/workflow-service.d.ts +4 -1
  21. package/dist/testing/index.d.ts +4 -0
  22. package/dist/testing/index.js +2 -0
  23. package/dist/testing/queue-identity-transport-tests.d.ts +18 -0
  24. package/dist/testing/queue-identity-transport-tests.js +162 -0
  25. package/dist/testing/queue-trace-conformance.d.ts +26 -0
  26. package/dist/testing/queue-trace-conformance.js +259 -0
  27. package/dist/testing/service-tests/channel-store-tests.js +11 -0
  28. package/dist/testing/service-tests/queued-workflow-harness.d.ts +18 -1
  29. package/dist/testing/service-tests/queued-workflow-harness.js +19 -3
  30. package/dist/testing/service-tests/workflow-service-tests.js +12 -0
  31. package/dist/testing/trace-matrix/assertions.d.ts +3 -0
  32. package/dist/testing/trace-matrix/assertions.js +190 -0
  33. package/dist/testing/trace-matrix/chain-harness.d.ts +119 -0
  34. package/dist/testing/trace-matrix/chain-harness.js +777 -0
  35. package/dist/testing/trace-matrix/dimensions.d.ts +55 -0
  36. package/dist/testing/trace-matrix/dimensions.js +305 -0
  37. package/dist/testing/trace-matrix/pairwise.d.ts +20 -0
  38. package/dist/testing/trace-matrix/pairwise.js +75 -0
  39. package/dist/testing/trace-matrix/report.d.ts +1 -0
  40. package/dist/testing/trace-matrix/report.js +16 -0
  41. package/dist/testing/trace-matrix/transports.d.ts +19 -0
  42. package/dist/testing/trace-matrix/transports.js +27 -0
  43. package/dist/testing/trace-matrix/workflow-cases.d.ts +24 -0
  44. package/dist/testing/trace-matrix/workflow-cases.js +147 -0
  45. package/dist/trace.d.ts +66 -0
  46. package/dist/trace.js +97 -0
  47. package/dist/types/core.types.d.ts +6 -2
  48. package/dist/wirings/addon/addon-reach.d.ts +3 -0
  49. package/dist/wirings/addon/addon-reach.js +9 -0
  50. package/dist/wirings/addon/define-plugin.d.ts +6 -0
  51. package/dist/wirings/addon/wire-addon.js +5 -0
  52. package/dist/wirings/addon/wire-remote-addon.d.ts +11 -0
  53. package/dist/wirings/agent/agent-finalize.d.ts +2 -0
  54. package/dist/wirings/agent/agent-finalize.js +1 -1
  55. package/dist/wirings/agent/agent-prepare.d.ts +3 -0
  56. package/dist/wirings/agent/agent-prepare.js +24 -8
  57. package/dist/wirings/agent/agent-rpc.js +1 -0
  58. package/dist/wirings/agent/agent-runner.js +3 -0
  59. package/dist/wirings/agent/agent-stream.js +4 -0
  60. package/dist/wirings/agent/voice-input.d.ts +1 -1
  61. package/dist/wirings/agent/voice-output.d.ts +1 -1
  62. package/dist/wirings/agent-scorer/agent-scorer-live.d.ts +4 -2
  63. package/dist/wirings/agent-scorer/agent-scorer-live.js +2 -2
  64. package/dist/wirings/channel/channel-common.d.ts +4 -1
  65. package/dist/wirings/channel/channel-common.js +3 -1
  66. package/dist/wirings/channel/channel-handler.js +2 -0
  67. package/dist/wirings/channel/channel-host-rpc.d.ts +1 -1
  68. package/dist/wirings/channel/channel-host-rpc.js +2 -2
  69. package/dist/wirings/channel/channel-rpc-responder.d.ts +7 -1
  70. package/dist/wirings/channel/channel-rpc-responder.js +2 -2
  71. package/dist/wirings/channel/channel-rpc-service.d.ts +1 -1
  72. package/dist/wirings/channel/channel-rpc-service.js +2 -2
  73. package/dist/wirings/channel/channel-rpc.types.d.ts +5 -2
  74. package/dist/wirings/channel/channel-rpc.types.js +1 -1
  75. package/dist/wirings/channel/channel-store.d.ts +4 -2
  76. package/dist/wirings/channel/channel-store.js +2 -2
  77. package/dist/wirings/channel/channel.types.d.ts +3 -0
  78. package/dist/wirings/channel/local/local-channel-runner.js +16 -5
  79. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +3 -1
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +27 -4
  82. package/dist/wirings/gateway/gateway-runner.js +3 -1
  83. package/dist/wirings/http/http-runner.d.ts +4 -4
  84. package/dist/wirings/http/http-runner.js +14 -18
  85. package/dist/wirings/http/http.types.d.ts +3 -3
  86. package/dist/wirings/mcp/mcp-runner.js +15 -5
  87. package/dist/wirings/queue/in-memory-queue-service.d.ts +1 -1
  88. package/dist/wirings/queue/in-memory-queue-service.js +8 -2
  89. package/dist/wirings/queue/index.d.ts +1 -0
  90. package/dist/wirings/queue/index.js +1 -0
  91. package/dist/wirings/queue/queue-runner.d.ts +3 -4
  92. package/dist/wirings/queue/queue-runner.js +8 -7
  93. package/dist/wirings/queue/queue-trace.d.ts +18 -0
  94. package/dist/wirings/queue/queue-trace.js +42 -0
  95. package/dist/wirings/queue/queue.types.d.ts +7 -0
  96. package/dist/wirings/queue/remote-queue-job.d.ts +1 -1
  97. package/dist/wirings/queue/remote-queue-job.js +5 -3
  98. package/dist/wirings/rpc/rpc-runner.d.ts +7 -1
  99. package/dist/wirings/rpc/rpc-runner.js +43 -21
  100. package/dist/wirings/rpc/rpc-types.d.ts +2 -0
  101. package/dist/wirings/scheduler/remote-scheduled-job.d.ts +1 -1
  102. package/dist/wirings/scheduler/remote-scheduled-job.js +2 -2
  103. package/dist/wirings/scheduler/scheduler-runner.d.ts +4 -4
  104. package/dist/wirings/scheduler/scheduler-runner.js +7 -6
  105. package/dist/wirings/trigger/pikku-trigger-service.js +7 -4
  106. package/dist/wirings/trigger/webhook-source-runner.d.ts +7 -1
  107. package/dist/wirings/trigger/webhook-source-runner.js +11 -3
  108. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  109. package/dist/wirings/workflow/graph/graph-runner.js +49 -30
  110. package/dist/wirings/workflow/index.d.ts +3 -1
  111. package/dist/wirings/workflow/index.js +1 -0
  112. package/dist/wirings/workflow/pikku-scenario-service.js +2 -0
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +37 -40
  114. package/dist/wirings/workflow/pikku-workflow-service.js +122 -133
  115. package/dist/wirings/workflow/workflow-child-step.d.ts +2 -2
  116. package/dist/wirings/workflow/workflow-child-step.js +11 -4
  117. package/dist/wirings/workflow/workflow-compensation.d.ts +6 -0
  118. package/dist/wirings/workflow/workflow-compensation.js +15 -1
  119. package/dist/wirings/workflow/workflow-constants.d.ts +13 -0
  120. package/dist/wirings/workflow/workflow-constants.js +13 -0
  121. package/dist/wirings/workflow/workflow-dsl-pass.d.ts +2 -0
  122. package/dist/wirings/workflow/workflow-dsl-pass.js +27 -11
  123. package/dist/wirings/workflow/workflow-recovery.d.ts +131 -3
  124. package/dist/wirings/workflow/workflow-recovery.js +233 -7
  125. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +1 -0
  126. package/dist/wirings/workflow/workflow-run-mirror.d.ts +4 -0
  127. package/dist/wirings/workflow/workflow-run-mirror.js +16 -0
  128. package/dist/wirings/workflow/workflow-run-status.d.ts +2 -0
  129. package/dist/wirings/workflow/workflow-run-status.js +13 -0
  130. package/dist/wirings/workflow/workflow-run-trace.d.ts +22 -0
  131. package/dist/wirings/workflow/workflow-run-trace.js +24 -0
  132. package/dist/wirings/workflow/workflow-step-claim.d.ts +18 -0
  133. package/dist/wirings/workflow/workflow-step-claim.js +47 -0
  134. package/dist/wirings/workflow/workflow-step-retry.d.ts +6 -2
  135. package/dist/wirings/workflow/workflow-step-retry.js +8 -3
  136. package/dist/wirings/workflow/workflow-wire.d.ts +12 -0
  137. package/dist/wirings/workflow/workflow-wire.js +33 -0
  138. package/dist/wirings/workflow/workflow.types.d.ts +16 -0
  139. package/package.json +2 -1
@@ -2,7 +2,8 @@ import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
2
2
  import { runPikkuFunc } from '../../function/function-runner.js';
3
3
  import { createMiddlewareSessionWireProps, } from '../../services/user-session-service.js';
4
4
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../../channel-middleware-runner.js';
5
- export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, channelMiddlewareMeta, userSession, wireServices, }) => {
5
+ import { childTraceWire } from '../../trace.js';
6
+ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, channelMiddlewareMeta, userSession, wireServices, trace, }) => {
6
7
  const lifecycleMiddleware = typeof lifecycleConfig === 'object' && 'middleware' in lifecycleConfig
7
8
  ? lifecycleConfig.middleware || []
8
9
  : [];
@@ -17,6 +18,7 @@ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, l
17
18
  });
18
19
  let wire = {
19
20
  channel,
21
+ ...childTraceWire(trace),
20
22
  ...(userSession ? createMiddlewareSessionWireProps(userSession) : {}),
21
23
  };
22
24
  if (allChannelMiddleware.length > 0) {
@@ -3,6 +3,7 @@ import { runPikkuFunc } from '../../function/function-runner.js';
3
3
  import { isChannelRPCPending, isChannelRPCResponse } from './channel-rpc.js';
4
4
  import { handleChannelRPCResponse } from './channel-host-rpc.js';
5
5
  import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js';
6
+ import { childTraceWire } from '../../trace.js';
6
7
  const getChannelMeta = (channelName) => {
7
8
  return pikkuState(null, 'channel', 'meta')[channelName];
8
9
  };
@@ -88,6 +89,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
88
89
  sessionService: userSession,
89
90
  wire: {
90
91
  channel: channelHandler.getChannel(),
92
+ ...childTraceWire(channelHandler.trace),
91
93
  ...(userSession && createMiddlewareSessionWireProps(userSession)),
92
94
  },
93
95
  packageName,
@@ -15,7 +15,7 @@ export declare const getChannelHostRPC: (channel: PikkuChannel<unknown, any>, op
15
15
  * Kept out of the channel handler so a channel nobody calls back on pays for no
16
16
  * registry, and because a handler has no services to build a validator with.
17
17
  */
18
- export declare const channelRemote: (channel: PikkuChannel<unknown, any, any>, funcName: string, data?: unknown) => Promise<unknown>;
18
+ export declare const channelRemote: (channel: PikkuChannel<unknown, any, any>, funcName: string, data?: unknown, traceparent?: string) => Promise<unknown>;
19
19
  /** False for an unknown connection or id, so a stray frame is dropped. */
20
20
  export declare const handleChannelRPCResponse: (channelId: string, message: unknown) => boolean;
21
21
  /**
@@ -19,10 +19,10 @@ export const getChannelHostRPC = (channel, options = {}) => {
19
19
  * Kept out of the channel handler so a channel nobody calls back on pays for no
20
20
  * registry, and because a handler has no services to build a validator with.
21
21
  */
22
- export const channelRemote = async (channel, funcName, data) => getChannelHostRPC(channel, {
22
+ export const channelRemote = async (channel, funcName, data, traceparent) => getChannelHostRPC(channel, {
23
23
  validateInput: createChannelRPCInputValidator(getSingletonServices()),
24
24
  validateResult: createChannelRPCResultValidator(getSingletonServices()),
25
- }).invoke(funcName, data);
25
+ }).invoke(funcName, data, undefined, traceparent);
26
26
  /** False for an unknown connection or id, so a stray frame is dropped. */
27
27
  export const handleChannelRPCResponse = (channelId, message) => hostRPCByChannel.get(channelId)?.handleResponse(message) ?? false;
28
28
  /**
@@ -7,9 +7,15 @@ import { type ApprovalRequester, type Capabilities } from './channel-rpc.types.j
7
7
  * name is refused exactly like an unknown one, so the caller cannot probe for
8
8
  * what this end can do.
9
9
  */
10
- export declare const createChannelRPCResponder: ({ capabilities, send, approve, }: {
10
+ export declare const createChannelRPCResponder: ({ capabilities, send, approve, trustInboundTraceparent, }: {
11
11
  capabilities: Capabilities;
12
12
  send: (data: unknown) => Promise<void> | void;
13
13
  /** Omitted means there is nobody to ask, which refuses rather than runs. */
14
14
  approve?: ApprovalRequester;
15
+ /**
16
+ * Hands a capability the peer's `traceparent`. Off by default, like public
17
+ * HTTP: the caller may turn it on only for a channel whose peer it has
18
+ * authenticated.
19
+ */
20
+ trustInboundTraceparent?: boolean;
15
21
  }) => (message: unknown) => Promise<boolean>;
@@ -7,7 +7,7 @@ import { CHANNEL_RPC_PENDING, CHANNEL_RPC_RESPONSE, isCapabilityDef, isChannelRP
7
7
  * name is refused exactly like an unknown one, so the caller cannot probe for
8
8
  * what this end can do.
9
9
  */
10
- export const createChannelRPCResponder = ({ capabilities, send, approve, }) => {
10
+ export const createChannelRPCResponder = ({ capabilities, send, approve, trustInboundTraceparent = false, }) => {
11
11
  return async (message) => {
12
12
  if (!isChannelRPCRequest(message)) {
13
13
  return false;
@@ -58,7 +58,7 @@ export const createChannelRPCResponder = ({ capabilities, send, approve, }) => {
58
58
  action: CHANNEL_RPC_RESPONSE,
59
59
  id: message.id,
60
60
  ok: true,
61
- result: await execute(message.data),
61
+ result: await execute(message.data, trustInboundTraceparent ? { traceparent: message.traceparent } : {}),
62
62
  };
63
63
  }
64
64
  catch (e) {
@@ -30,5 +30,5 @@ export declare class ChannelDeploymentService implements DeploymentService {
30
30
  * session on the wire would carry the caller's credentials while authorising
31
31
  * nothing.
32
32
  */
33
- invoke(funcName: string, data: unknown, _session?: unknown, traceId?: string): Promise<unknown>;
33
+ invoke(funcName: string, data: unknown, _session?: unknown, traceparent?: string): Promise<unknown>;
34
34
  }
@@ -37,7 +37,7 @@ export class ChannelDeploymentService {
37
37
  * session on the wire would carry the caller's credentials while authorising
38
38
  * nothing.
39
39
  */
40
- async invoke(funcName, data, _session, traceId) {
40
+ async invoke(funcName, data, _session, traceparent) {
41
41
  // Not a security control — the peer must check what it is handed, and a
42
42
  // hostile caller would send arguments that pass. It catches version drift
43
43
  // and fails it here rather than inside the peer's process.
@@ -60,7 +60,7 @@ export class ChannelDeploymentService {
60
60
  id,
61
61
  funcName,
62
62
  data,
63
- traceId,
63
+ traceparent,
64
64
  };
65
65
  try {
66
66
  await this.send(request);
@@ -12,7 +12,8 @@ export interface ChannelRPCRequest {
12
12
  id: string;
13
13
  funcName: string;
14
14
  data: unknown;
15
- traceId?: string;
15
+ /** A claim by the peer. See `trustInboundTraceparent` on the responder. */
16
+ traceparent?: string;
16
17
  }
17
18
  /** The answer to a {@link ChannelRPCRequest} with the same `id`: a result when `ok`, an error name and message otherwise. */
18
19
  export interface ChannelRPCResponse {
@@ -63,7 +64,9 @@ export interface ApprovalPolicy {
63
64
  approvalDescriptionFn?: (input: unknown) => Promise<string> | string;
64
65
  }
65
66
  /** A function one end of a channel offers for the peer to call through `channel.remote`. */
66
- export type CapabilityHandler = (data: any) => Promise<unknown> | unknown;
67
+ export type CapabilityHandler = (data: any, context?: {
68
+ traceparent?: string;
69
+ }) => Promise<unknown> | unknown;
67
70
  /**
68
71
  * `needsApproval` is required here on purpose. On `AgentToolDef` the same
69
72
  * field is optional and absence means "do not ask"; here absence means the
@@ -18,7 +18,7 @@ export const isChannelRPCRequest = (message) => isRecord(message) &&
18
18
  message.id.length > 0 &&
19
19
  typeof message.funcName === 'string' &&
20
20
  message.funcName.length > 0 &&
21
- (message.traceId === undefined || typeof message.traceId === 'string');
21
+ (message.traceparent === undefined || typeof message.traceparent === 'string');
22
22
  export const isChannelRPCResponse = (message) => isRecord(message) &&
23
23
  message.action === CHANNEL_RPC_RESPONSE &&
24
24
  typeof message.id === 'string' &&
@@ -4,12 +4,14 @@ export type Channel<ChannelType = unknown, OpeningData = unknown> = {
4
4
  channelName: string;
5
5
  channelObject?: ChannelType;
6
6
  openingData?: OpeningData;
7
+ /** The connection's W3C trace, so every frame continues it. */
8
+ traceparent?: string;
7
9
  };
8
10
  /**
9
11
  * Persists open channels for serverless runtimes, where each connect, message and disconnect runs in a separate invocation.
10
12
  *
11
- * It keeps the channel's opening data, its state, and the `pikkuUserId` of the
12
- * session established on connect. The session itself is not stored: each
13
+ * It keeps the channel's opening data, its trace, its state, and the
14
+ * `pikkuUserId` of the session established on connect. The session itself is not stored: each
13
15
  * message reloads it from the `sessionStore` by that id.
14
16
  */
15
17
  export declare abstract class ChannelStore<ChannelType = unknown, OpeningData = unknown, TypedChannel = Channel<ChannelType, OpeningData>> {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Persists open channels for serverless runtimes, where each connect, message and disconnect runs in a separate invocation.
3
3
  *
4
- * It keeps the channel's opening data, its state, and the `pikkuUserId` of the
5
- * session established on connect. The session itself is not stored: each
4
+ * It keeps the channel's opening data, its trace, its state, and the
5
+ * `pikkuUserId` of the session established on connect. The session itself is not stored: each
6
6
  * message reloads it from the `sessionStore` by that id.
7
7
  */
8
8
  export class ChannelStore {
@@ -7,6 +7,7 @@ export type CorePikkuChannelMiddleware<Services = any, Event = unknown> = (servi
7
7
  /** Builds a {@link CorePikkuChannelMiddleware} from configuration, so the same middleware can be reused with different settings. */
8
8
  export type CorePikkuChannelMiddlewareFactory<In = any, Services = any, Event = unknown> = (input: In) => CorePikkuChannelMiddleware<Services, Event>;
9
9
  import type { CorePikkuFunction, CorePikkuFunctionConfig, CorePikkuFunctionSessionless, CorePikkuPermission } from '../../function/functions.types.js';
10
+ import type { TraceContext } from '../../trace.js';
10
11
  /** Options for running a channel connection. They mean the same as the matching {@link RunHTTPWiringOptions}. */
11
12
  export type RunChannelOptions = Partial<{
12
13
  skipUserSession: boolean;
@@ -134,6 +135,8 @@ export interface PikkuChannel<OpeningData, out Out, Remote extends (...args: any
134
135
  }
135
136
  /** The runtime side of one connection: how a channel's sends reach the actual socket. */
136
137
  export interface PikkuChannelHandler<OpeningData = unknown, Out = unknown> {
138
+ /** The connection's trace. Each message and reverse RPC is a span in it. */
139
+ trace?: TraceContext;
137
140
  send(message: Out, isBinary?: boolean): Promise<void> | void;
138
141
  sendBinary(data: BinaryData): Promise<void> | void;
139
142
  getChannel(): PikkuChannel<OpeningData, Out>;
@@ -10,6 +10,7 @@ import { PikkuSessionService, createMiddlewareSessionWireProps, } from '../../..
10
10
  import { combineMiddleware, runMiddleware } from '../../../middleware-runner.js';
11
11
  import { runChannelLifecycleWithMiddleware } from '../channel-common.js';
12
12
  import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../../pikku-state.js';
13
+ import { childTraceWire, createTrace, tracedLogger } from '../../../trace.js';
13
14
  const runUpgradeMiddleware = async (singletonServices, request, response, userSession) => {
14
15
  const http = createHTTPWire(request, response);
15
16
  const httpGroups = pikkuState(null, 'middleware', 'httpGroup');
@@ -73,9 +74,14 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
73
74
  const main = async () => {
74
75
  try {
75
76
  channelHandler = new PikkuLocalChannelHandler(channelId, channelConfig.name, openingData, singletonServices.logger);
77
+ // knowledge: decisions/internals/a-channel-connection-is-one-trace.md
78
+ channelHandler.trace = createTrace();
79
+ const { traceId } = channelHandler.trace;
80
+ const logger = tracedLogger(singletonServices.logger, traceId);
76
81
  const channel = channelHandler.getChannel();
77
82
  const wire = {
78
83
  channel,
84
+ ...childTraceWire(channelHandler.trace),
79
85
  ...createMiddlewareSessionWireProps(userSession),
80
86
  };
81
87
  if (createWireServices) {
@@ -103,17 +109,19 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
103
109
  channelMiddlewareMeta: meta.channelMiddleware,
104
110
  userSession,
105
111
  wireServices,
112
+ trace: channelHandler?.trace,
106
113
  });
107
114
  if (result !== undefined) {
108
115
  await channel.send(result);
109
116
  }
110
117
  }
111
118
  catch (e) {
112
- singletonServices.logger.error(`Error handling onConnect: ${e}`);
119
+ logger.error(`Error handling onConnect: ${e}`);
113
120
  const errorResponse = getErrorResponse(e);
114
121
  await channel.send({
115
122
  error: errorResponse?.message ??
116
123
  (isProduction() ? 'Internal server error' : e.message),
124
+ errorId: traceId,
117
125
  ...(!isProduction() && { errorName: e.constructor?.name }),
118
126
  });
119
127
  }
@@ -132,14 +140,16 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
132
140
  channelMiddlewareMeta: meta.channelMiddleware,
133
141
  userSession,
134
142
  wireServices,
143
+ trace: channelHandler?.trace,
135
144
  });
136
145
  }
137
146
  catch (e) {
138
- singletonServices.logger.error(`Error handling onDisconnect: ${e}`);
147
+ logger.error(`Error handling onDisconnect: ${e}`);
139
148
  const errorResponse = getErrorResponse(e);
140
149
  await channel.send({
141
150
  error: errorResponse?.message ??
142
151
  (isProduction() ? 'Internal server error' : e.message),
152
+ errorId: traceId,
143
153
  ...(!isProduction() && { errorName: e.constructor?.name }),
144
154
  });
145
155
  }
@@ -160,15 +170,16 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
160
170
  }
161
171
  }
162
172
  catch (e) {
163
- singletonServices.logger.error(e);
173
+ logger.error(e);
164
174
  const errorResponse = getErrorResponse(e);
165
175
  await channel.send({
166
176
  error: errorResponse?.message ??
167
177
  (isProduction() ? 'Internal server error' : e.message),
178
+ errorId: traceId,
168
179
  ...(!isProduction() && { errorName: e.constructor?.name }),
169
180
  });
170
181
  setTimeout(() => {
171
- Promise.resolve(channel.close()).catch((closeError) => singletonServices.logger.error(closeError));
182
+ Promise.resolve(channel.close()).catch((closeError) => logger.error(closeError));
172
183
  }, 200);
173
184
  }
174
185
  });
@@ -181,7 +192,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
181
192
  }
182
193
  }
183
194
  catch (e) {
184
- singletonServices.logger.error(`Error handling binary message: ${e.message}`);
195
+ logger.error(`Error handling binary message: ${e.message}`);
185
196
  }
186
197
  });
187
198
  }
@@ -1,4 +1,5 @@
1
1
  import type { BinaryData, PikkuChannel, PikkuChannelHandler } from './channel.types.js';
2
+ import { type TraceContext } from '../../trace.js';
2
3
  /**
3
4
  * Base class for a runtime's {@link PikkuChannelHandler}. A runtime implements `send` and `sendBinary`; this builds the {@link PikkuChannel} on top, with in-memory state and `remote`.
4
5
  *
@@ -10,6 +11,7 @@ export declare abstract class PikkuAbstractChannelHandler<OpeningData = unknown,
10
11
  protected openingData: OpeningData;
11
12
  protected channel?: PikkuChannel<OpeningData, Out>;
12
13
  constructor(channelId: string, channelName: string, openingData: OpeningData);
14
+ trace?: TraceContext;
13
15
  abstract send(message: Out, isBinary?: boolean): Promise<void> | void;
14
16
  abstract sendBinary(data: BinaryData): Promise<void> | void;
15
17
  getChannel(): PikkuChannel<OpeningData, Out>;
@@ -1,4 +1,5 @@
1
1
  import { channelRemote, releaseChannelHostRPC } from './channel-host-rpc.js';
2
+ import { outgoingTraceparent } from '../../trace.js';
2
3
  /**
3
4
  * Base class for a runtime's {@link PikkuChannelHandler}. A runtime implements `send` and `sendBinary`; this builds the {@link PikkuChannel} on top, with in-memory state and `remote`.
4
5
  *
@@ -14,6 +15,7 @@ export class PikkuAbstractChannelHandler {
14
15
  this.channelName = channelName;
15
16
  this.openingData = openingData;
16
17
  }
18
+ trace;
17
19
  getChannel() {
18
20
  if (!this.channel) {
19
21
  let channelState;
@@ -32,7 +34,7 @@ export class PikkuAbstractChannelHandler {
32
34
  clearState: () => {
33
35
  channelState = undefined;
34
36
  },
35
- remote: (funcName, data) => channelRemote(this.getChannel(), funcName, data),
37
+ remote: (funcName, data) => channelRemote(this.getChannel(), funcName, data, outgoingTraceparent(this)),
36
38
  };
37
39
  }
38
40
  return this.channel;
@@ -7,6 +7,7 @@ import { PikkuSessionService, createMiddlewareSessionWireProps, } from '../../..
7
7
  import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../../pikku-state.js';
8
8
  import { PikkuFetchHTTPRequest } from '../../http/pikku-fetch-http-request.js';
9
9
  import { runChannelLifecycleWithMiddleware } from '../channel-common.js';
10
+ import { createTrace, formatTraceparent, parseTraceparent, tracedLogger, } from '../../../trace.js';
10
11
  const getVariablesForChannel = ({ channelId, channelName, channelHandlerFactory, openingData, }) => {
11
12
  const channels = pikkuState(null, 'channel', 'channels');
12
13
  const channelConfig = channels.get(channelName);
@@ -26,6 +27,21 @@ const getVariablesForChannel = ({ channelId, channelName, channelHandlerFactory,
26
27
  meta,
27
28
  };
28
29
  };
30
+ /**
31
+ * The connection's trace as connect stored it. A frame is a span in it, so this
32
+ * is the connection's own span and not a new one.
33
+ */
34
+ // knowledge: decisions/internals/a-channel-connection-is-one-trace.md
35
+ const storedTrace = (traceparent) => {
36
+ const stored = parseTraceparent(traceparent);
37
+ return stored
38
+ ? {
39
+ traceId: stored.traceId,
40
+ spanId: stored.parentSpanId,
41
+ sampled: stored.sampled,
42
+ }
43
+ : createTrace();
44
+ };
29
45
  /**
30
46
  * Handles a serverless connect event: opens the channel, stores it, and runs `onConnect`.
31
47
  *
@@ -56,18 +72,21 @@ export const runChannelConnect = async ({ channelId, channelObject, request, res
56
72
  coerceDataFromSchema,
57
73
  userSession,
58
74
  });
75
+ const trace = createTrace();
59
76
  try {
60
77
  await channelStore.addChannel({
61
78
  channelId,
62
79
  channelName: channelConfig.name,
63
80
  openingData,
64
81
  channelObject,
82
+ traceparent: formatTraceparent(trace),
65
83
  });
66
- const { channel } = getVariablesForChannel({
84
+ const { channel, channelHandler } = getVariablesForChannel({
67
85
  channelId,
68
86
  channelHandlerFactory,
69
87
  channelName: channelConfig.name,
70
88
  });
89
+ channelHandler.trace = trace;
71
90
  channel.setState = (state) => channelStore.setState(channelId, state);
72
91
  // knowledge: decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md
73
92
  channel.getState = () => channelStore.getState(channelId);
@@ -95,6 +114,7 @@ export const runChannelConnect = async ({ channelId, channelObject, request, res
95
114
  channelMiddlewareMeta: meta.channelMiddleware,
96
115
  userSession,
97
116
  wireServices,
117
+ trace: channelHandler.trace,
98
118
  });
99
119
  }
100
120
  const pikkuUserId = userSession.getPikkuUserId();
@@ -131,7 +151,7 @@ export const runChannelDisconnect = async ({ channelId, channelStore, channelHan
131
151
  singletonServices.logger.info(`Channel ${channelId} not found during disconnect - already cleaned up`);
132
152
  return;
133
153
  }
134
- const { openingData, channelName, pikkuUserId } = channelData;
154
+ const { openingData, channelName, pikkuUserId, traceparent } = channelData;
135
155
  const { channel, channelConfig, meta } = getVariablesForChannel({
136
156
  channelId,
137
157
  channelHandlerFactory,
@@ -173,6 +193,7 @@ export const runChannelDisconnect = async ({ channelId, channelStore, channelHan
173
193
  channelMiddlewareMeta: meta.channelMiddleware,
174
194
  userSession,
175
195
  wireServices,
196
+ trace: storedTrace(traceparent),
176
197
  });
177
198
  }
178
199
  catch (e) {
@@ -198,13 +219,14 @@ export const runChannelMessage = async ({ channelId, channelStore, channelHandle
198
219
  const singletonServices = getSingletonServices();
199
220
  const createWireServices = getCreateWireServices();
200
221
  let wireServices;
201
- const { openingData, channelName, pikkuUserId } = await channelStore.getChannel(channelId);
222
+ const { openingData, channelName, pikkuUserId, traceparent } = await channelStore.getChannel(channelId);
202
223
  const { channel, channelHandler, channelConfig } = getVariablesForChannel({
203
224
  channelId,
204
225
  channelHandlerFactory,
205
226
  openingData,
206
227
  channelName,
207
228
  });
229
+ channelHandler.trace = storedTrace(traceparent);
208
230
  channel.setState = (state) => channelStore.setState(channelId, state);
209
231
  // knowledge: decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md
210
232
  channel.getState = () => channelStore.getState(channelId);
@@ -234,10 +256,11 @@ export const runChannelMessage = async ({ channelId, channelStore, channelHandle
234
256
  response = await onMessage(data);
235
257
  }
236
258
  catch (e) {
237
- singletonServices.logger.error(`Error processing message: ${e.message || e}`);
259
+ tracedLogger(singletonServices.logger, channelHandler.trace.traceId).error(`Error processing message: ${e.message || e}`);
238
260
  return {
239
261
  error: e.message || 'Unknown error',
240
262
  errorName: e.constructor?.name,
263
+ errorId: channelHandler.trace.traceId,
241
264
  };
242
265
  }
243
266
  finally {
@@ -66,6 +66,7 @@ export const resolveGatewayAdapter = (config, services) => {
66
66
  }
67
67
  return resolved;
68
68
  };
69
+ import { traceWire } from '../../trace.js';
69
70
  /**
70
71
  * Wires a messaging gateway (Slack, WhatsApp, a web chat, ...) so each inbound message runs `func`
71
72
  * with the adapter-normalised message.
@@ -318,7 +319,8 @@ export const createListenerMessageHandler = (name, config, singletonServices) =>
318
319
  const parsed = adapter.parse(rawData);
319
320
  if (!parsed)
320
321
  return;
321
- const wire = {};
322
+ // Here rather than left to the function runner: middleware runs first.
323
+ const wire = traceWire();
322
324
  const gateway = {
323
325
  gatewayName: name,
324
326
  senderId: parsed.senderId,
@@ -62,9 +62,9 @@ export declare const pikkuFetch: <In, Out>(request: Request | PikkuHTTPRequest,
62
62
  *
63
63
  * The lower-level form of {@link fetch} for adapters that bring their own
64
64
  * request and response objects (Express, Fastify, uWS). A web `Request` is
65
- * wrapped with the `maxBodySize` limit. The trace ID is taken from `traceId`,
66
- * then the `x-request-id` header, then `generateRequestId`, and is returned to
67
- * the client as `errorId` on failures.
65
+ * wrapped with the `maxBodySize` limit. The trace comes from a valid `traceId`,
66
+ * then `traceparent` when `trustInboundTraceparent` is set, and is otherwise new.
67
+ * Its id is returned to the client as `errorId` on failures.
68
68
  *
69
69
  * Errors are written to `response`: a 4xx keeps its message and payload, while a
70
70
  * 5xx or an unknown error hides its message unless `exposeErrors` is set outside
@@ -78,4 +78,4 @@ export declare const pikkuFetch: <In, Out>(request: Request | PikkuHTTPRequest,
78
78
  * @throws The original error when `bubbleErrors` is set, after the error response has been written.
79
79
  * @throws {Error} When `skipUserSession` is set on a route that requires auth.
80
80
  */
81
- export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, exposeErrors, generateRequestId, traceId: externalTraceId, maxBodySize, }?: RunHTTPWiringOptions) => Promise<Out | void>;
81
+ export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, exposeErrors, traceId, trustInboundTraceparent, maxBodySize, }?: RunHTTPWiringOptions) => Promise<Out | void>;
@@ -1,5 +1,6 @@
1
1
  import { NotFoundError } from '../../errors/errors.js';
2
2
  import { createWeakUID, isSerializable } from '../../utils.js';
3
+ import { resolveHTTPTrace } from '../../trace.js';
3
4
  import { getSingletonServices, getCreateWireServices, } from '../../pikku-state.js';
4
5
  import { PikkuSessionService, createMiddlewareSessionWireProps, } from '../../services/user-session-service.js';
5
6
  import { getErrorResponse } from '../../errors/error-handler.js';
@@ -120,7 +121,7 @@ export const createHTTPWire = (request, response) => {
120
121
  };
121
122
  const executeRoute = async (services, matchedRoute, http, options) => {
122
123
  const { params, route, meta } = matchedRoute;
123
- const { singletonServices, createWireServices, skipUserSession, requestId } = services;
124
+ const { singletonServices, createWireServices, skipUserSession, trace } = services;
124
125
  const userSession = new PikkuSessionService(singletonServices.sessionStore);
125
126
  http?.request?.setParams(params);
126
127
  if (meta.headersSchemaName && http.request && singletonServices.schema) {
@@ -206,7 +207,8 @@ const executeRoute = async (services, matchedRoute, http, options) => {
206
207
  });
207
208
  }
208
209
  const wire = {
209
- traceId: requestId,
210
+ traceId: trace.traceId,
211
+ trace,
210
212
  http,
211
213
  channel,
212
214
  ...createMiddlewareSessionWireProps(userSession),
@@ -295,9 +297,9 @@ export const pikkuFetch = async (request, params = {}) => {
295
297
  *
296
298
  * The lower-level form of {@link fetch} for adapters that bring their own
297
299
  * request and response objects (Express, Fastify, uWS). A web `Request` is
298
- * wrapped with the `maxBodySize` limit. The trace ID is taken from `traceId`,
299
- * then the `x-request-id` header, then `generateRequestId`, and is returned to
300
- * the client as `errorId` on failures.
300
+ * wrapped with the `maxBodySize` limit. The trace comes from a valid `traceId`,
301
+ * then `traceparent` when `trustInboundTraceparent` is set, and is otherwise new.
302
+ * Its id is returned to the client as `errorId` on failures.
301
303
  *
302
304
  * Errors are written to `response`: a 4xx keeps its message and payload, while a
303
305
  * 5xx or an unknown error hides its message unless `exposeErrors` is set outside
@@ -311,22 +313,16 @@ export const pikkuFetch = async (request, params = {}) => {
311
313
  * @throws The original error when `bubbleErrors` is set, after the error response has been written.
312
314
  * @throws {Error} When `skipUserSession` is set on a route that requires auth.
313
315
  */
314
- export const fetchData = async (request, response, { skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceDataFromSchema = true, bubbleErrors = false, exposeErrors = !isProduction(), generateRequestId, traceId: externalTraceId, maxBodySize, } = {}) => {
316
+ export const fetchData = async (request, response, { skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceDataFromSchema = true, bubbleErrors = false, exposeErrors = !isProduction(), traceId, trustInboundTraceparent, maxBodySize, } = {}) => {
315
317
  const singletonServices = getSingletonServices();
316
318
  const createWireServices = getCreateWireServices();
317
319
  let result;
318
320
  const pikkuRequest = request instanceof Request
319
321
  ? new PikkuFetchHTTPRequest(request, { maxBodySize })
320
322
  : request;
321
- let requestId = externalTraceId ?? null;
322
- if (!requestId) {
323
- try {
324
- requestId = pikkuRequest.header('x-request-id');
325
- }
326
- catch { }
327
- }
328
- requestId = requestId || generateRequestId?.() || createWeakUID();
329
- const scopedLogger = singletonServices.logger.scope?.(requestId) ?? singletonServices.logger;
323
+ const trace = resolveHTTPTrace(() => pikkuRequest.header('traceparent'), singletonServices.logger, { traceId, trustInboundTraceparent });
324
+ // Read at the point of logging: mesh auth can continue the caller's trace.
325
+ const scopedLogger = () => singletonServices.logger.scope?.(trace.traceId) ?? singletonServices.logger;
330
326
  const http = createHTTPWire(pikkuRequest, response);
331
327
  const apiType = http.request.method();
332
328
  const apiRoute = http.request.path();
@@ -344,7 +340,7 @@ export const fetchData = async (request, response, { skipUserSession = false, re
344
340
  response.status(204).json(undefined);
345
341
  return;
346
342
  }
347
- scopedLogger.info({
343
+ scopedLogger().info({
348
344
  message: 'Route not found',
349
345
  apiRoute,
350
346
  apiType,
@@ -356,11 +352,11 @@ export const fetchData = async (request, response, { skipUserSession = false, re
356
352
  singletonServices,
357
353
  createWireServices,
358
354
  skipUserSession,
359
- requestId,
355
+ trace,
360
356
  }, matchedRoute, http, { coerceDataFromSchema }));
361
357
  return result;
362
358
  }
363
359
  catch (e) {
364
- handleHTTPError(e, http, requestId, scopedLogger, logWarningsForStatusCodes, respondWith404, bubbleErrors, exposeErrors);
360
+ handleHTTPError(e, http, trace.traceId, scopedLogger(), logWarningsForStatusCodes, respondWith404, bubbleErrors, exposeErrors);
365
361
  }
366
362
  };
@@ -20,7 +20,7 @@ export type AssertHTTPWiringParams<In, HTTPWiring extends string> = ExtractHTTPW
20
20
  * - `coerceDataFromSchema` – coerce query and path strings to the schema's types (default `true`).
21
21
  * - `bubbleErrors` – rethrow after writing the error response.
22
22
  * - `exposeErrors` – include messages and stacks of 5xx errors; ignored in production.
23
- * - `generateRequestId` – produce a trace ID when neither `traceId` nor `x-request-id` is present.
23
+ * - `trustInboundTraceparent` – continue the trace named by `traceparent` on every request. Off by default, because the header is caller-supplied.
24
24
  */
25
25
  export type RunHTTPWiringOptions = Partial<{
26
26
  skipUserSession: boolean;
@@ -29,8 +29,8 @@ export type RunHTTPWiringOptions = Partial<{
29
29
  coerceDataFromSchema: boolean;
30
30
  bubbleErrors: boolean;
31
31
  exposeErrors: boolean;
32
- generateRequestId: () => string;
33
- /** Pre-resolved trace ID (e.g. CF-Ray). Falls back to x-request-id header or generated ID. */
32
+ trustInboundTraceparent: boolean;
33
+ /** A trace ID the runtime already holds: 32 lowercase hex, anything else is ignored. */
34
34
  traceId: string;
35
35
  /** Maximum request body size in bytes, applied when pikku wraps a fetch `Request`. */
36
36
  maxBodySize: number;