@langwatch/mcp-server 1.0.0 → 2.1.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.
Files changed (204) hide show
  1. package/.python-version +1 -0
  2. package/CHANGELOG.md +110 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/README.md +86 -4
  5. package/dist/{archive-project-MK3673A7.js → archive-project-W3IKI7UL.js} +5 -5
  6. package/dist/archive-run-plan-474OKHHU.js +16 -0
  7. package/dist/{archive-scenario-KATSXRIN.js → archive-scenario-V5UNYK26.js} +5 -5
  8. package/dist/archive-test-suite-O7FLDMXK.js +16 -0
  9. package/dist/{assign-prompt-tag-E3C5W6PI.js → assign-prompt-tag-Q46WRB6P.js} +4 -4
  10. package/dist/chunk-56OGTS24.js +44 -0
  11. package/dist/chunk-6DJDEXET.js +63 -0
  12. package/dist/chunk-7E4FZIYJ.js +15 -0
  13. package/dist/{chunk-J45SVWPR.js → chunk-AN4OUA4D.js} +2 -2
  14. package/dist/chunk-CKNAH5TG.js +74 -0
  15. package/dist/{chunk-CE6QMZOP.js → chunk-EYXMLE5X.js} +86 -11
  16. package/dist/{chunk-XY4STOBE.js → chunk-G4ZAZTZW.js} +2 -2
  17. package/dist/{chunk-5UWY24RP.js → chunk-GELJ2MQR.js} +12 -3
  18. package/dist/chunk-HV4DWKBI.js +46 -0
  19. package/dist/chunk-JR6DJZOU.js +49 -0
  20. package/dist/{chunk-DAIQKS27.js → chunk-JTQJRNFJ.js} +2 -2
  21. package/dist/{chunk-WWMDHZZV.js → chunk-K37ZNPXK.js} +2 -2
  22. package/dist/{chunk-EU22L52K.js → chunk-OJLYYJWI.js} +2 -2
  23. package/dist/{chunk-Y3HDZ6OW.js → chunk-QAMP3CGR.js} +2 -2
  24. package/dist/{chunk-VYPSKRPN.js → chunk-QRO35SOM.js} +347 -146
  25. package/dist/{chunk-O3LWVN67.js → chunk-RVAE2MEA.js} +2 -2
  26. package/dist/{chunk-UXXA4D6K.js → chunk-S4GVX36W.js} +2 -2
  27. package/dist/{chunk-BJ7B4R26.js → chunk-SAS5OB7F.js} +18 -2
  28. package/dist/{chunk-RYRB2YVJ.js → chunk-SR6QPRIE.js} +2 -2
  29. package/dist/chunk-UF6XIGYR.js +58 -0
  30. package/dist/{chunk-T7WJVS2N.js → chunk-UFOYFYI7.js} +2 -2
  31. package/dist/chunk-UJPXFA26.js +267 -0
  32. package/dist/{chunk-KOO5733S.js → chunk-WT4PO4A3.js} +8 -2
  33. package/dist/config.js +2 -2
  34. package/dist/{create-agent-7W57J7MT.js → create-agent-JFJ2K6X3.js} +5 -5
  35. package/dist/{create-annotation-W2XELBCQ.js → create-annotation-EUXXQ6NS.js} +5 -5
  36. package/dist/{create-api-key-MKRXKFJM.js → create-api-key-BGBB76JX.js} +5 -5
  37. package/dist/{create-dashboard-QGL7IODC.js → create-dashboard-Z76AIPLS.js} +5 -5
  38. package/dist/{create-dataset-3PH7A4HG.js → create-dataset-FD5VAXU4.js} +5 -5
  39. package/dist/{create-dataset-records-4DZS3VRL.js → create-dataset-records-WQUXXOHI.js} +5 -5
  40. package/dist/{create-evaluator-YWH6NC4M.js → create-evaluator-6NV33Z52.js} +5 -5
  41. package/dist/create-mcp-server.js +9 -4
  42. package/dist/{create-project-BOBQ32C5.js → create-project-ZQ7TFIO2.js} +5 -5
  43. package/dist/{create-prompt-FLCFGITE.js → create-prompt-BS6QRCUU.js} +4 -4
  44. package/dist/{create-prompt-tag-EMPNIKEA.js → create-prompt-tag-FCQ5ELSQ.js} +4 -4
  45. package/dist/{create-scenario-BSBIHUL6.js → create-scenario-DRPTPXU3.js} +12 -5
  46. package/dist/create-test-suite-ZIZCCFJ4.js +37 -0
  47. package/dist/{delete-agent-7MSYNY5E.js → delete-agent-SA4JMUZF.js} +5 -5
  48. package/dist/{delete-annotation-ZT5IGN24.js → delete-annotation-X3KKK7QY.js} +5 -5
  49. package/dist/{delete-dashboard-BTIY5S3L.js → delete-dashboard-LYMRPF27.js} +5 -5
  50. package/dist/{delete-dataset-U45HQHPR.js → delete-dataset-W6EZ44HY.js} +5 -5
  51. package/dist/{delete-dataset-records-R43IMAWB.js → delete-dataset-records-7CNOTALB.js} +5 -5
  52. package/dist/{delete-evaluator-U6JMQ6C7.js → delete-evaluator-RKYC4TNB.js} +5 -5
  53. package/dist/{delete-prompt-tag-4EV4TDHB.js → delete-prompt-tag-QB3OCZUV.js} +4 -4
  54. package/dist/{delete-workflow-5QYALXEL.js → delete-workflow-NG2VXYYM.js} +5 -5
  55. package/dist/{discover-evaluator-schema-MRGHVNTT.js → discover-evaluator-schema-ESSHQGPY.js} +58 -321
  56. package/dist/{discover-scenario-schema-UPBTCWPG.js → discover-scenario-schema-W7KBXA4J.js} +54 -2
  57. package/dist/{discover-schema-ICGXPHUW.js → discover-schema-L6QDPON3.js} +2 -2
  58. package/dist/get-agent-7THF7HCU.js +66 -0
  59. package/dist/{get-analytics-PVERWOA5.js → get-analytics-R6AZD3AW.js} +4 -4
  60. package/dist/{get-dashboard-ZZPC3WMD.js → get-dashboard-P4WWVHFU.js} +5 -5
  61. package/dist/{get-dataset-25TB2LQH.js → get-dataset-AKTYSQDI.js} +5 -5
  62. package/dist/{get-evaluator-3FTRQKEC.js → get-evaluator-RZ44UIN6.js} +5 -5
  63. package/dist/{get-project-5OAXYC57.js → get-project-QSQEAB33.js} +5 -5
  64. package/dist/get-prompt-GVKXF5NL.js +100 -0
  65. package/dist/get-run-plan-NY7J5GRE.js +61 -0
  66. package/dist/{get-scenario-DSSTR6FI.js → get-scenario-Q3LLQE4T.js} +9 -5
  67. package/dist/{get-simulation-run-RQPMS2MC.js → get-simulation-run-WINEYDMK.js} +14 -5
  68. package/dist/get-test-suite-4HKA7OKS.js +49 -0
  69. package/dist/{get-trace-O5VT3NAC.js → get-trace-RCUV7N4O.js} +4 -4
  70. package/dist/{get-workflow-3EZDOEIF.js → get-workflow-EEOC7Y45.js} +5 -5
  71. package/dist/http-security-EU3CHTY7.js +26 -0
  72. package/dist/{http-server-2ACJ3CXW.js → http-server-WCDUARTH.js} +1038 -693
  73. package/dist/index.js +6437 -4020
  74. package/dist/{langwatch-api-annotations-E65MI3WZ.js → langwatch-api-annotations-6DHVZWUK.js} +5 -5
  75. package/dist/{langwatch-api-dashboards-QCDECRCY.js → langwatch-api-dashboards-2KJL2M42.js} +5 -5
  76. package/dist/{langwatch-api-monitors-UIO2KTDV.js → langwatch-api-monitors-6ISFJ7WX.js} +4 -4
  77. package/dist/{langwatch-api-secrets-4T2EKSJ7.js → langwatch-api-secrets-OEBD6GBU.js} +4 -4
  78. package/dist/{langwatch-api-triggers-MPOEKPUY.js → langwatch-api-triggers-5E6DDGX6.js} +4 -4
  79. package/dist/langwatch-api-workflows-6UJL5U4Z.js +16 -0
  80. package/dist/list-agents-AMVDQPQL.js +43 -0
  81. package/dist/{list-annotations-CGX5BT7E.js → list-annotations-6PR5NLIC.js} +5 -5
  82. package/dist/{list-api-keys-JLWJPFRX.js → list-api-keys-GVPNBIHQ.js} +5 -5
  83. package/dist/{list-dashboards-RNIWX3TX.js → list-dashboards-Z3BZKDGF.js} +5 -5
  84. package/dist/{list-dataset-records-B5MHZR66.js → list-dataset-records-O67HZ35Y.js} +5 -5
  85. package/dist/{list-datasets-BVCAMQCP.js → list-datasets-WXZL6UMP.js} +5 -5
  86. package/dist/{list-evaluators-65FBTL2U.js → list-evaluators-PAX72WCN.js} +5 -5
  87. package/dist/{list-model-providers-GRK3I3VV.js → list-model-providers-VVLZMLCX.js} +5 -5
  88. package/dist/{list-projects-JLMBFMW3.js → list-projects-AQVI6SUK.js} +5 -5
  89. package/dist/{list-prompt-tags-UHNWDGQI.js → list-prompt-tags-WZFBJOHG.js} +4 -4
  90. package/dist/{list-prompts-7254B3U7.js → list-prompts-BQOHRHYT.js} +4 -4
  91. package/dist/list-run-plans-UBAU2DGH.js +46 -0
  92. package/dist/{list-scenarios-46YNICKM.js → list-scenarios-MS4IUIVM.js} +10 -7
  93. package/dist/{list-simulation-runs-LGNEFNPN.js → list-simulation-runs-D6YF4ZOU.js} +13 -7
  94. package/dist/list-test-suites-L7R2O6DR.js +35 -0
  95. package/dist/{list-workflows-MLQDM6CP.js → list-workflows-6MV4XJHF.js} +5 -5
  96. package/dist/{rename-prompt-tag-RHQMNEQL.js → rename-prompt-tag-K5LQ4CY6.js} +4 -4
  97. package/dist/rename-test-suite-NTYHAO5X.js +21 -0
  98. package/dist/report-issue-RHZZ33IS.js +885 -0
  99. package/dist/rerun-run-plan-MUDWJBE4.js +22 -0
  100. package/dist/{revoke-api-key-3Y35EMBT.js → revoke-api-key-6LAZ3QZZ.js} +5 -5
  101. package/dist/run-agent-CAGWGTYT.js +72 -0
  102. package/dist/run-plan-T4ZYOZGA.js +39 -0
  103. package/dist/run-test-suite-L4MZ7G4D.js +26 -0
  104. package/dist/{run-workflow-5LVBZYLS.js → run-workflow-DFIS23AP.js} +5 -5
  105. package/dist/{search-traces-U4HLOCZ2.js → search-traces-4XTJYXIK.js} +4 -4
  106. package/dist/{set-model-provider-V3M5CDIY.js → set-model-provider-XRNMFOVR.js} +5 -5
  107. package/dist/{update-agent-47RZIHS7.js → update-agent-DUWNYBNA.js} +5 -5
  108. package/dist/{update-dataset-G6QJJGS5.js → update-dataset-YSOMDCOB.js} +5 -5
  109. package/dist/{update-dataset-record-RMWLSWCS.js → update-dataset-record-DEA2ITYR.js} +5 -5
  110. package/dist/{update-evaluator-SFRRI5Q3.js → update-evaluator-NH7Q5GWV.js} +5 -5
  111. package/dist/{update-project-PI5DWBVK.js → update-project-Q26FBSYH.js} +5 -5
  112. package/dist/update-prompt-ZQQ2C3KU.js +143 -0
  113. package/dist/{update-scenario-G3IU2UIB.js → update-scenario-WUX34JAR.js} +12 -5
  114. package/package.json +8 -9
  115. package/pyproject.toml +5 -5
  116. package/src/__tests__/agent-tools.unit.test.ts +257 -0
  117. package/src/__tests__/all-tools.integration.test.ts +321 -75
  118. package/src/__tests__/config.unit.test.ts +33 -0
  119. package/src/__tests__/docker.integration.test.ts +91 -7
  120. package/src/__tests__/documentation-fetch-security.integration.test.ts +8 -2
  121. package/src/__tests__/feature-map-tool-drift.unit.test.ts +93 -0
  122. package/src/__tests__/http-security-origin.unit.test.ts +97 -0
  123. package/src/__tests__/http-security-sessions.unit.test.ts +209 -0
  124. package/src/__tests__/http-security-verifier.unit.test.ts +248 -0
  125. package/src/__tests__/http-server-allocation.integration.test.ts +279 -0
  126. package/src/__tests__/http-server-origin.integration.test.ts +169 -0
  127. package/src/__tests__/http-server-session-auth.integration.test.ts +341 -0
  128. package/src/__tests__/http-transport.integration.test.ts +85 -87
  129. package/src/__tests__/integration.integration.test.ts +7 -11
  130. package/src/__tests__/prompt-response-types.unit.test.ts +51 -0
  131. package/src/__tests__/prompts.integration.test.ts +736 -0
  132. package/src/__tests__/run-plan-tools.unit.test.ts +565 -0
  133. package/src/__tests__/scenario-tools.unit.test.ts +19 -0
  134. package/src/__tests__/simulation-run-tools.unit.test.ts +116 -0
  135. package/src/__tests__/support/http-server-harness.ts +138 -0
  136. package/src/__tests__/test-suite-tools.unit.test.ts +615 -0
  137. package/src/__tests__/{tools.unit.test.ts → tools.integration.test.ts} +31 -59
  138. package/src/config.ts +17 -2
  139. package/src/create-mcp-server.ts +334 -55
  140. package/src/http-security.ts +492 -0
  141. package/src/http-server.ts +536 -152
  142. package/src/index.ts +49 -2
  143. package/src/langwatch-api-agents.ts +196 -12
  144. package/src/langwatch-api-run-plans.ts +157 -0
  145. package/src/langwatch-api-scenarios.ts +9 -0
  146. package/src/langwatch-api-simulation-runs.ts +5 -0
  147. package/src/langwatch-api-test-suites.ts +135 -0
  148. package/src/langwatch-api.ts +62 -12
  149. package/src/schemas/index.ts +5 -0
  150. package/src/schemas/run-plan.ts +92 -0
  151. package/src/schemas/suite-fields.ts +149 -0
  152. package/src/tools/archive-run-plan.ts +12 -0
  153. package/src/tools/archive-test-suite.ts +12 -0
  154. package/src/tools/create-scenario.ts +8 -0
  155. package/src/tools/create-test-suite.ts +43 -0
  156. package/src/tools/discover-evaluator-schema.ts +2 -2
  157. package/src/tools/discover-scenario-schema.ts +54 -0
  158. package/src/tools/format-run-plan.ts +68 -0
  159. package/src/tools/format-scenario.ts +14 -0
  160. package/src/tools/format-suite-details.ts +96 -0
  161. package/src/tools/get-agent.ts +45 -3
  162. package/src/tools/get-prompt.ts +108 -24
  163. package/src/tools/get-run-plan.ts +62 -0
  164. package/src/tools/get-scenario.ts +3 -0
  165. package/src/tools/get-simulation-run.ts +9 -0
  166. package/src/tools/get-test-suite.ts +49 -0
  167. package/src/tools/list-agents.ts +20 -3
  168. package/src/tools/list-run-plans.ts +47 -0
  169. package/src/tools/list-scenarios.ts +12 -2
  170. package/src/tools/list-simulation-runs.ts +6 -2
  171. package/src/tools/list-test-suites.ts +35 -0
  172. package/src/tools/rename-test-suite.ts +18 -0
  173. package/src/tools/rerun-run-plan.ts +21 -0
  174. package/src/tools/run-agent.ts +54 -6
  175. package/src/tools/run-plan.ts +49 -0
  176. package/src/tools/run-test-suite.ts +30 -0
  177. package/src/tools/test-agent.ts +18 -0
  178. package/src/tools/update-prompt.ts +195 -10
  179. package/src/tools/update-scenario.ts +8 -0
  180. package/src/tools/update-test-suite.ts +45 -0
  181. package/tsconfig.json +7 -2
  182. package/uv.lock +418 -298
  183. package/dist/archive-suite-C3KX6IIZ.js +0 -16
  184. package/dist/chunk-WDH5P5RF.js +0 -48
  185. package/dist/create-suite-TWF6BBR4.js +0 -39
  186. package/dist/get-agent-7QJVPH72.js +0 -29
  187. package/dist/get-prompt-I753LZKZ.js +0 -46
  188. package/dist/get-suite-GTOCY5PL.js +0 -45
  189. package/dist/langwatch-api-workflows-5HRZ7HR4.js +0 -16
  190. package/dist/list-agents-TIJTP566.js +0 -33
  191. package/dist/list-suites-5VK4HXEK.js +0 -43
  192. package/dist/report-issue-DXJ6TPTC.js +0 -412
  193. package/dist/run-agent-RBSIFKRT.js +0 -37
  194. package/dist/run-suite-K3DNQ3SE.js +0 -30
  195. package/dist/update-prompt-RQFTEO7Y.js +0 -25
  196. package/dist/update-suite-TRGGLNR7.js +0 -40
  197. package/pnpm-workspace.yaml +0 -34
  198. package/src/langwatch-api-suites.ts +0 -118
  199. package/src/tools/archive-suite.ts +0 -12
  200. package/src/tools/create-suite.ts +0 -42
  201. package/src/tools/get-suite.ts +0 -47
  202. package/src/tools/list-suites.ts +0 -43
  203. package/src/tools/run-suite.ts +0 -28
  204. package/src/tools/update-suite.ts +0 -44
