@hashspan/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ Transaction lifecycle tracing for the on-chain actions of AI agents, built on Op
9
9
  revert reason. It carries a span link to its `send` span.
10
10
 
11
11
  The core is library-agnostic and read-only: it never signs, sends or fetches anything. Adapters such as
12
- [`@hashspan/viem`](https://github.com/selimaytac/hashspan/tree/main/packages/viem) call it for you. Use the core directly to instrument any other send path.
12
+ [`@hashspan/viem`](https://github.com/selimaytac/hashspan/tree/@hashspan/core@0.3.0/packages/viem) call it for you. Use the core directly to instrument any other send path.
13
13
 
14
14
  ## Install
15
15
 
@@ -17,7 +17,9 @@ The core is library-agnostic and read-only: it never signs, sends or fetches any
17
17
  npm install @hashspan/core @opentelemetry/api
18
18
  ```
19
19
 
20
- `@opentelemetry/api` is the only peer dependency. Bring your own OpenTelemetry SDK and exporter.
20
+ `@opentelemetry/api` is the only peer dependency. Bring your own OpenTelemetry SDK and exporter. Requires Node.js 22.3
21
+ or later; in other runtimes, `hashed` address mode needs a custom `hash` function and otherwise records no addresses,
22
+ with a `diag` warning.
21
23
 
22
24
  ## Usage
23
25
 
@@ -55,9 +57,13 @@ confirm.end({
55
57
  share one confirm span. A receipt from any of them ends it; a timeout or failure ends it once every caller gave up.
56
58
  End every handle you start, since an open handle keeps the shared span open.
57
59
 
60
+ `send.fail(error, endTime, { errorType })` records a library's machine-readable error code as `error.type` instead
61
+ of the error's class name, if it is a short identifier (`[A-Za-z0-9_.-]`, at most 64 characters); `exception.type`
62
+ stays the class name.
63
+
58
64
  Both calls accept an explicit parent `Context` as a second argument. An integration that learns about a call only
59
65
  after it started can record it after the fact: pass `startTime` in the input and the end time as the last argument
60
- of the handle method, e.g. `send.end(hash, endTime)` ([ADR 0009](https://github.com/selimaytac/hashspan/blob/main/docs/adr/0009-telemetry-off-the-call-path.md)). Every method is safe to call: failures inside
66
+ of the handle method, e.g. `send.end(hash, endTime)` ([ADR 0009](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.3.0/docs/adr/0009-telemetry-off-the-call-path.md)). Every method is safe to call: failures inside
61
67
  the instrumentation are reported through `diag` and never thrown into your code.
62
68
 
63
69
  ## Options
@@ -66,9 +72,10 @@ the instrumentation are reported through `diag` and never thrown into your code.
66
72
  |---|---|---|
67
73
  | `tracerProvider` | global provider | Tracer provider to use |
68
74
  | `address` | `'raw'` | `'raw'`, `'hashed'`, `'off'`, or `{ mode: 'hashed', hash: (address) => string }` |
69
- | `errorMessages` | `'off'` | What failed spans record about the error: `'off'` (type only), `'sanitized'` (first line, addresses per `address` mode, calldata removed; in `hashed` and `off` mode any hex longer than an address) or `'raw'` (full message and stack trace) |
75
+ | `errorMessages` | `'off'` | What failed spans record about the error: `'off'` (type only), `'sanitized'` (first line, addresses per `address` mode, calldata removed; in `hashed` and `off` mode any hex longer than an address) or `'raw'` (full message and stack trace). `'raw'` can record RPC URLs that include API keys, as some libraries put the request URL in the message; `'sanitized'` keeps only the first line (viem puts the URL on a later line), which is best effort. See [ADR 0006](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.3.0/docs/adr/0006-error-privacy.md) |
70
76
  | `recordFunctionArguments` | `false` | Record `functionArguments` as a JSON array in `blockchain.contract.function.arguments`: bigints as decimal strings, addresses per `address` mode (longer hex values become `<hex>` in `hashed` and `off` mode), at most 4096 characters. Reads only own enumerable data properties: `toJSON()` and getters are never called, so a `Date` records as `{}`; a Proxy's traps still run |
71
- | `agent` | none | Fallback `{ id, name }`; Baggage entries `gen_ai.agent.id` / `gen_ai.agent.name` take precedence |
77
+ | `agent` | none | Agent `{ id, name }`; a field set here always wins, unset fields come from the Baggage entries `gen_ai.agent.id` / `gen_ai.agent.name` |
78
+ | `agentFromBaggage` | `true` | Read agent identity fields that `agent` leaves unset from Baggage; set to `false` in services that accept requests from outside their trust boundary |
72
79
  | `redact` | none | `(attributes) => attributes`, runs last on every attribute set, including exception event attributes; if it throws, only non-sensitive identifiers are kept |
73
80
  | `linkTtlMs` | `600000` | How long a sent transaction can be linked from its confirmation |
74
81
  | `maxTrackedTransactions` | `10000` | Upper bound on transactions kept for linking |
@@ -79,7 +86,7 @@ Chain id, transaction hash, sender/recipient (per `address` mode), value, nonce,
79
86
  on confirmation, status, block number, gas used, effective gas price, L1 fee, total fee and revert reason. Decoded
80
87
  call arguments are recorded only with `recordFunctionArguments`, and error messages only with `errorMessages`.
81
88
  Attribute definitions:
82
- [docs/semconv.md](https://github.com/selimaytac/hashspan/blob/main/docs/semconv.md).
89
+ [docs/semconv.md](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.3.0/docs/semconv.md).
83
90
 
84
91
  ## Privacy notes
85
92
 
@@ -92,6 +99,9 @@ Attribute definitions:
92
99
  party APIs. Put only identifiers there that may leave your system, such as an opaque agent id. For identifiers
93
100
  that must stay internal, use the tracker's static `agent` option instead, which is recorded on spans but never
94
101
  propagated, or strip the entries before outbound calls.
102
+ - **Inbound Baggage can claim an identity.** A caller can send Baggage entries with any agent id. A field set in the
103
+ `agent` option cannot be overridden that way; to ignore identity from Baggage entirely, set `agentFromBaggage: false`
104
+ ([ADR 0011](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.3.0/docs/adr/0011-agent-identity-precedence.md)).
95
105
  - The redaction hook (`redact`) runs last on every attribute set and on exception attributes; use it for anything
96
106
  else your policy forbids.
97
107
 
package/dist/index.cjs CHANGED
@@ -3,11 +3,14 @@ let _opentelemetry_api = require("@opentelemetry/api");
3
3
  //#region src/agent.ts
4
4
  const ATTR_GEN_AI_AGENT_ID = "gen_ai.agent.id";
5
5
  const ATTR_GEN_AI_AGENT_NAME = "gen_ai.agent.name";
6
- /** Agent identity from baggage (preferred) or the static fallback, as GenAI attributes. */
7
- function agentAttributes(ctx, fallback) {
8
- const baggage = _opentelemetry_api.propagation.getBaggage(ctx);
9
- const id = baggage?.getEntry("gen_ai.agent.id")?.value ?? fallback?.id;
10
- const name = baggage?.getEntry("gen_ai.agent.name")?.value ?? fallback?.name;
6
+ /**
7
+ * Agent identity as GenAI attributes. A field set in the static identity always wins; Baggage, which a remote caller
8
+ * can set, only fills fields it leaves unset, and is not read at all with `fromBaggage` false (docs/adr/0011).
9
+ */
10
+ function agentAttributes(ctx, identity, fromBaggage = true) {
11
+ const baggage = fromBaggage ? _opentelemetry_api.propagation.getBaggage(ctx) : void 0;
12
+ const id = identity?.id ?? baggage?.getEntry("gen_ai.agent.id")?.value;
13
+ const name = identity?.name ?? baggage?.getEntry("gen_ai.agent.name")?.value;
11
14
  const attributes = {};
12
15
  if (id !== void 0) attributes[ATTR_GEN_AI_AGENT_ID] = id;
13
16
  if (name !== void 0) attributes[ATTR_GEN_AI_AGENT_NAME] = name;
@@ -299,7 +302,7 @@ function resolveErrorMessageMode(mode) {
299
302
  }
300
303
  //#endregion
301
304
  //#region src/version.ts
302
- const VERSION = "0.1.0";
305
+ const VERSION = "0.3.0";
303
306
  //#endregion
304
307
  //#region src/tracker.ts
305
308
  const INSTRUMENTATION_NAME = "@hashspan/core";
@@ -354,6 +357,15 @@ function toInt(value) {
354
357
  function errorType(error) {
355
358
  return error instanceof Error && error.name ? error.name : ERROR_TYPE_VALUE_OTHER;
356
359
  }
360
+ const ERROR_TYPE_OVERRIDE = /^[A-Za-z0-9_.-]{1,64}$/;
361
+ /** The `error.type` for a failure: an adapter's override when it is a short identifier, else the class name. */
362
+ function reportedErrorType(error, options) {
363
+ const override = options?.errorType;
364
+ if (override === void 0) return errorType(error);
365
+ if (typeof override === "string" && ERROR_TYPE_OVERRIDE.test(override)) return override;
366
+ _opentelemetry_api.diag.debug("hashspan: ignoring an error type that is not a short identifier");
367
+ return errorType(error);
368
+ }
357
369
  function createTxTracker(options = {}) {
358
370
  const links = new LinkStore({
359
371
  ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
@@ -403,12 +415,15 @@ function createTxTracker(options = {}) {
403
415
  if (error instanceof Error && error.stack) attributes[ATTR_EXCEPTION_STACKTRACE] = error.stack;
404
416
  return attributes;
405
417
  };
406
- /** Error names are free text too: they follow the address mode and pass through the redaction hook. */
407
- const markError = (span, errorName, error) => {
418
+ /**
419
+ * Error names are free text too: they follow the address mode and pass through the redaction hook.
420
+ * `exceptionName` is the class name for `exception.type` when `errorName` is an adapter's error type.
421
+ */
422
+ const markError = (span, errorName, error, exceptionName = errorName) => {
408
423
  const type = formatAddressesIn(errorName, formatAddress);
409
424
  let message;
410
425
  if (error !== void 0) {
411
- const exception = redact(exceptionAttributes(type, error));
426
+ const exception = redact(exceptionAttributes(formatAddressesIn(exceptionName, formatAddress), error));
412
427
  span.addEvent(EXCEPTION_EVENT, exception);
413
428
  const recorded = exception[ATTR_EXCEPTION_MESSAGE];
414
429
  if (typeof recorded === "string") message = recorded;
@@ -443,7 +458,7 @@ function createTxTracker(options = {}) {
443
458
  [ATTR_BLOCKCHAIN_SYSTEM]: "evm",
444
459
  [ATTR_BLOCKCHAIN_CHAIN_ID]: chainId,
445
460
  [ATTR_BLOCKCHAIN_OPERATION_NAME]: operation,
446
- ...agentAttributes(ctx, options.agent)
461
+ ...agentAttributes(ctx, options.agent, options.agentFromBaggage !== false)
447
462
  });
448
463
  const startSend = (input, parentCtx) => {
449
464
  const parent = parentCtx ?? _opentelemetry_api.context.active();
@@ -473,7 +488,7 @@ function createTxTracker(options = {}) {
473
488
  });
474
489
  span.setAttributes(redact({ [ATTR_BLOCKCHAIN_TX_HASH]: hash }));
475
490
  }, endTime),
476
- fail: (error, endTime) => finish("record send failure", () => markError(span, errorType(error), error), endTime)
491
+ fail: (error, endTime, options) => finish("record send failure", () => markError(span, reportedErrorType(error, options), error, errorType(error)), endTime)
477
492
  };
478
493
  };
479
494
  const receiptAttributes = (receipt) => {
package/dist/index.d.cts CHANGED
@@ -38,8 +38,17 @@ interface TxTrackerOptions {
38
38
  * text; addresses in them follow the address mode and the redaction hook runs on them.
39
39
  */
40
40
  recordFunctionArguments?: boolean | undefined;
41
- /** Fallback agent identity. Baggage entries `gen_ai.agent.id` / `gen_ai.agent.name` take precedence. */
41
+ /**
42
+ * Agent identity. A field set here always wins; fields left unset are taken from the Baggage entries
43
+ * `gen_ai.agent.id` / `gen_ai.agent.name` unless `agentFromBaggage` is false (docs/adr/0011).
44
+ */
42
45
  agent?: AgentIdentity | undefined;
46
+ /**
47
+ * Read agent identity fields that `agent` leaves unset from Baggage. Default: true. Baggage travels with requests
48
+ * between services, so a remote caller can set it; services that accept requests from outside their trust boundary
49
+ * should set this to false.
50
+ */
51
+ agentFromBaggage?: boolean | undefined;
43
52
  /**
44
53
  * Runs last on every attribute set and returns the attributes to record.
45
54
  * If it throws, only non-sensitive identifiers (system, chain id, operation, hash) are recorded.
@@ -73,7 +82,16 @@ interface SendHandle {
73
82
  /** Ends the send span successfully once the transaction hash is known; `endTime` defaults to now. */
74
83
  end(hash: string, endTime?: TimeInput): void;
75
84
  /** Ends the send span with an error (signing, simulation or broadcast failure); `endTime` defaults to now. */
76
- fail(error: unknown, endTime?: TimeInput): void;
85
+ fail(error: unknown, endTime?: TimeInput, options?: FailOptions): void;
86
+ }
87
+ interface FailOptions {
88
+ /**
89
+ * `error.type` to record instead of the error's class name, for adapters whose library reports a stable,
90
+ * machine-readable error code (for example a wallet API's error type). Recorded only if it matches
91
+ * `/^[A-Za-z0-9_.-]{1,64}$/`, so that the attribute keeps a bounded set of values; otherwise the class name is
92
+ * recorded. `exception.type` is always the class name.
93
+ */
94
+ errorType?: string | undefined;
77
95
  }
78
96
  interface ConfirmInput {
79
97
  chainId: number;
@@ -187,4 +205,4 @@ export declare function createTxTracker(options?: TxTrackerOptions): TxTracker;
187
205
  //#region src/version.d.ts
188
206
  export declare const VERSION: string;
189
207
  //#endregion
190
- export type { AddressMode, AddressOptions, AgentIdentity, ConfirmHandle, ConfirmInput, ErrorMessageMode, ReceiptLike, ReplacementReason, SendHandle, SendInput, TxTracker, TxTrackerOptions };
208
+ export type { AddressMode, AddressOptions, AgentIdentity, ConfirmHandle, ConfirmInput, ErrorMessageMode, FailOptions, ReceiptLike, ReplacementReason, SendHandle, SendInput, TxTracker, TxTrackerOptions };
package/dist/index.d.mts CHANGED
@@ -38,8 +38,17 @@ interface TxTrackerOptions {
38
38
  * text; addresses in them follow the address mode and the redaction hook runs on them.
39
39
  */
40
40
  recordFunctionArguments?: boolean | undefined;
41
- /** Fallback agent identity. Baggage entries `gen_ai.agent.id` / `gen_ai.agent.name` take precedence. */
41
+ /**
42
+ * Agent identity. A field set here always wins; fields left unset are taken from the Baggage entries
43
+ * `gen_ai.agent.id` / `gen_ai.agent.name` unless `agentFromBaggage` is false (docs/adr/0011).
44
+ */
42
45
  agent?: AgentIdentity | undefined;
46
+ /**
47
+ * Read agent identity fields that `agent` leaves unset from Baggage. Default: true. Baggage travels with requests
48
+ * between services, so a remote caller can set it; services that accept requests from outside their trust boundary
49
+ * should set this to false.
50
+ */
51
+ agentFromBaggage?: boolean | undefined;
43
52
  /**
44
53
  * Runs last on every attribute set and returns the attributes to record.
45
54
  * If it throws, only non-sensitive identifiers (system, chain id, operation, hash) are recorded.
@@ -73,7 +82,16 @@ interface SendHandle {
73
82
  /** Ends the send span successfully once the transaction hash is known; `endTime` defaults to now. */
74
83
  end(hash: string, endTime?: TimeInput): void;
75
84
  /** Ends the send span with an error (signing, simulation or broadcast failure); `endTime` defaults to now. */
76
- fail(error: unknown, endTime?: TimeInput): void;
85
+ fail(error: unknown, endTime?: TimeInput, options?: FailOptions): void;
86
+ }
87
+ interface FailOptions {
88
+ /**
89
+ * `error.type` to record instead of the error's class name, for adapters whose library reports a stable,
90
+ * machine-readable error code (for example a wallet API's error type). Recorded only if it matches
91
+ * `/^[A-Za-z0-9_.-]{1,64}$/`, so that the attribute keeps a bounded set of values; otherwise the class name is
92
+ * recorded. `exception.type` is always the class name.
93
+ */
94
+ errorType?: string | undefined;
77
95
  }
78
96
  interface ConfirmInput {
79
97
  chainId: number;
@@ -187,4 +205,4 @@ export declare function createTxTracker(options?: TxTrackerOptions): TxTracker;
187
205
  //#region src/version.d.ts
188
206
  export declare const VERSION: string;
189
207
  //#endregion
190
- export type { AddressMode, AddressOptions, AgentIdentity, ConfirmHandle, ConfirmInput, ErrorMessageMode, ReceiptLike, ReplacementReason, SendHandle, SendInput, TxTracker, TxTrackerOptions };
208
+ export type { AddressMode, AddressOptions, AgentIdentity, ConfirmHandle, ConfirmInput, ErrorMessageMode, FailOptions, ReceiptLike, ReplacementReason, SendHandle, SendInput, TxTracker, TxTrackerOptions };
package/dist/index.mjs CHANGED
@@ -2,11 +2,14 @@ import { SpanKind, SpanStatusCode, context, diag, propagation, trace } from "@op
2
2
  //#region src/agent.ts
3
3
  const ATTR_GEN_AI_AGENT_ID = "gen_ai.agent.id";
4
4
  const ATTR_GEN_AI_AGENT_NAME = "gen_ai.agent.name";
5
- /** Agent identity from baggage (preferred) or the static fallback, as GenAI attributes. */
6
- function agentAttributes(ctx, fallback) {
7
- const baggage = propagation.getBaggage(ctx);
8
- const id = baggage?.getEntry("gen_ai.agent.id")?.value ?? fallback?.id;
9
- const name = baggage?.getEntry("gen_ai.agent.name")?.value ?? fallback?.name;
5
+ /**
6
+ * Agent identity as GenAI attributes. A field set in the static identity always wins; Baggage, which a remote caller
7
+ * can set, only fills fields it leaves unset, and is not read at all with `fromBaggage` false (docs/adr/0011).
8
+ */
9
+ function agentAttributes(ctx, identity, fromBaggage = true) {
10
+ const baggage = fromBaggage ? propagation.getBaggage(ctx) : void 0;
11
+ const id = identity?.id ?? baggage?.getEntry("gen_ai.agent.id")?.value;
12
+ const name = identity?.name ?? baggage?.getEntry("gen_ai.agent.name")?.value;
10
13
  const attributes = {};
11
14
  if (id !== void 0) attributes[ATTR_GEN_AI_AGENT_ID] = id;
12
15
  if (name !== void 0) attributes[ATTR_GEN_AI_AGENT_NAME] = name;
@@ -298,7 +301,7 @@ function resolveErrorMessageMode(mode) {
298
301
  }
299
302
  //#endregion
300
303
  //#region src/version.ts
301
- const VERSION = "0.1.0";
304
+ const VERSION = "0.3.0";
302
305
  //#endregion
303
306
  //#region src/tracker.ts
304
307
  const INSTRUMENTATION_NAME = "@hashspan/core";
@@ -353,6 +356,15 @@ function toInt(value) {
353
356
  function errorType(error) {
354
357
  return error instanceof Error && error.name ? error.name : ERROR_TYPE_VALUE_OTHER;
355
358
  }
359
+ const ERROR_TYPE_OVERRIDE = /^[A-Za-z0-9_.-]{1,64}$/;
360
+ /** The `error.type` for a failure: an adapter's override when it is a short identifier, else the class name. */
361
+ function reportedErrorType(error, options) {
362
+ const override = options?.errorType;
363
+ if (override === void 0) return errorType(error);
364
+ if (typeof override === "string" && ERROR_TYPE_OVERRIDE.test(override)) return override;
365
+ diag.debug("hashspan: ignoring an error type that is not a short identifier");
366
+ return errorType(error);
367
+ }
356
368
  function createTxTracker(options = {}) {
357
369
  const links = new LinkStore({
358
370
  ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
@@ -402,12 +414,15 @@ function createTxTracker(options = {}) {
402
414
  if (error instanceof Error && error.stack) attributes[ATTR_EXCEPTION_STACKTRACE] = error.stack;
403
415
  return attributes;
404
416
  };
405
- /** Error names are free text too: they follow the address mode and pass through the redaction hook. */
406
- const markError = (span, errorName, error) => {
417
+ /**
418
+ * Error names are free text too: they follow the address mode and pass through the redaction hook.
419
+ * `exceptionName` is the class name for `exception.type` when `errorName` is an adapter's error type.
420
+ */
421
+ const markError = (span, errorName, error, exceptionName = errorName) => {
407
422
  const type = formatAddressesIn(errorName, formatAddress);
408
423
  let message;
409
424
  if (error !== void 0) {
410
- const exception = redact(exceptionAttributes(type, error));
425
+ const exception = redact(exceptionAttributes(formatAddressesIn(exceptionName, formatAddress), error));
411
426
  span.addEvent(EXCEPTION_EVENT, exception);
412
427
  const recorded = exception[ATTR_EXCEPTION_MESSAGE];
413
428
  if (typeof recorded === "string") message = recorded;
@@ -442,7 +457,7 @@ function createTxTracker(options = {}) {
442
457
  [ATTR_BLOCKCHAIN_SYSTEM]: "evm",
443
458
  [ATTR_BLOCKCHAIN_CHAIN_ID]: chainId,
444
459
  [ATTR_BLOCKCHAIN_OPERATION_NAME]: operation,
445
- ...agentAttributes(ctx, options.agent)
460
+ ...agentAttributes(ctx, options.agent, options.agentFromBaggage !== false)
446
461
  });
447
462
  const startSend = (input, parentCtx) => {
448
463
  const parent = parentCtx ?? context.active();
@@ -472,7 +487,7 @@ function createTxTracker(options = {}) {
472
487
  });
473
488
  span.setAttributes(redact({ [ATTR_BLOCKCHAIN_TX_HASH]: hash }));
474
489
  }, endTime),
475
- fail: (error, endTime) => finish("record send failure", () => markError(span, errorType(error), error), endTime)
490
+ fail: (error, endTime, options) => finish("record send failure", () => markError(span, reportedErrorType(error, options), error, errorType(error)), endTime)
476
491
  };
477
492
  };
478
493
  const receiptAttributes = (receipt) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hashspan/core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Transaction lifecycle tracing for on-chain actions of AI agents, built on OpenTelemetry.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Selim Aytac",
@@ -57,7 +57,7 @@
57
57
  "@opentelemetry/sdk-trace-node": "^2.11.0"
58
58
  },
59
59
  "engines": {
60
- "node": ">=18"
60
+ "node": ">=22.3.0"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsdown",