@totalreclaw/totalreclaw 3.3.1-rc.20 → 3.3.1-rc.21

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/index.ts CHANGED
@@ -135,6 +135,7 @@ import {
135
135
  } from './onboarding-cli.js';
136
136
  import { PluginHotCache, type HotFact } from './hot-cache-wrapper.js';
137
137
  import { CONFIG, setRecoveryPhraseOverride } from './config.js';
138
+ import { buildRelayHeaders } from './relay-headers.js';
138
139
  import {
139
140
  readBillingCache,
140
141
  writeBillingCache,
@@ -151,6 +152,7 @@ import {
151
152
  resolveOnboardingState,
152
153
  writeOnboardingState,
153
154
  readPluginVersion,
155
+ cleanupInstallStagingDirs,
154
156
  type OnboardingState,
155
157
  } from './fs-helpers.js';
156
158
  import { isRcBuild } from './qa-bug-report.js';
@@ -161,6 +163,19 @@ import { detectGatewayHost } from './gateway-url.js';
161
163
  import { validateMnemonic } from '@scure/bip39';
162
164
  import { wordlist } from '@scure/bip39/wordlists/english.js';
163
165
  import crypto from 'node:crypto';
166
+ import { createRequire } from 'node:module';
167
+ import { fileURLToPath } from 'node:url';
168
+ import * as nodePath from 'node:path';
169
+
170
+ // CJS-style require for the @totalreclaw/core WASM module. We keep this
171
+ // load path lazy (only inside getSmartImportWasm() below) so a partial
172
+ // install of the dependency tree doesn't crash module init. Bare
173
+ // `require()` is a CommonJS global and is undefined under bare Node ESM —
174
+ // the shipped `dist/index.js` declares `"type":"module"`, so calling the
175
+ // global directly emits "require is not defined" at runtime (issue #124).
176
+ // createRequire bridges the gap. Same shape as crypto.ts / lsh.ts /
177
+ // subgraph-store.ts / claims-helper.ts.
178
+ const __cjsRequire = createRequire(import.meta.url);
164
179
 
165
180
  // ---------------------------------------------------------------------------
166
181
  // OpenClaw Plugin API type (defined locally to avoid SDK dependency)
@@ -876,11 +891,10 @@ async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
876
891
  const billingUrl = CONFIG.serverUrl;
877
892
  const resp = await fetch(`${billingUrl}/v1/billing/status?wallet_address=${encodeURIComponent(walletAddr)}`, {
878
893
  method: 'GET',
879
- headers: {
894
+ headers: buildRelayHeaders({
880
895
  'Authorization': `Bearer ${authKeyHex}`,
881
896
  'Accept': 'application/json',
882
- 'X-TotalReclaw-Client': 'openclaw-plugin',
883
- },
897
+ }),
884
898
  });
885
899
  if (resp.ok) {
886
900
  const billingData = await resp.json() as Record<string, unknown>;
@@ -1453,11 +1467,11 @@ async function migrationGqlQuery<T>(
1453
1467
  authKey?: string,
1454
1468
  ): Promise<T | null> {
1455
1469
  try {
1456
- const headers: Record<string, string> = {
1470
+ const overrides: Record<string, string> = {
1457
1471
  'Content-Type': 'application/json',
1458
- 'X-TotalReclaw-Client': 'openclaw-plugin',
1459
1472
  };
1460
- if (authKey) headers['Authorization'] = `Bearer ${authKey}`;
1473
+ if (authKey) overrides['Authorization'] = `Bearer ${authKey}`;
1474
+ const headers = buildRelayHeaders(overrides);
1461
1475
  const response = await fetch(endpoint, {
1462
1476
  method: 'POST',
1463
1477
  headers,
@@ -2298,10 +2312,13 @@ async function handlePluginImportFrom(
2298
2312
  // Smart Import — Two-Pass Pipeline (Profile + Triage)
2299
2313
  // ---------------------------------------------------------------------------
2300
2314
 
2301
- // Lazy-load WASM for smart import functions (same pattern as crypto.ts / subgraph-store.ts).
2315
+ // Lazy-load WASM for smart import functions (same pattern as crypto.ts /
2316
+ // subgraph-store.ts). Goes through __cjsRequire (createRequire(import.meta.url))
2317
+ // declared at the top of the file — bare `require()` is undefined under
2318
+ // pure-ESM Node, see issue #124.
2302
2319
  let _smartImportWasm: typeof import('@totalreclaw/core') | null = null;
2303
2320
  function getSmartImportWasm() {
2304
- if (!_smartImportWasm) _smartImportWasm = require('@totalreclaw/core');
2321
+ if (!_smartImportWasm) _smartImportWasm = __cjsRequire('@totalreclaw/core');
2305
2322
  return _smartImportWasm;
2306
2323
  }
2307
2324
 
@@ -2840,17 +2857,37 @@ const plugin = {
2840
2857
  // the loopback callsite if package.json read fails.
2841
2858
  let pluginVersion: string | null = null;
2842
2859
  try {
2843
- // `import.meta.url` is ESM-only; fallback to `__dirname` for the CJS
2844
- // build path. `require` comes from Node core and is available in both
2845
- // module formats. `fileURLToPath` / `path.dirname` are pure-sync.
2846
- const url = require('node:url') as typeof import('node:url');
2847
- const nodePath = require('node:path') as typeof import('node:path');
2848
- const pluginDir = nodePath.dirname(url.fileURLToPath(import.meta.url));
2860
+ // Resolve our own dist/ directory so `readPluginVersion` can locate
2861
+ // package.json. We use `import.meta.url` + ESM-static stdlib imports
2862
+ // (`fileURLToPath` from `node:url`, `nodePath.dirname` from `node:path`,
2863
+ // both imported at the top of this file). Earlier shape used inline
2864
+ // `require('node:url')` undefined under bare-ESM Node, broke the
2865
+ // before_agent_start hook in the published rc.20 bundle (issue #124).
2866
+ const pluginDir = nodePath.dirname(fileURLToPath(import.meta.url));
2849
2867
  pluginVersion = readPluginVersion(pluginDir);
2850
2868
  rcMode = isRcBuild(pluginVersion);
2851
2869
  if (rcMode) {
2852
2870
  api.logger.info(`TotalReclaw: RC build detected (version=${pluginVersion}). RC-gated tools will be registered.`);
2853
2871
  }
2872
+
2873
+ // 3.3.1-rc.21 (issue #126 — rc.20 finding F3): clean up
2874
+ // `.openclaw-install-stage-*` siblings left behind by an interrupted
2875
+ // `openclaw plugins install` run. Without cleanup, OpenClaw's plugin
2876
+ // loader auto-discovers the orphan directory on the next gateway
2877
+ // start and registers a duplicate `totalreclaw` plugin (duplicate
2878
+ // hooks, duplicate tools, "duplicate-plugin-id" warning every cycle).
2879
+ // Best-effort — never throws on permission / race failures.
2880
+ try {
2881
+ const removed = cleanupInstallStagingDirs(pluginDir);
2882
+ if (removed.length > 0) {
2883
+ api.logger.info(
2884
+ `TotalReclaw: removed ${removed.length} stale install-staging dir(s) from prior interrupted install: ${removed.join(', ')}`,
2885
+ );
2886
+ }
2887
+ } catch {
2888
+ // Best-effort — already swallowed inside the helper, but keep this
2889
+ // outer try as belt-and-braces against future helper changes.
2890
+ }
2854
2891
  } catch {
2855
2892
  rcMode = false;
2856
2893
  }
@@ -3014,8 +3051,30 @@ const plugin = {
3014
3051
  // Write credentials.json + flip state to 'active' via
3015
3052
  // fs-helpers. This centralizes disk I/O off the
3016
3053
  // pair-http surface (scanner isolation).
3054
+ //
3055
+ // 3.3.1 (internal#130) — derive + persist the Smart Account
3056
+ // address right here so the user can see their scope address
3057
+ // immediately after pair, without waiting for a first chain
3058
+ // write. SA derivation runs locally (WASM deriveEoa + factory
3059
+ // getAddress eth_call); the mnemonic NEVER crosses any new
3060
+ // boundary — it's already on disk in credentials.json and is
3061
+ // consumed by the same `deriveSmartAccountAddress` call the
3062
+ // store/search paths use. Only the derived public address is
3063
+ // persisted to credentials.json (`scope_address`).
3064
+ let scopeAddress: string | undefined;
3065
+ try {
3066
+ scopeAddress = await deriveSmartAccountAddress(mnemonic, CONFIG.chainId);
3067
+ } catch (err) {
3068
+ // Best-effort. If chain RPC is unreachable at pair time, the
3069
+ // status tool re-tries derivation lazily on next call —
3070
+ // fall through and write credentials.json without it.
3071
+ api.logger.warn(
3072
+ `pair: scope_address derivation failed (will retry lazily): ${err instanceof Error ? err.message : String(err)}`,
3073
+ );
3074
+ }
3017
3075
  const creds = loadCredentialsJson(CREDENTIALS_PATH) ?? {};
3018
- const next = { ...creds, mnemonic };
3076
+ const next: typeof creds = { ...creds, mnemonic };
3077
+ if (scopeAddress) next.scope_address = scopeAddress;
3019
3078
  if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
3020
3079
  return { state: 'error', error: 'credentials_write_failed' };
3021
3080
  }
@@ -3803,11 +3862,10 @@ const plugin = {
3803
3862
 
3804
3863
  const res = await fetch(`${relayUrl}/v1/subgraph`, {
3805
3864
  method: 'POST',
3806
- headers: {
3865
+ headers: buildRelayHeaders({
3807
3866
  'Content-Type': 'application/json',
3808
- 'X-TotalReclaw-Client': 'openclaw-plugin',
3809
3867
  ...(authKeyHex ? { Authorization: `Bearer ${authKeyHex}` } : {}),
3810
- },
3868
+ }),
3811
3869
  body: JSON.stringify({ query, variables }),
3812
3870
  });
3813
3871
 
@@ -3943,11 +4001,10 @@ const plugin = {
3943
4001
  const walletAddr = subgraphOwner || userId || '';
3944
4002
  const response = await fetch(`${serverUrl}/v1/billing/status?wallet_address=${encodeURIComponent(walletAddr)}`, {
3945
4003
  method: 'GET',
3946
- headers: {
4004
+ headers: buildRelayHeaders({
3947
4005
  'Authorization': `Bearer ${authKeyHex}`,
3948
4006
  'Accept': 'application/json',
3949
- 'X-TotalReclaw-Client': 'openclaw-plugin',
3950
- },
4007
+ }),
3951
4008
  });
3952
4009
 
3953
4010
  if (!response.ok) {
@@ -3972,6 +4029,37 @@ const plugin = {
3972
4029
  checked_at: Date.now(),
3973
4030
  });
3974
4031
 
4032
+ // 3.3.1 (internal#130) — surface the Smart Account / scope
4033
+ // address so the user can do subgraph queries, BaseScan
4034
+ // lookups, and cross-client portability checks BEFORE any
4035
+ // chain write completes. Resolution priority:
4036
+ // 1. In-memory `subgraphOwner` (already derived earlier).
4037
+ // 2. credentials.json `scope_address` (persisted at pair).
4038
+ // 3. Lazy derive now from the loaded mnemonic + cache it
4039
+ // back to credentials.json so the next call is free.
4040
+ let scopeAddress: string | undefined =
4041
+ subgraphOwner ?? undefined;
4042
+ if (!scopeAddress) {
4043
+ try {
4044
+ const credsCache = loadCredentialsJson(CREDENTIALS_PATH);
4045
+ if (credsCache?.scope_address && typeof credsCache.scope_address === 'string') {
4046
+ scopeAddress = credsCache.scope_address;
4047
+ } else if (credsCache?.mnemonic && typeof credsCache.mnemonic === 'string') {
4048
+ scopeAddress = await deriveSmartAccountAddress(
4049
+ credsCache.mnemonic,
4050
+ CONFIG.chainId,
4051
+ );
4052
+ if (scopeAddress) {
4053
+ writeCredentialsJson(CREDENTIALS_PATH, { ...credsCache, scope_address: scopeAddress });
4054
+ }
4055
+ }
4056
+ } catch (deriveErr) {
4057
+ api.logger.warn(
4058
+ `totalreclaw_status: scope_address lookup failed: ${deriveErr instanceof Error ? deriveErr.message : String(deriveErr)}`,
4059
+ );
4060
+ }
4061
+ }
4062
+
3975
4063
  const tierLabel = tier === 'pro' ? 'Pro' : 'Free';
3976
4064
  const lines: string[] = [
3977
4065
  `Tier: ${tierLabel}`,
@@ -3980,13 +4068,21 @@ const plugin = {
3980
4068
  if (freeWritesResetAt) {
3981
4069
  lines.push(`Resets: ${new Date(freeWritesResetAt).toLocaleDateString()}`);
3982
4070
  }
4071
+ if (scopeAddress) {
4072
+ lines.push(`Smart Account: ${scopeAddress}`);
4073
+ }
3983
4074
  if (tier !== 'pro') {
3984
4075
  lines.push(`Pricing: https://totalreclaw.xyz/pricing`);
3985
4076
  }
3986
4077
 
3987
4078
  return {
3988
4079
  content: [{ type: 'text', text: lines.join('\n') }],
3989
- details: { tier, free_writes_used: freeWritesUsed, free_writes_limit: freeWritesLimit },
4080
+ details: {
4081
+ tier,
4082
+ free_writes_used: freeWritesUsed,
4083
+ free_writes_limit: freeWritesLimit,
4084
+ scope_address: scopeAddress,
4085
+ },
3990
4086
  };
3991
4087
  } catch (err: unknown) {
3992
4088
  const message = err instanceof Error ? err.message : String(err);
@@ -4709,11 +4805,10 @@ const plugin = {
4709
4805
 
4710
4806
  const response = await fetch(`${serverUrl}/v1/billing/checkout`, {
4711
4807
  method: 'POST',
4712
- headers: {
4808
+ headers: buildRelayHeaders({
4713
4809
  'Authorization': `Bearer ${authKeyHex}`,
4714
4810
  'Content-Type': 'application/json',
4715
- 'X-TotalReclaw-Client': 'openclaw-plugin',
4716
- },
4811
+ }),
4717
4812
  body: JSON.stringify({
4718
4813
  wallet_address: walletAddr,
4719
4814
  tier: 'pro',
@@ -4797,11 +4892,10 @@ const plugin = {
4797
4892
  `${serverUrl}/v1/billing/status?wallet_address=${encodeURIComponent(subgraphOwner)}`,
4798
4893
  {
4799
4894
  method: 'GET',
4800
- headers: {
4895
+ headers: buildRelayHeaders({
4801
4896
  'Authorization': `Bearer ${authKeyHex}`,
4802
4897
  'Content-Type': 'application/json',
4803
- 'X-TotalReclaw-Client': 'openclaw-plugin',
4804
- },
4898
+ }),
4805
4899
  },
4806
4900
  );
4807
4901
  if (!billingResp.ok) {
@@ -5080,9 +5174,27 @@ const plugin = {
5080
5174
  validateMnemonic(p, wordlist),
5081
5175
  completePairing: async ({ mnemonic }) => {
5082
5176
  try {
5177
+ // 3.3.1 (internal#130) — derive + persist the
5178
+ // Smart Account address now so the user can see
5179
+ // it immediately after pair, before any chain
5180
+ // write. Mnemonic stays in this scope (already
5181
+ // on disk in credentials.json); only the
5182
+ // derived public scope_address is added.
5183
+ let scopeAddress: string | undefined;
5184
+ try {
5185
+ scopeAddress = await deriveSmartAccountAddress(
5186
+ mnemonic,
5187
+ CONFIG.chainId,
5188
+ );
5189
+ } catch (deriveErr) {
5190
+ api.logger.warn(
5191
+ `totalreclaw_pair(relay): scope_address derivation failed (will retry lazily): ${deriveErr instanceof Error ? deriveErr.message : String(deriveErr)}`,
5192
+ );
5193
+ }
5083
5194
  const creds =
5084
5195
  loadCredentialsJson(CREDENTIALS_PATH) ?? {};
5085
- const next = { ...creds, mnemonic };
5196
+ const next: typeof creds = { ...creds, mnemonic };
5197
+ if (scopeAddress) next.scope_address = scopeAddress;
5086
5198
  if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
5087
5199
  return { state: 'error', error: 'credentials_write_failed' };
5088
5200
  }
@@ -5094,7 +5206,7 @@ const plugin = {
5094
5206
  version: pluginVersion ?? '3.3.0',
5095
5207
  });
5096
5208
  api.logger.info(
5097
- `totalreclaw_pair(relay): session ${remoteSession.token.slice(0, 8)}… completed; credentials written`,
5209
+ `totalreclaw_pair(relay): session ${remoteSession.token.slice(0, 8)}… completed; credentials written${scopeAddress ? ` (scope_address=${scopeAddress})` : ''}`,
5098
5210
  );
5099
5211
  return { state: 'active' };
5100
5212
  } catch (err: unknown) {
@@ -5234,11 +5346,20 @@ const plugin = {
5234
5346
  // declared. If the agent then reports the tool is missing from its
5235
5347
  // tool list, the gap is upstream OpenClaw tool propagation, not our
5236
5348
  // plugin — see issue #110 fix 3 + PR #102 (CLI fallback).
5237
- api.logger.info(
5238
- 'TotalReclaw: registerTool(totalreclaw_pair) returned. If the agent does not see it in its tool list ' +
5239
- 'after gateway restart, the issue is upstream tool injection (containerized agents) — fall back to ' +
5240
- '`openclaw totalreclaw pair generate --url-pin-only` (PR #102) or `openclaw totalreclaw onboard --pair-only`.',
5241
- );
5349
+ //
5350
+ // 3.3.1-rc.21 (issue #128): the breadcrumb is debug-grade it was
5351
+ // bleeding into `openclaw agent --json` stdout, breaking programmatic
5352
+ // parsers that expect the JSON-RPC body to be the only thing on the
5353
+ // wire. Gated behind `TOTALRECLAW_VERBOSE_REGISTER` (or the general
5354
+ // `TOTALRECLAW_DEBUG` toggle) so ops can opt back in when chasing
5355
+ // a tool-injection regression. Default OFF — clean stdout for users.
5356
+ if (CONFIG.verboseRegister) {
5357
+ api.logger.info(
5358
+ 'TotalReclaw: registerTool(totalreclaw_pair) returned. If the agent does not see it in its tool list ' +
5359
+ 'after gateway restart, the issue is upstream tool injection (containerized agents) — fall back to ' +
5360
+ '`openclaw totalreclaw pair generate --url-pin-only` (PR #102) or `openclaw totalreclaw onboard --pair-only`.',
5361
+ );
5362
+ }
5242
5363
 
5243
5364
  // ---------------------------------------------------------------
5244
5365
  // Tool: totalreclaw_report_qa_bug (3.3.1-rc.3 — RC-gated)
@@ -5370,9 +5491,15 @@ const plugin = {
5370
5491
  },
5371
5492
  { name: 'totalreclaw_report_qa_bug' },
5372
5493
  );
5373
- api.logger.info(
5374
- 'totalreclaw_report_qa_bug registered (RC build this tool is hidden in stable releases).',
5375
- );
5494
+ // 3.3.1-rc.21 (issue #128): demote the registration-confirmation
5495
+ // breadcrumb to verbose-only. Same `--json` stdout pollution risk
5496
+ // as the totalreclaw_pair breadcrumb above; ops can opt back in
5497
+ // via TOTALRECLAW_VERBOSE_REGISTER / TOTALRECLAW_DEBUG.
5498
+ if (CONFIG.verboseRegister) {
5499
+ api.logger.info(
5500
+ 'totalreclaw_report_qa_bug registered (RC build — this tool is hidden in stable releases).',
5501
+ );
5502
+ }
5376
5503
  }
5377
5504
 
5378
5505
  // ---------------------------------------------------------------
@@ -5508,7 +5635,7 @@ const plugin = {
5508
5635
  const walletParam = encodeURIComponent(subgraphOwner || userId || '');
5509
5636
  const billingResp = await fetch(`${billingUrl}/v1/billing/status?wallet_address=${walletParam}`, {
5510
5637
  method: 'GET',
5511
- headers: { 'Authorization': `Bearer ${authKeyHex}`, 'Accept': 'application/json', 'X-TotalReclaw-Client': 'openclaw-plugin' },
5638
+ headers: buildRelayHeaders({ 'Authorization': `Bearer ${authKeyHex}`, 'Accept': 'application/json' }),
5512
5639
  });
5513
5640
  if (billingResp.ok) {
5514
5641
  const billingData = await billingResp.json() as Record<string, unknown>;
package/lsh.ts CHANGED
@@ -7,10 +7,15 @@
7
7
  * Default parameters: 32 bits per table, 20 tables.
8
8
  */
9
9
 
10
- // Lazy-load WASM to avoid crash when npm install hasn't finished yet.
10
+ // Lazy-load WASM via createRequire. The shipped `dist/index.js` is ESM-only
11
+ // (`"type":"module"`) so the bare `require` global is undefined at runtime.
12
+ // See issue #124 for the bug this avoids; matches the pattern in
13
+ // claims-helper / consolidation / digest-sync / pin / retype-setscope.
14
+ import { createRequire } from 'node:module';
15
+ const requireWasm = createRequire(import.meta.url);
11
16
  let _WasmLshHasher: typeof import('@totalreclaw/core')['WasmLshHasher'] | null = null;
12
17
  function getWasmLshHasher() {
13
- if (!_WasmLshHasher) _WasmLshHasher = require('@totalreclaw/core').WasmLshHasher;
18
+ if (!_WasmLshHasher) _WasmLshHasher = requireWasm('@totalreclaw/core').WasmLshHasher;
14
19
  return _WasmLshHasher!;
15
20
  }
16
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "3.3.1-rc.20",
3
+ "version": "3.3.1-rc.21",
4
4
  "description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -62,7 +62,8 @@
62
62
  "scripts": {
63
63
  "build": "rm -rf dist && tsc -p tsconfig.json --noCheck",
64
64
  "verify-tarball": "node ../scripts/verify-tarball.mjs",
65
- "test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts && npx tsx import-time-smoke.test.ts && npx tsx recall-relevance-gate.test.ts",
65
+ "test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts && npx tsx import-time-smoke.test.ts && npx tsx recall-relevance-gate.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx json-stdout-cleanliness.test.ts",
66
+ "smoke:dist": "npx tsx dist-esm-smoke.test.ts",
66
67
  "check-scanner": "node ../scripts/check-scanner.mjs",
67
68
  "prepack": "npm run build",
68
69
  "prepublishOnly": "node ../scripts/check-scanner.mjs"
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared outbound-header helper for relay calls.
3
+ *
4
+ * Centralizes the common `X-TotalReclaw-*` headers so every fetch site
5
+ * consistently tags requests with:
6
+ * - `X-TotalReclaw-Client` — caller identity (defaults to `openclaw-plugin`).
7
+ * - `X-TotalReclaw-Session` — optional QA / observability tag from
8
+ * `TOTALRECLAW_SESSION_ID`. Used by Axiom log filters and the
9
+ * `qa-totalreclaw` skill to scope log searches per QA run.
10
+ *
11
+ * Pure function — no I/O, no network. Reads `getSessionId()` (which reads the
12
+ * env var via getter so harnesses that flip the env between calls pick up
13
+ * the new value).
14
+ *
15
+ * The session-id env var was accidentally placed in the v1 REMOVED_ENV_VARS
16
+ * list and silently warned-and-dropped, breaking Axiom traceability for QA
17
+ * runs (see internal#127). This helper is the canonical re-entry point for
18
+ * the variable.
19
+ */
20
+
21
+ import { getSessionId } from './config.js';
22
+
23
+ /** Default `X-TotalReclaw-Client` value. */
24
+ export const DEFAULT_CLIENT_ID = 'openclaw-plugin';
25
+
26
+ /**
27
+ * Build the standard outbound header set.
28
+ *
29
+ * @param overrides - merge-in additional headers (`Authorization`,
30
+ * `Content-Type`, etc.); these win over the defaults.
31
+ * @param clientId - override the `X-TotalReclaw-Client` value.
32
+ *
33
+ * Always includes `X-TotalReclaw-Client`. Includes `X-TotalReclaw-Session`
34
+ * only when `TOTALRECLAW_SESSION_ID` is set + non-empty.
35
+ */
36
+ export function buildRelayHeaders(
37
+ overrides: Record<string, string> = {},
38
+ clientId: string = DEFAULT_CLIENT_ID,
39
+ ): Record<string, string> {
40
+ const headers: Record<string, string> = {
41
+ 'X-TotalReclaw-Client': clientId,
42
+ };
43
+ const sessionId = getSessionId();
44
+ if (sessionId) {
45
+ headers['X-TotalReclaw-Session'] = sessionId;
46
+ }
47
+ // Caller-supplied headers (Authorization, Content-Type, Accept, etc.) take
48
+ // precedence over the defaults but should generally not stomp the X-* tags.
49
+ return { ...headers, ...overrides };
50
+ }
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { getSubgraphConfig } from './subgraph-store.js';
24
24
  import { CONFIG } from './config.js';
25
+ import { buildRelayHeaders } from './relay-headers.js';
25
26
 
26
27
  export interface SubgraphSearchFact {
27
28
  id: string;
@@ -48,13 +49,13 @@ async function gqlQuery<T>(
48
49
  authKeyHex?: string,
49
50
  ): Promise<T | null> {
50
51
  try {
51
- const headers: Record<string, string> = {
52
+ const overrides: Record<string, string> = {
52
53
  'Content-Type': 'application/json',
53
- 'X-TotalReclaw-Client': 'openclaw-plugin',
54
54
  };
55
55
  if (authKeyHex) {
56
- headers['Authorization'] = `Bearer ${authKeyHex}`;
56
+ overrides['Authorization'] = `Bearer ${authKeyHex}`;
57
57
  }
58
+ const headers = buildRelayHeaders(overrides);
58
59
  const response = await fetch(endpoint, {
59
60
  method: 'POST',
60
61
  headers,
package/subgraph-store.ts CHANGED
@@ -10,13 +10,18 @@
10
10
  * and chain RPCs. No viem, no permissionless.
11
11
  */
12
12
 
13
- // Lazy-load WASM to avoid crash when npm install hasn't finished yet.
13
+ // Lazy-load WASM via createRequire the shipped bundle is ESM-only and
14
+ // the bare `require` global is undefined there (issue #124). Same pattern
15
+ // as crypto / lsh / claims-helper / consolidation / digest-sync.
16
+ import { createRequire } from 'node:module';
17
+ const requireWasm = createRequire(import.meta.url);
14
18
  let _wasm: typeof import('@totalreclaw/core') | null = null;
15
19
  function getWasm() {
16
- if (!_wasm) _wasm = require('@totalreclaw/core');
20
+ if (!_wasm) _wasm = requireWasm('@totalreclaw/core');
17
21
  return _wasm;
18
22
  }
19
23
  import { CONFIG } from './config.js';
24
+ import { buildRelayHeaders } from './relay-headers.js';
20
25
 
21
26
  // ---------------------------------------------------------------------------
22
27
  // Pimlico 429 retry helper
@@ -383,12 +388,12 @@ async function submitFactOnChainLocked(
383
388
  sender: string,
384
389
  ): Promise<{ txHash: string; userOpHash: string; success: boolean }> {
385
390
  const bundlerUrl = `${config.relayUrl}/v1/bundler`;
386
- const headers: Record<string, string> = {
391
+ const overrides: Record<string, string> = {
387
392
  'Content-Type': 'application/json',
388
- 'X-TotalReclaw-Client': 'openclaw-plugin',
389
393
  };
390
- if (config.authKeyHex) headers['Authorization'] = `Bearer ${config.authKeyHex}`;
391
- if (config.walletAddress) headers['X-Wallet-Address'] = config.walletAddress;
394
+ if (config.authKeyHex) overrides['Authorization'] = `Bearer ${config.authKeyHex}`;
395
+ if (config.walletAddress) overrides['X-Wallet-Address'] = config.walletAddress;
396
+ const headers = buildRelayHeaders(overrides);
392
397
 
393
398
  // Helper for JSON-RPC calls to relay bundler (with 429 retry)
394
399
  async function rpc(method: string, params: unknown[]): Promise<any> {
@@ -600,12 +605,12 @@ async function submitFactBatchOnChainLocked(
600
605
  sender: string,
601
606
  ): Promise<{ txHash: string; userOpHash: string; success: boolean; batchSize: number }> {
602
607
  const bundlerUrl = `${config.relayUrl}/v1/bundler`;
603
- const headers: Record<string, string> = {
608
+ const overrides: Record<string, string> = {
604
609
  'Content-Type': 'application/json',
605
- 'X-TotalReclaw-Client': 'openclaw-plugin',
606
610
  };
607
- if (config.authKeyHex) headers['Authorization'] = `Bearer ${config.authKeyHex}`;
608
- if (config.walletAddress) headers['X-Wallet-Address'] = config.walletAddress;
611
+ if (config.authKeyHex) overrides['Authorization'] = `Bearer ${config.authKeyHex}`;
612
+ if (config.walletAddress) overrides['X-Wallet-Address'] = config.walletAddress;
613
+ const headers = buildRelayHeaders(overrides);
609
614
 
610
615
  // Helper for JSON-RPC calls to relay bundler (with 429 retry)
611
616
  async function rpc(method: string, params: unknown[]): Promise<any> {