@@ -0,0 +1,885 @@
1
+ import {
2
+ getConfig
3
+ } from "./chunk-WT4PO4A3.js";
4
+ import "./chunk-SAS5OB7F.js";
5
+
6
+ // ../../packages/redaction/src/markers.ts
7
+ var SECRET_MARKER = "[SECRET]";
8
+ function formatPiiMarker(entity) {
9
+ return `[${entity}]`;
10
+ }
11
+
12
+ // ../../packages/redaction/src/secrets.ts
13
+ var SECRETS_REDACTION_MARKER = SECRET_MARKER;
14
+ var REPLACEMENT = SECRETS_REDACTION_MARKER;
15
+ var MAX_SCAN_LENGTH = 25e4;
16
+ var ENTROPY_SAMPLE_LENGTH = 256;
17
+ function shannonEntropyBits(value) {
18
+ const sample = value.length > ENTROPY_SAMPLE_LENGTH ? value.slice(0, ENTROPY_SAMPLE_LENGTH) : value;
19
+ const counts = /* @__PURE__ */ new Map();
20
+ for (const char of sample) {
21
+ counts.set(char, (counts.get(char) ?? 0) + 1);
22
+ }
23
+ let entropy = 0;
24
+ for (const count of counts.values()) {
25
+ const probability = count / sample.length;
26
+ entropy -= probability * Math.log2(probability);
27
+ }
28
+ return entropy;
29
+ }
30
+ function countCharClasses(value) {
31
+ let lower = 0;
32
+ let upper = 0;
33
+ let digit = 0;
34
+ for (let index = 0; index < value.length; index++) {
35
+ const code = value.charCodeAt(index);
36
+ if (code >= 97 && code <= 122) lower++;
37
+ else if (code >= 65 && code <= 90) upper++;
38
+ else if (code >= 48 && code <= 57) digit++;
39
+ }
40
+ return { lower, upper, digit };
41
+ }
42
+ var TOKEN_START = String.raw`(?<![A-Za-z0-9_-])`;
43
+ var TOKEN_END = String.raw`(?![A-Za-z0-9_-])`;
44
+ var VENDOR_KEY_PATTERNS = [
45
+ // LangWatch's own API, ingest and legacy personal-access tokens, minted as
46
+ // `{prefix}{lookupId}_{secret}` by
47
+ // platform/app/src/server/api-key/api-key-token.utils.ts. Matched on the
48
+ // prefix plus three body characters, like every other known vendor, so a
49
+ // truncated or short-bodied one still redacts: `sk-lw-` would otherwise reach
50
+ // only the generic `sk-` rule and its 20-character floor, and `ik-lw-`
51
+ // nothing at all. The three-character floor is what keeps the bare prefix,
52
+ // which documentation and error messages print on its own, from reading as a
53
+ // key.
54
+ // The prefixes are duplicated rather than imported because this package is
55
+ // shared with the SDK and stays dependency-free; a test pins them to the
56
+ // constants so the two cannot drift.
57
+ String.raw`(?:sk|ik|pat|vk)-lw-[A-Za-z0-9_-]{3,}`,
58
+ // GitLab personal, project, deploy, runner and agent tokens.
59
+ String.raw`gl(?:pat|rt|dt|soat|ptt|cbt|imt|agent|ffct)-[A-Za-z0-9_-]{20,}`,
60
+ String.raw`npm_[A-Za-z0-9]{36}`,
61
+ // Google OAuth client secret.
62
+ String.raw`GOCSPX-[A-Za-z0-9_-]{20,}`,
63
+ String.raw`mb_[A-Za-z0-9+/=]{40,}`,
64
+ String.raw`dckr_pat_[A-Za-z0-9_-]{20,}`,
65
+ // Shopify admin, storefront, custom and private app tokens.
66
+ String.raw`shp(?:at|ss|ca|pa)_[0-9a-fA-F]{32}`,
67
+ String.raw`SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}`,
68
+ String.raw`hf_[A-Za-z0-9]{30,}`,
69
+ String.raw`gsk_[A-Za-z0-9]{40,}`,
70
+ String.raw`pplx-[A-Za-z0-9]{30,}`,
71
+ String.raw`nvapi-[A-Za-z0-9_-]{40,}`,
72
+ String.raw`r8_[A-Za-z0-9]{30,}`,
73
+ String.raw`xai-[A-Za-z0-9]{40,}`,
74
+ // Notion integration tokens, current and legacy.
75
+ String.raw`ntn_[A-Za-z0-9]{30,}`,
76
+ String.raw`secret_[A-Za-z0-9]{40,}`,
77
+ String.raw`dop_v1_[0-9a-f]{64}`,
78
+ String.raw`figd_[A-Za-z0-9_-]{30,}`,
79
+ String.raw`ATATT[A-Za-z0-9_=-]{100,}`,
80
+ String.raw`sq0(?:atp|csp)-[A-Za-z0-9_-]{20,}`,
81
+ String.raw`EAAG[A-Za-z0-9]{60,}`,
82
+ String.raw`key-[0-9a-f]{32}`,
83
+ String.raw`re_[A-Za-z0-9_-]{20,}`,
84
+ String.raw`phx_[A-Za-z0-9]{30,}`,
85
+ String.raw`lin_api_[A-Za-z0-9]{30,}`,
86
+ String.raw`sl\.[A-Za-z0-9_-]{60,}`,
87
+ String.raw`ya29\.[A-Za-z0-9_-]{20,}`,
88
+ String.raw`sbp_[0-9a-f]{40,}`,
89
+ String.raw`sntry(?:s|u)_[A-Za-z0-9_.-]{30,}`,
90
+ String.raw`fw_[A-Za-z0-9]{20,}`,
91
+ String.raw`gl(?:c|sa)_[A-Za-z0-9]{30,}`,
92
+ String.raw`NRAK-[A-Za-z0-9]{20,}`,
93
+ String.raw`PMAK-[A-Za-z0-9]{20,}-[A-Za-z0-9]{20,}`,
94
+ String.raw`dp\.(?:pt|st|ct|sa)\.[A-Za-z0-9_-]{20,}`,
95
+ // Airtable personal access token: patXXXXXXXXXXXXXX.<64 hex>.
96
+ String.raw`pat[A-Za-z0-9]{14}\.[0-9a-f]{64}`,
97
+ // Telegram bot token: <numeric bot id>:AA<35-char body>.
98
+ String.raw`[0-9]{8,10}:AA[A-Za-z0-9_-]{33}`
99
+ ];
100
+ var VENDOR_KEY_REGEX = new RegExp(
101
+ `${TOKEN_START}(?:${VENDOR_KEY_PATTERNS.join("|")})${TOKEN_END}`,
102
+ "g"
103
+ );
104
+ var SHAPED_TOKEN_MIN_BODY = 26;
105
+ var SHAPED_TOKEN_MAX_BODY = 120;
106
+ var SHAPED_TOKEN_MIN_ENTROPY = 3.9;
107
+ function isKeyShapedBody(body) {
108
+ if (body.length < SHAPED_TOKEN_MIN_BODY || body.length > SHAPED_TOKEN_MAX_BODY) {
109
+ return false;
110
+ }
111
+ const { lower, upper, digit } = countCharClasses(body);
112
+ if (lower < 2 || upper < 2 || digit < 2) return false;
113
+ return shannonEntropyBits(body) >= SHAPED_TOKEN_MIN_ENTROPY;
114
+ }
115
+ var DIGEST_PREFIXES = /* @__PURE__ */ new Set([
116
+ "sha1",
117
+ "sha224",
118
+ "sha256",
119
+ "sha384",
120
+ "sha512",
121
+ "sha3",
122
+ "md4",
123
+ "md5",
124
+ "blake2b",
125
+ "blake2s",
126
+ "blake3",
127
+ "crc32",
128
+ "xxh3",
129
+ "xxh64",
130
+ "base32",
131
+ "base58",
132
+ "base64",
133
+ "hex",
134
+ "uuid",
135
+ "urn",
136
+ "cid",
137
+ "etag",
138
+ "hash",
139
+ "digest",
140
+ "checksum",
141
+ "integrity"
142
+ ]);
143
+ var HEX_BODY_CREDENTIAL_SEGMENTS = [
144
+ "live",
145
+ "test",
146
+ "prod",
147
+ "sk",
148
+ "pk",
149
+ "key",
150
+ "secret",
151
+ "token"
152
+ ];
153
+ var IDENTIFIER_PREFIXES = /* @__PURE__ */ new Set([
154
+ "commit",
155
+ "sha",
156
+ "sha1",
157
+ "sha256",
158
+ "md5",
159
+ "hash",
160
+ "digest",
161
+ "trace",
162
+ "span",
163
+ "id",
164
+ "uuid",
165
+ "rev",
166
+ "blob",
167
+ "tree",
168
+ "etag",
169
+ "checksum"
170
+ ]);
171
+ var RECORD_ID_PREFIXES = /* @__PURE__ */ new Set([
172
+ "project",
173
+ "provider",
174
+ "card",
175
+ "eval",
176
+ "monitor",
177
+ "scenario",
178
+ "ses",
179
+ "sess",
180
+ "session",
181
+ "thread",
182
+ "conv",
183
+ "langyconv",
184
+ "span",
185
+ "trace",
186
+ "run",
187
+ "msg",
188
+ "task",
189
+ "job",
190
+ "step",
191
+ "node",
192
+ "team",
193
+ "org",
194
+ "user",
195
+ "call",
196
+ "req",
197
+ "resp",
198
+ "chatcmpl",
199
+ "toolu",
200
+ "asst",
201
+ "file",
202
+ "batch",
203
+ "evt",
204
+ "acct",
205
+ "cus",
206
+ "sub"
207
+ ]);
208
+ var PUBLIC_KEY_PREFIXES = /* @__PURE__ */ new Set(["phc"]);
209
+ function isNonCredentialPrefix(prefix) {
210
+ const lower = prefix.toLowerCase();
211
+ return DIGEST_PREFIXES.has(lower) || IDENTIFIER_PREFIXES.has(lower) || RECORD_ID_PREFIXES.has(lower) || PUBLIC_KEY_PREFIXES.has(lower);
212
+ }
213
+ var HEX_BODY_MIN = 24;
214
+ var HEX_BODY_MAX = 128;
215
+ var PLACEHOLDER_VALUE_REGEX = /^(?:x+|\*+|\.+|-+|_+|0+|(?:your|my|our|insert|replace|example|sample|dummy|fake|placeholder|changeme|redacted|removed|hidden|none|null|nil|undefined|todo|tbd|fixme)[a-z0-9_\- ]*)$/i;
216
+ var ENV_REFERENCE_REGEX = /^(?:\$[A-Za-z_][A-Za-z0-9_]*|[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)$/;
217
+ var CODE_EXPRESSION_REGEX = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
218
+ var PATH_LIKE_REGEX = /^[~.]{0,2}\/|\/[^/\s]*\.[a-z]{1,5}$/;
219
+ var CONTEXT_VALUE_MIN_LENGTH = 16;
220
+ var CONTEXT_VALUE_MIN_ENTROPY = 2.9;
221
+ function isCredentialValue(value) {
222
+ if (value.length < CONTEXT_VALUE_MIN_LENGTH) return false;
223
+ if (PLACEHOLDER_VALUE_REGEX.test(value)) return false;
224
+ if (ENV_REFERENCE_REGEX.test(value)) return false;
225
+ if (CODE_EXPRESSION_REGEX.test(value)) return false;
226
+ if (PATH_LIKE_REGEX.test(value)) return false;
227
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return false;
228
+ if (value.includes(SECRETS_REDACTION_MARKER)) return false;
229
+ return shannonEntropyBits(value) >= CONTEXT_VALUE_MIN_ENTROPY;
230
+ }
231
+ var VERSION_STRING_REGEX = /^v?\d+(?:[._-]\d+)+/;
232
+ var LOOSE_VALUE_MIN_LENGTH = 20;
233
+ var LOOSE_VALUE_MIN_ENTROPY = 3.4;
234
+ function isKeyMaterial(value) {
235
+ if (value.length < LOOSE_VALUE_MIN_LENGTH) return false;
236
+ if (!isCredentialValue(value)) return false;
237
+ if (VERSION_STRING_REGEX.test(value)) return false;
238
+ if (shannonEntropyBits(value) < LOOSE_VALUE_MIN_ENTROPY) return false;
239
+ if (/^[0-9a-f]{32,}$/i.test(value)) return true;
240
+ if (/^[A-Z2-7]{32,}={0,6}$/.test(value)) return true;
241
+ const { lower, upper, digit } = countCharClasses(value);
242
+ return digit >= 2 && (lower >= 2 || upper >= 2);
243
+ }
244
+ var CREDENTIAL_QUALIFIERS = /* @__PURE__ */ new Set([
245
+ "master",
246
+ "encryption",
247
+ "signing",
248
+ "private",
249
+ "access",
250
+ "api",
251
+ "auth",
252
+ "secret",
253
+ "refresh",
254
+ "session",
255
+ "bearer",
256
+ "verification",
257
+ "webhook",
258
+ "client",
259
+ "service",
260
+ "personal",
261
+ "root",
262
+ "admin"
263
+ ]);
264
+ var QUALIFIER_ALTERNATION = [...CREDENTIAL_QUALIFIERS].join("|");
265
+ var CREDENTIAL_KEYWORD = String.raw`(?:x[_.\- ]?)?(?:` + String.raw`(?:${QUALIFIER_ALTERNATION})[_.\- ]?(?:api[_.\- ]?)?key` + String.raw`|(?:${QUALIFIER_ALTERNATION})?[_.\- ]?(?:api[_.\- ]?)?` + String.raw`(?:token|secret|password|passwd|pwd|credentials?|authorization|cookie)` + String.raw`)`;
266
+ function isBasicAuthPayload(value) {
267
+ if (value.endsWith("=")) return true;
268
+ const { lower, upper, digit } = countCharClasses(value);
269
+ return digit > 0 || lower > 0 && upper > 0;
270
+ }
271
+ var VALUE_RULES = [
272
+ {
273
+ id: "pem_private_key",
274
+ description: "PEM private key block",
275
+ regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g
276
+ },
277
+ {
278
+ // PuTTY's own private-key format, which carries no PEM armour. The body
279
+ // runs to the end of the file, so it is clamped at the next blank line
280
+ // rather than allowed to swallow the rest of the payload.
281
+ id: "putty_private_key",
282
+ description: "PuTTY private key file",
283
+ regex: /PuTTY-User-Key-File-\d+:[\s\S]*?(?:\n\s*\n|$)/g
284
+ },
285
+ {
286
+ // A kubeconfig embeds the client certificate and its key as base64. The
287
+ // key is the credential; the certificate is redacted with it because
288
+ // together they are a working login.
289
+ id: "kubeconfig_client_credentials",
290
+ description: "Embedded kubeconfig client key or certificate",
291
+ regex: /\b(client-(?:key|certificate)-data:\s*)[A-Za-z0-9+/=]{40,}/g,
292
+ render: (_m, prefix) => `${prefix}${REPLACEMENT}`
293
+ },
294
+ {
295
+ id: "aws_access_key_id",
296
+ description: "AWS access key id",
297
+ regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|ANPA|ANVA)[0-9A-Z]{16}\b/g
298
+ },
299
+ {
300
+ id: "github_token",
301
+ description: "GitHub token",
302
+ regex: /\b(?:gh[posru]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{60,})\b/g
303
+ },
304
+ {
305
+ // Provider secret keys share the `sk-` namespace: OpenAI (`sk-proj-...`,
306
+ // legacy `sk-...`), Anthropic (`sk-ant-...`), LangWatch (`sk-lw-...`), and
307
+ // others. The body is base64url, so it includes `_` and `-` and has no inner
308
+ // word boundary; matching the whole token and stopping at the next non-key
309
+ // char catches modern keys a `[A-Za-z0-9]+\b` rule misses.
310
+ id: "provider_api_key",
311
+ description: "Provider API key (sk-...)",
312
+ regex: /\bsk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
313
+ },
314
+ {
315
+ id: "stripe_secret_key",
316
+ description: "Stripe secret key",
317
+ regex: /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}\b/g
318
+ },
319
+ {
320
+ id: "slack_token",
321
+ description: "Slack token",
322
+ regex: /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g
323
+ },
324
+ {
325
+ id: "google_api_key",
326
+ description: "Google API key",
327
+ regex: /\bAIza[0-9A-Za-z_-]{35}\b/g
328
+ },
329
+ {
330
+ id: "vendor_api_key",
331
+ description: "Developer-service API key (GitLab, npm, Docker Hub, Shopify, SendGrid, Hugging Face, Groq, Notion, Atlassian and others)",
332
+ regex: VENDOR_KEY_REGEX
333
+ },
334
+ {
335
+ id: "jwt",
336
+ description: "JSON Web Token",
337
+ regex: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g
338
+ },
339
+ {
340
+ id: "url_credentials",
341
+ description: "Password embedded in a connection URL",
342
+ // scheme://user:password@host -> keep everything but the password. The
343
+ // scheme length is bounded (real schemes are short) so a long run of
344
+ // name-like characters costs constant backtracking per position instead
345
+ // of a quadratic scan on huge inputs.
346
+ //
347
+ // The scheme sits in a lookbehind so that the first character the engine
348
+ // must find is the literal `:`. With the scheme inside the match the
349
+ // pattern starts with a character class, every lowercase letter in the
350
+ // text is a candidate start, and each one costs up to 30 characters of
351
+ // scheme scan before it fails: 6.2 ms on a 200 KB payload, more than every
352
+ // other rule together. Anchored on `:` the same payload costs 0.09 ms.
353
+ // The scheme stays outside the match and therefore untouched, which is
354
+ // what the rule already did with it.
355
+ regex: /(?<=[a-z][a-z0-9+.-]{0,30})(:\/\/[^\s:@/]+:)([^\s:@/]+)(@)/gi,
356
+ // The same scheme the lookbehind reads, so a reported match still spans the
357
+ // whole URL rather than starting at the colon.
358
+ precededBy: /[a-z][a-z0-9+.-]{0,30}$/i,
359
+ render: (_m, prefix, _password, at) => `${prefix}${REPLACEMENT}${at}`
360
+ },
361
+ {
362
+ id: "bearer_token",
363
+ description: "Bearer authorization token",
364
+ regex: /\b(Bearer\s+)[A-Za-z0-9._~+/-]{10,}=*/gi,
365
+ render: (_m, prefix) => `${prefix}${REPLACEMENT}`
366
+ },
367
+ {
368
+ // The other Authorization schemes. Unlike `Bearer` these are ordinary
369
+ // words (`Token`, `OAuth`, `Splunk`), so they are only a credential cue
370
+ // inside an actual Authorization header: a bare "token acme_live_abcd1234"
371
+ // in prose is a sentence, not a header, and matching it redacted one.
372
+ id: "authorization_scheme_token",
373
+ description: "Non-Bearer authorization scheme token",
374
+ regex: /\b(Authorization:\s*(?:Token|SSWS|GenieKey|Splunk|OAuth)\s+)[A-Za-z0-9._~+/-]{10,}=*/gi,
375
+ render: (_m, prefix) => `${prefix}${REPLACEMENT}`
376
+ },
377
+ {
378
+ id: "basic_auth_credentials",
379
+ description: "Basic authorization credentials",
380
+ regex: /\b(Basic\s+)([A-Za-z0-9+/]{16,}={0,2})(?![A-Za-z0-9+/=])/g,
381
+ accept: (groups) => isBasicAuthPayload(groups[2] ?? ""),
382
+ render: (_m, prefix) => `${prefix}${REPLACEMENT}`
383
+ },
384
+ {
385
+ // The all-hex sibling of the shape rule below, which cannot accept a hex
386
+ // body without also accepting every digest and trace id in the transcript.
387
+ // A credential segment in the middle of the token is what makes the
388
+ // difference, so this rule requires one and refuses identifier prefixes on
389
+ // top of it.
390
+ id: "prefixed_hex_api_key",
391
+ description: "API key with a vendor prefix and an all-hex body",
392
+ regex: new RegExp(
393
+ `${TOKEN_START}([A-Za-z][A-Za-z0-9]{1,11})_(?:${HEX_BODY_CREDENTIAL_SEGMENTS.join("|")})_([0-9a-f]{${HEX_BODY_MIN},${HEX_BODY_MAX}})${TOKEN_END}`,
394
+ "gi"
395
+ ),
396
+ accept: (groups) => !IDENTIFIER_PREFIXES.has((groups[1] ?? "").toLowerCase()),
397
+ precondition: (text) => text.includes("_")
398
+ },
399
+ {
400
+ // The layer that catches a vendor nobody has heard of. A short prefix, a
401
+ // separator and a high-entropy body is the shape almost every modern key is
402
+ // minted in, and it needs no vendor knowledge at all.
403
+ //
404
+ // The prefix may be upper, lower or mixed case: plenty of vendors mint
405
+ // `LW_…` or `Xy_…`, and restricting it to lowercase missed them. That also
406
+ // makes the prefix the shape of a screaming-snake environment variable
407
+ // NAME, which must survive as a bare name, and does: `AWS_SECRET_ACCESS_KEY`
408
+ // and `DATABASE_URL_PRODUCTION` are dictionary words with no digits and no
409
+ // lowercase, so the length floor and the character-mix gate turn them both
410
+ // away. The digest check lowercases the prefix so `SHA512-…` is still
411
+ // recognised as a digest.
412
+ //
413
+ // A declined match consumes the text it spanned, so in principle a benign
414
+ // outer match could hide a secret further inside the same unbroken token.
415
+ // It cannot in practice: a body containing key material inherits that
416
+ // material's entropy and character mix, so the outer match is accepted and
417
+ // the secret is redacted along with its prefix.
418
+ id: "shaped_api_key",
419
+ description: "High-entropy API key with a vendor-style prefix",
420
+ regex: new RegExp(
421
+ `${TOKEN_START}([A-Za-z][A-Za-z0-9]{1,11})[_-]([A-Za-z0-9_+/-]{${SHAPED_TOKEN_MIN_BODY},})${TOKEN_END}`,
422
+ "g"
423
+ ),
424
+ accept: (groups) => !isNonCredentialPrefix(groups[1] ?? "") && isKeyShapedBody(groups[2] ?? ""),
425
+ precondition: (text) => text.includes("_") || text.includes("-")
426
+ },
427
+ {
428
+ // The layer that needs no shape at all: the text says what the value is.
429
+ // Up to two filler words are tolerated between the keyword and the
430
+ // separator, because people write "key now:" and "token here =" as often as
431
+ // they write "key:".
432
+ id: "sensitive_assignment",
433
+ description: "Value assigned to a credential-named field",
434
+ // One capture for everything that introduces the value, one for the value
435
+ // itself, so the replacement puts the sentence back and swaps only the
436
+ // credential.
437
+ // `key is <value>` was accepted as a separator too, and across a 236 MB
438
+ // corpus of real traces it caught zero credentials while being the sole
439
+ // source of English-prose redactions: "a bare digest of an API key is
440
+ // offline-checkable" lost the words after "key is". A cue that only ever
441
+ // fires on prose is not a cue, so the strict separator is `:` or `=`.
442
+ //
443
+ // A whitespace separator is accepted on a HIGHER bar instead of not at
444
+ // all, because `Authorization <token>` and `key <token>` do carry real
445
+ // credentials. Ungated it matched 5,816 further spans over 309 shapes,
446
+ // mostly prose; gated on the value looking like key material in its own
447
+ // right it matches 338 over 19 at roughly 87% precision.
448
+ //
449
+ // The value class excludes backslash. Span content arrives JSON-encoded,
450
+ // so a literal two-character `\n` sits inside the text; letting a value run
451
+ // through one carried it across logical lines and past the `$VAR` and
452
+ // code-expression guards, which is why `api_key = $OPENAI_API_KEY` was kept
453
+ // with a real newline and redacted with an escaped one.
454
+ regex: new RegExp(
455
+ `((?:^|[\\W_])(?:${CREDENTIAL_KEYWORD})(?:\\s+[A-Za-z]{1,8}){0,2}["'\`]?(?:\\s*[:=]{1,2}\\s*|[ \\t?-]+)["'\`]?)([^\\s"'\`,;<>(){}\\[\\]\\\\]{${CONTEXT_VALUE_MIN_LENGTH},})`,
456
+ "gi"
457
+ ),
458
+ accept: (groups) => {
459
+ const introduction = groups[1] ?? "";
460
+ const value = groups[2] ?? "";
461
+ const strict = /[:=]\s*["'`]?$/.test(introduction);
462
+ return strict ? isCredentialValue(value) : isKeyMaterial(value);
463
+ },
464
+ render: (_m, introduction) => `${introduction}${REPLACEMENT}`
465
+ }
466
+ ];
467
+ var BUILTIN_SECRET_RULES = VALUE_RULES.map(({ id, description }) => ({ id, description }));
468
+ var SENSITIVE_KEY_REGEX = /(?:^|[._-])(?:password|passwd|pwd|secret|api[_-]?key|apikey|access[_-]?token|auth[_-]?token|authorization|auth|bearer|credentials?|private[_-]?key|client[_-]?secret|db[_-]?password|connection[_-]?string|session[_-]?token|refresh[_-]?token|set[_-]?cookie|cookie|x-api-key)(?:$|[._-])/i;
469
+ var CREDENTIAL_NOUNS = /* @__PURE__ */ new Set([
470
+ "password",
471
+ "passwd",
472
+ "pwd",
473
+ "secret",
474
+ "authorization",
475
+ "auth",
476
+ "bearer",
477
+ "credential",
478
+ "credentials",
479
+ "cookie"
480
+ ]);
481
+ function tokenizeAttributeName(name) {
482
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((token) => token.toLowerCase());
483
+ }
484
+ function namesACredential(name) {
485
+ const tokens = tokenizeAttributeName(name);
486
+ return tokens.some((token, index) => {
487
+ if (CREDENTIAL_NOUNS.has(token)) return true;
488
+ if (token !== "key" && token !== "token") return false;
489
+ const previous = tokens[index - 1];
490
+ return previous !== void 0 && CREDENTIAL_QUALIFIERS.has(previous);
491
+ });
492
+ }
493
+ function isSensitiveAttributeKey(key) {
494
+ return SENSITIVE_KEY_REGEX.test(key) || namesACredential(key);
495
+ }
496
+ var VALUE_BOUNDARY = /["'`]/;
497
+ function keptLengthAtBoundary(match) {
498
+ const index = match.search(VALUE_BOUNDARY);
499
+ return index === -1 ? match.length : index;
500
+ }
501
+ var CUSTOM_VALUE_BOUNDARY = /[\s"'`<>]/;
502
+ function keptLengthForCustomPattern(match) {
503
+ const index = match.search(CUSTOM_VALUE_BOUNDARY);
504
+ return index === -1 ? match.length : index;
505
+ }
506
+ function replacementFor(rule, args) {
507
+ const full = args[0] ?? "";
508
+ if (rule.accept && !rule.accept(args)) return null;
509
+ if (rule.render) return rule.render(...args);
510
+ const kept = keptLengthAtBoundary(full);
511
+ if (kept === 0) return null;
512
+ return REPLACEMENT + full.slice(kept);
513
+ }
514
+ function sliceForScan(text) {
515
+ if (text.length <= MAX_SCAN_LENGTH) return [text];
516
+ const slices = [];
517
+ let start = 0;
518
+ while (start < text.length) {
519
+ const end = sliceEndAfter(text, start);
520
+ slices.push(text.slice(start, end));
521
+ start = end;
522
+ }
523
+ return slices;
524
+ }
525
+ function sliceEndAfter(text, start) {
526
+ const target = Math.min(start + MAX_SCAN_LENGTH, text.length);
527
+ if (target >= text.length) return text.length;
528
+ let end;
529
+ const lastSpace = text.slice(start, target).search(/\s(?=\S*$)/);
530
+ if (lastSpace > 0) {
531
+ end = start + lastSpace;
532
+ } else {
533
+ const lookahead = text.slice(target, target + SAFE_CUT_LOOKAHEAD);
534
+ const next = lookahead.search(/\s/);
535
+ end = next === -1 ? Math.min(target + SAFE_CUT_LOOKAHEAD, text.length) : target + next;
536
+ }
537
+ const begin = text.lastIndexOf(PEM_BEGIN, end);
538
+ const close = text.indexOf(PEM_END, begin);
539
+ if (begin >= start && close >= end) {
540
+ end = close + PEM_END.length;
541
+ }
542
+ return end;
543
+ }
544
+ var SAFE_CUT_LOOKAHEAD = 4096;
545
+ var PEM_BEGIN = "-----BEGIN";
546
+ var PEM_END = "-----END";
547
+ function redactSecretsInText({
548
+ text,
549
+ customPatterns = [],
550
+ skipRuleIds
551
+ }) {
552
+ if (typeof text !== "string" || text.length === 0) {
553
+ return { text, redactedCount: 0 };
554
+ }
555
+ const skipped = toSkipSet(skipRuleIds);
556
+ if (text.length > MAX_SCAN_LENGTH) {
557
+ let total = 0;
558
+ const pieces = sliceForScan(text).map((slice) => {
559
+ const scanned = redactOneSlice(slice, customPatterns, skipped);
560
+ total += scanned.redactedCount;
561
+ return scanned.text;
562
+ });
563
+ return { text: pieces.join(""), redactedCount: total };
564
+ }
565
+ return redactOneSlice(text, customPatterns, skipped);
566
+ }
567
+ function toSkipSet(skipRuleIds) {
568
+ if (!skipRuleIds || skipRuleIds.length === 0) return null;
569
+ return new Set(skipRuleIds);
570
+ }
571
+ function redactOneSlice(text, customPatterns, skipped) {
572
+ let redactedCount = 0;
573
+ let result = text;
574
+ for (const rule of VALUE_RULES) {
575
+ if (skipped?.has(rule.id)) continue;
576
+ if (rule.precondition && !rule.precondition(result)) continue;
577
+ result = result.replace(rule.regex, (...args) => {
578
+ const replacement = replacementFor(rule, args);
579
+ if (replacement === null) return args[0] ?? "";
580
+ redactedCount++;
581
+ return replacement;
582
+ });
583
+ }
584
+ for (const pattern of customPatterns) {
585
+ result = result.replace(pattern, (...args) => {
586
+ const full = args[0] ?? "";
587
+ const kept = keptLengthForCustomPattern(full);
588
+ if (kept === 0) return full;
589
+ redactedCount++;
590
+ return REPLACEMENT + full.slice(kept);
591
+ });
592
+ }
593
+ return { text: result, redactedCount };
594
+ }
595
+
596
+ // ../../packages/redaction/src/sessionReport.ts
597
+ var EMAIL_MARKER = formatPiiMarker("EMAIL_ADDRESS");
598
+ var PHONE_MARKER = formatPiiMarker("PHONE_NUMBER");
599
+ var CARD_MARKER = formatPiiMarker("CREDIT_CARD");
600
+ var IP_MARKER = formatPiiMarker("IP_ADDRESS");
601
+ var ENV_SECRET_NAME_REGEX = /(?:^|[._-])(?:key|token|secret|password|passwd|pwd|credentials?|auth)(?:$|[._-])/i;
602
+ var MIN_ENV_SECRET_LENGTH = 8;
603
+ function collectSensitiveEnvValues(env) {
604
+ const values = /* @__PURE__ */ new Set();
605
+ for (const [name, value] of Object.entries(env)) {
606
+ if (!value) continue;
607
+ const trimmed = value.trim();
608
+ if (trimmed.length < MIN_ENV_SECRET_LENGTH) continue;
609
+ if (!ENV_SECRET_NAME_REGEX.test(name)) continue;
610
+ values.add(trimmed);
611
+ }
612
+ return [...values].sort((a, b) => b.length - a.length);
613
+ }
614
+ var EMAIL_REGEX = /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}/g;
615
+ var INTL_PHONE_REGEX = /(?<![\w.+-])\+[1-9]\d{0,2}(?:[\s.-]?(?:\(\d{1,4}\)[\s.-]?)?\d{1,4}){1,5}(?!\d)/g;
616
+ var NATIONAL_PHONE_REGEX = /(?<!\d)(?:\(\d{3}\)[\s.-]?|\d{3}[.-])\d{3}[.-]\d{4}(?!\d)/g;
617
+ var CARD_REGEX = /(?<![\d.-])\d(?:[ -]?\d){12,18}(?![\d.-])/g;
618
+ var IPV4_REGEX = /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g;
619
+ var countDigits = (value) => value.replace(/\D/g, "").length;
620
+ function passesLuhn(digits) {
621
+ let sum = 0;
622
+ let double = false;
623
+ for (let i = digits.length - 1; i >= 0; i--) {
624
+ let d = digits.charCodeAt(i) - 48;
625
+ if (double) {
626
+ d *= 2;
627
+ if (d > 9) d -= 9;
628
+ }
629
+ sum += d;
630
+ double = !double;
631
+ }
632
+ return sum % 10 === 0;
633
+ }
634
+ function isCardNumber(candidate) {
635
+ const digits = candidate.replace(/[ -]/g, "");
636
+ if (!/^\d+$/.test(digits)) return false;
637
+ const separated = /[ -]/.test(candidate);
638
+ if (!separated && (digits.length < 14 || digits.length > 16)) return false;
639
+ if (digits.length < 13 || digits.length > 19) return false;
640
+ return passesLuhn(digits);
641
+ }
642
+ function isPrivateOrLocalIp(ip) {
643
+ const parts = ip.split(".").map(Number);
644
+ const [a = 0, b = 0] = parts;
645
+ if (a === 0 || a === 10 || a === 127) return true;
646
+ if (a === 192 && b === 168) return true;
647
+ if (a === 172 && b >= 16 && b <= 31) return true;
648
+ if (a === 169 && b === 254) return true;
649
+ if (a === 100 && b >= 64 && b <= 127) return true;
650
+ if (a >= 224) return true;
651
+ return false;
652
+ }
653
+ var SLICE_TARGET = 2e5;
654
+ function sliceLongText(text) {
655
+ if (text.length <= SLICE_TARGET) return [text];
656
+ const slices = [];
657
+ let start = 0;
658
+ while (start < text.length) {
659
+ let end = Math.min(start + SLICE_TARGET, text.length);
660
+ if (end < text.length) {
661
+ const newline = text.lastIndexOf("\n", end);
662
+ if (newline > start + SLICE_TARGET / 2) end = newline;
663
+ }
664
+ slices.push(text.slice(start, end));
665
+ start = end;
666
+ }
667
+ return slices;
668
+ }
669
+ function redactPiiPatterns(text) {
670
+ let redactedCount = 0;
671
+ let result = text.replace(EMAIL_REGEX, () => {
672
+ redactedCount++;
673
+ return EMAIL_MARKER;
674
+ });
675
+ result = result.replace(CARD_REGEX, (match) => {
676
+ if (!isCardNumber(match)) return match;
677
+ redactedCount++;
678
+ return CARD_MARKER;
679
+ });
680
+ result = result.replace(INTL_PHONE_REGEX, (match) => {
681
+ const digits = countDigits(match);
682
+ if (digits < 7 || digits > 15) return match;
683
+ redactedCount++;
684
+ return PHONE_MARKER;
685
+ });
686
+ result = result.replace(NATIONAL_PHONE_REGEX, () => {
687
+ redactedCount++;
688
+ return PHONE_MARKER;
689
+ });
690
+ result = result.replace(IPV4_REGEX, (match) => {
691
+ if (isPrivateOrLocalIp(match)) return match;
692
+ redactedCount++;
693
+ return IP_MARKER;
694
+ });
695
+ return { text: result, redactedCount };
696
+ }
697
+ function scrubEnvLiterals(text, envValues) {
698
+ let result = text;
699
+ let redactedCount = 0;
700
+ for (const value of envValues) {
701
+ if (!result.includes(value)) continue;
702
+ const parts = result.split(value);
703
+ redactedCount += parts.length - 1;
704
+ result = parts.join(SECRET_MARKER);
705
+ }
706
+ return { text: result, redactedCount };
707
+ }
708
+ function redactReportText({
709
+ text,
710
+ envValues = []
711
+ }) {
712
+ if (typeof text !== "string" || text.length === 0) {
713
+ return { text, redactedCount: 0 };
714
+ }
715
+ const envPass = scrubEnvLiterals(text, envValues);
716
+ let redactedCount = envPass.redactedCount;
717
+ const pieces = sliceLongText(envPass.text).map((slice) => {
718
+ const secrets = redactSecretsInText({ text: slice });
719
+ redactedCount += secrets.redactedCount;
720
+ const pii = redactPiiPatterns(secrets.text);
721
+ redactedCount += pii.redactedCount;
722
+ return pii.text;
723
+ });
724
+ return { text: pieces.join(""), redactedCount };
725
+ }
726
+ function redactJsonValue(value, envValues, count) {
727
+ if (typeof value === "string") {
728
+ const result = redactReportText({ text: value, envValues });
729
+ count.redacted += result.redactedCount;
730
+ return result.text;
731
+ }
732
+ if (Array.isArray(value)) {
733
+ return value.map((item) => redactJsonValue(item, envValues, count));
734
+ }
735
+ if (value && typeof value === "object") {
736
+ const out = {};
737
+ for (const [key, entry] of Object.entries(value)) {
738
+ if (isSensitiveAttributeKey(key) && entry !== null && entry !== void 0) {
739
+ out[key] = SECRET_MARKER;
740
+ count.redacted++;
741
+ continue;
742
+ }
743
+ out[key] = redactJsonValue(entry, envValues, count);
744
+ }
745
+ return out;
746
+ }
747
+ return value;
748
+ }
749
+ function redactSessionJsonl({
750
+ jsonl,
751
+ envValues = []
752
+ }) {
753
+ let redactedCount = 0;
754
+ const lines = jsonl.split("\n").map((line) => {
755
+ if (line.trim().length === 0) return line;
756
+ try {
757
+ const parsed = JSON.parse(line);
758
+ const count = { redacted: 0 };
759
+ const redacted = redactJsonValue(parsed, envValues, count);
760
+ redactedCount += count.redacted;
761
+ return JSON.stringify(redacted);
762
+ } catch {
763
+ const result = redactReportText({ text: line, envValues });
764
+ redactedCount += result.redactedCount;
765
+ return result.text;
766
+ }
767
+ });
768
+ return { text: lines.join("\n"), redactedCount };
769
+ }
770
+ function truncateJsonlToByteBudget({
771
+ jsonl,
772
+ maxBytes
773
+ }) {
774
+ const encoder = new TextEncoder();
775
+ if (encoder.encode(jsonl).length <= maxBytes) {
776
+ return { text: jsonl, truncated: false };
777
+ }
778
+ const lines = jsonl.split("\n");
779
+ const kept = [];
780
+ let used = 0;
781
+ for (let i = lines.length - 1; i >= 0; i--) {
782
+ const line = lines[i] ?? "";
783
+ const size = encoder.encode(line).length + 1;
784
+ if (used + size > maxBytes) break;
785
+ kept.unshift(line);
786
+ used += size;
787
+ }
788
+ return { text: kept.join("\n"), truncated: true };
789
+ }
790
+ var REDACTION_AUDIT_URL = "https://github.com/langwatch/langwatch/blob/main/packages/redaction/src/sessionReport.ts";
791
+
792
+ // src/tools/report-issue.ts
793
+ var MAX_SESSION_BYTES = 8 * 1024 * 1024;
794
+ var REQUEST_TIMEOUT_MS = 6e4;
795
+ function redactReport(params) {
796
+ const envValues = collectSensitiveEnvValues(process.env);
797
+ let redactedCount = 0;
798
+ const titleResult = redactReportText({ text: params.title, envValues });
799
+ redactedCount += titleResult.redactedCount;
800
+ let summary;
801
+ if (params.summary) {
802
+ const result = redactReportText({ text: params.summary, envValues });
803
+ summary = result.text;
804
+ redactedCount += result.redactedCount;
805
+ }
806
+ let sessionData;
807
+ let sessionTruncated = false;
808
+ if (params.session_content) {
809
+ const redacted = redactSessionJsonl({
810
+ jsonl: params.session_content,
811
+ envValues
812
+ });
813
+ redactedCount += redacted.redactedCount;
814
+ const truncated = truncateJsonlToByteBudget({
815
+ jsonl: redacted.text,
816
+ maxBytes: MAX_SESSION_BYTES
817
+ });
818
+ sessionData = truncated.text;
819
+ sessionTruncated = truncated.truncated;
820
+ }
821
+ return {
822
+ title: titleResult.text,
823
+ summary,
824
+ sessionData,
825
+ sessionTruncated,
826
+ redactedCount
827
+ };
828
+ }
829
+ async function deliverReport(report, params) {
830
+ const config = getConfig();
831
+ const endpoint = config.endpoint.replace(/\/+$/, "");
832
+ const response = await fetch(`${endpoint}/api/bug-reports`, {
833
+ method: "POST",
834
+ headers: {
835
+ "content-type": "application/json",
836
+ ...config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {}
837
+ },
838
+ body: JSON.stringify({
839
+ source: "mcp",
840
+ kind: report.sessionData ? "full_session" : "summary",
841
+ title: report.title,
842
+ ...report.summary ? { summary: report.summary } : {},
843
+ ...report.sessionData ? { sessionData: report.sessionData, sessionTruncated: report.sessionTruncated } : {},
844
+ ...params.agent ? { agent: params.agent } : {},
845
+ ...params.contact_email ? { contactEmail: params.contact_email } : {}
846
+ }),
847
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
848
+ }).catch(() => {
849
+ throw new Error(
850
+ `Could not reach ${endpoint} to deliver the report. Retry in a moment; if it keeps failing, email support@langwatch.ai with the same content.`
851
+ );
852
+ });
853
+ if (!response.ok) {
854
+ const body = (await response.text().catch(() => "")).slice(0, 300);
855
+ throw new Error(
856
+ `The reports endpoint answered HTTP ${response.status}: ${body}. Retry in a moment; if it keeps failing, email support@langwatch.ai.`
857
+ );
858
+ }
859
+ const { id } = await response.json();
860
+ return id;
861
+ }
862
+ async function handleReportIssue(params) {
863
+ if (!params.user_approved) {
864
+ throw new Error(
865
+ `This sends a report to the LangWatch team, so the user must approve it first. Ask the user: "Can I send this issue report to LangWatch to help them fix it?" If they agree, call again with user_approved=true. Secrets and personal data are redacted locally before sending; audit the rules at ${REDACTION_AUDIT_URL}`
866
+ );
867
+ }
868
+ if (!params.summary?.trim() && !params.session_content?.trim()) {
869
+ throw new Error(
870
+ "Nothing to report: pass a summary (what you tried, verbatim errors, what you had to figure out), session_content, or both."
871
+ );
872
+ }
873
+ const report = redactReport(params);
874
+ const id = await deliverReport(report, params);
875
+ return [
876
+ `Report sent to the LangWatch team (${id}). Thank you!`,
877
+ `Redacted ${report.redactedCount} sensitive value${report.redactedCount === 1 ? "" : "s"} locally before sending.`,
878
+ ...report.sessionTruncated ? ["The session content was truncated to the most recent activity to fit the upload limit."] : [],
879
+ "Reports like this directly shape what gets fixed next."
880
+ ].join(" ");
881
+ }
882
+ export {
883
+ handleReportIssue
884
+ };
885
+ //# sourceMappingURL=report-issue-RHZZ33IS.js.map