boondmanager-mcp-server 2.15.2 → 2.17.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 (272) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +16 -2
  3. package/README.md +60 -15
  4. package/dist/config/access-policy.d.ts.map +1 -1
  5. package/dist/config/access-policy.js +6 -18
  6. package/dist/config/access-policy.js.map +1 -1
  7. package/dist/config/dictionary-overrides.d.ts.map +1 -1
  8. package/dist/config/dictionary-overrides.js +2 -12
  9. package/dist/config/dictionary-overrides.js.map +1 -1
  10. package/dist/config/env.d.ts +63 -0
  11. package/dist/config/env.d.ts.map +1 -0
  12. package/dist/config/env.js +116 -0
  13. package/dist/config/env.js.map +1 -0
  14. package/dist/config/profiles.d.ts +22 -8
  15. package/dist/config/profiles.d.ts.map +1 -1
  16. package/dist/config/profiles.js +48 -4
  17. package/dist/config/profiles.js.map +1 -1
  18. package/dist/constants.d.ts +13 -2
  19. package/dist/constants.d.ts.map +1 -1
  20. package/dist/constants.js +25 -1
  21. package/dist/constants.js.map +1 -1
  22. package/dist/icons.d.ts.map +1 -1
  23. package/dist/icons.js +6 -1
  24. package/dist/icons.js.map +1 -1
  25. package/dist/index.js +33 -15
  26. package/dist/index.js.map +1 -1
  27. package/dist/instructions.d.ts +1 -1
  28. package/dist/instructions.d.ts.map +1 -1
  29. package/dist/instructions.js +6 -6
  30. package/dist/instructions.js.map +1 -1
  31. package/dist/prompts/index.d.ts +19 -1
  32. package/dist/prompts/index.d.ts.map +1 -1
  33. package/dist/prompts/index.js +495 -25
  34. package/dist/prompts/index.js.map +1 -1
  35. package/dist/prompts/periods.d.ts +45 -0
  36. package/dist/prompts/periods.d.ts.map +1 -0
  37. package/dist/prompts/periods.js +154 -0
  38. package/dist/prompts/periods.js.map +1 -0
  39. package/dist/resources/index.d.ts +14 -0
  40. package/dist/resources/index.d.ts.map +1 -1
  41. package/dist/resources/index.js +285 -0
  42. package/dist/resources/index.js.map +1 -1
  43. package/dist/resources/templates.d.ts +1 -1
  44. package/dist/resources/templates.d.ts.map +1 -1
  45. package/dist/resources/templates.js +2 -0
  46. package/dist/resources/templates.js.map +1 -1
  47. package/dist/schema-dialect.js.map +1 -1
  48. package/dist/schemas/filter-aliases.d.ts +1 -1
  49. package/dist/schemas/filter-aliases.d.ts.map +1 -1
  50. package/dist/schemas/filter-aliases.js +101 -2
  51. package/dist/schemas/filter-aliases.js.map +1 -1
  52. package/dist/schemas/index.d.ts +987 -161
  53. package/dist/schemas/index.d.ts.map +1 -1
  54. package/dist/schemas/index.js +853 -323
  55. package/dist/schemas/index.js.map +1 -1
  56. package/dist/server.d.ts.map +1 -1
  57. package/dist/server.js +10 -3
  58. package/dist/server.js.map +1 -1
  59. package/dist/services/boond-client.d.ts +29 -213
  60. package/dist/services/boond-client.d.ts.map +1 -1
  61. package/dist/services/boond-client.js +29 -1168
  62. package/dist/services/boond-client.js.map +1 -1
  63. package/dist/services/dictionary.d.ts +27 -3
  64. package/dist/services/dictionary.d.ts.map +1 -1
  65. package/dist/services/dictionary.js +60 -23
  66. package/dist/services/dictionary.js.map +1 -1
  67. package/dist/services/document-text.d.ts +32 -0
  68. package/dist/services/document-text.d.ts.map +1 -0
  69. package/dist/services/document-text.js +120 -0
  70. package/dist/services/document-text.js.map +1 -0
  71. package/dist/services/format/detail.d.ts +16 -0
  72. package/dist/services/format/detail.d.ts.map +1 -0
  73. package/dist/services/format/detail.js +36 -0
  74. package/dist/services/format/detail.js.map +1 -0
  75. package/dist/services/format/html.d.ts +19 -0
  76. package/dist/services/format/html.d.ts.map +1 -0
  77. package/dist/services/format/html.js +87 -0
  78. package/dist/services/format/html.js.map +1 -0
  79. package/dist/services/format/list.d.ts +3 -0
  80. package/dist/services/format/list.d.ts.map +1 -0
  81. package/dist/services/format/list.js +44 -0
  82. package/dist/services/format/list.js.map +1 -0
  83. package/dist/services/format/summary.d.ts +16 -0
  84. package/dist/services/format/summary.d.ts.map +1 -0
  85. package/dist/services/format/summary.js +172 -0
  86. package/dist/services/format/summary.js.map +1 -0
  87. package/dist/services/format/tab.d.ts +9 -0
  88. package/dist/services/format/tab.d.ts.map +1 -0
  89. package/dist/services/format/tab.js +32 -0
  90. package/dist/services/format/tab.js.map +1 -0
  91. package/dist/services/http/auth.d.ts +44 -0
  92. package/dist/services/http/auth.d.ts.map +1 -0
  93. package/dist/services/http/auth.js +146 -0
  94. package/dist/services/http/auth.js.map +1 -0
  95. package/dist/services/http/download.d.ts +60 -0
  96. package/dist/services/http/download.d.ts.map +1 -0
  97. package/dist/services/http/download.js +160 -0
  98. package/dist/services/http/download.js.map +1 -0
  99. package/dist/services/http/errors.d.ts +45 -0
  100. package/dist/services/http/errors.d.ts.map +1 -0
  101. package/dist/services/http/errors.js +188 -0
  102. package/dist/services/http/errors.js.map +1 -0
  103. package/dist/services/http/rate-limit.d.ts +40 -0
  104. package/dist/services/http/rate-limit.d.ts.map +1 -0
  105. package/dist/services/http/rate-limit.js +92 -0
  106. package/dist/services/http/rate-limit.js.map +1 -0
  107. package/dist/services/http/retry.d.ts +46 -0
  108. package/dist/services/http/retry.d.ts.map +1 -0
  109. package/dist/services/http/retry.js +101 -0
  110. package/dist/services/http/retry.js.map +1 -0
  111. package/dist/services/http/transport.d.ts +73 -0
  112. package/dist/services/http/transport.d.ts.map +1 -0
  113. package/dist/services/http/transport.js +272 -0
  114. package/dist/services/http/transport.js.map +1 -0
  115. package/dist/services/logger.d.ts +53 -2
  116. package/dist/services/logger.d.ts.map +1 -1
  117. package/dist/services/logger.js +82 -21
  118. package/dist/services/logger.js.map +1 -1
  119. package/dist/services/oauth.d.ts +12 -0
  120. package/dist/services/oauth.d.ts.map +1 -1
  121. package/dist/services/oauth.js +21 -8
  122. package/dist/services/oauth.js.map +1 -1
  123. package/dist/services/rate-limiter.d.ts +9 -2
  124. package/dist/services/rate-limiter.d.ts.map +1 -1
  125. package/dist/services/rate-limiter.js +18 -6
  126. package/dist/services/rate-limiter.js.map +1 -1
  127. package/dist/services/request-context.d.ts +47 -0
  128. package/dist/services/request-context.d.ts.map +1 -0
  129. package/dist/services/request-context.js +112 -0
  130. package/dist/services/request-context.js.map +1 -0
  131. package/dist/services/search.d.ts +26 -0
  132. package/dist/services/search.d.ts.map +1 -0
  133. package/dist/services/search.js +98 -0
  134. package/dist/services/search.js.map +1 -0
  135. package/dist/services/update-checker.d.ts.map +1 -1
  136. package/dist/services/update-checker.js +15 -8
  137. package/dist/services/update-checker.js.map +1 -1
  138. package/dist/tools/absences.d.ts +9 -0
  139. package/dist/tools/absences.d.ts.map +1 -1
  140. package/dist/tools/absences.js +65 -20
  141. package/dist/tools/absences.js.map +1 -1
  142. package/dist/tools/actions.d.ts.map +1 -1
  143. package/dist/tools/actions.js +11 -27
  144. package/dist/tools/actions.js.map +1 -1
  145. package/dist/tools/advantages.d.ts +2 -0
  146. package/dist/tools/advantages.d.ts.map +1 -1
  147. package/dist/tools/advantages.js +66 -13
  148. package/dist/tools/advantages.js.map +1 -1
  149. package/dist/tools/alerts.d.ts +18 -0
  150. package/dist/tools/alerts.d.ts.map +1 -0
  151. package/dist/tools/alerts.js +82 -0
  152. package/dist/tools/alerts.js.map +1 -0
  153. package/dist/tools/application.d.ts +2 -1
  154. package/dist/tools/application.d.ts.map +1 -1
  155. package/dist/tools/application.js +9 -3
  156. package/dist/tools/application.js.map +1 -1
  157. package/dist/tools/contacts.d.ts.map +1 -1
  158. package/dist/tools/contacts.js +3 -7
  159. package/dist/tools/contacts.js.map +1 -1
  160. package/dist/tools/contracts.d.ts +22 -0
  161. package/dist/tools/contracts.d.ts.map +1 -1
  162. package/dist/tools/contracts.js +185 -52
  163. package/dist/tools/contracts.js.map +1 -1
  164. package/dist/tools/crud-factory.d.ts +33 -2
  165. package/dist/tools/crud-factory.d.ts.map +1 -1
  166. package/dist/tools/crud-factory.js +42 -15
  167. package/dist/tools/crud-factory.js.map +1 -1
  168. package/dist/tools/deliveries.d.ts +3 -0
  169. package/dist/tools/deliveries.d.ts.map +1 -1
  170. package/dist/tools/deliveries.js +50 -94
  171. package/dist/tools/deliveries.js.map +1 -1
  172. package/dist/tools/description-builders.d.ts +0 -1
  173. package/dist/tools/description-builders.d.ts.map +1 -1
  174. package/dist/tools/description-builders.js +46 -3
  175. package/dist/tools/description-builders.js.map +1 -1
  176. package/dist/tools/documents.d.ts.map +1 -1
  177. package/dist/tools/documents.js +73 -21
  178. package/dist/tools/documents.js.map +1 -1
  179. package/dist/tools/expenses.d.ts.map +1 -1
  180. package/dist/tools/expenses.js +2 -11
  181. package/dist/tools/expenses.js.map +1 -1
  182. package/dist/tools/find.d.ts +61 -0
  183. package/dist/tools/find.d.ts.map +1 -0
  184. package/dist/tools/find.js +221 -0
  185. package/dist/tools/find.js.map +1 -0
  186. package/dist/tools/flags.d.ts +5 -0
  187. package/dist/tools/flags.d.ts.map +1 -1
  188. package/dist/tools/flags.js +116 -1
  189. package/dist/tools/flags.js.map +1 -1
  190. package/dist/tools/forms.d.ts +5 -0
  191. package/dist/tools/forms.d.ts.map +1 -0
  192. package/dist/tools/forms.js +64 -0
  193. package/dist/tools/forms.js.map +1 -0
  194. package/dist/tools/groupments.d.ts +5 -0
  195. package/dist/tools/groupments.d.ts.map +1 -0
  196. package/dist/tools/groupments.js +68 -0
  197. package/dist/tools/groupments.js.map +1 -0
  198. package/dist/tools/inactivities.d.ts +5 -0
  199. package/dist/tools/inactivities.d.ts.map +1 -0
  200. package/dist/tools/inactivities.js +59 -0
  201. package/dist/tools/inactivities.js.map +1 -0
  202. package/dist/tools/index.d.ts +4 -0
  203. package/dist/tools/index.d.ts.map +1 -1
  204. package/dist/tools/index.js +4 -0
  205. package/dist/tools/index.js.map +1 -1
  206. package/dist/tools/invoices.d.ts.map +1 -1
  207. package/dist/tools/invoices.js +27 -2
  208. package/dist/tools/invoices.js.map +1 -1
  209. package/dist/tools/linked-entity-filters.d.ts +48 -0
  210. package/dist/tools/linked-entity-filters.d.ts.map +1 -0
  211. package/dist/tools/linked-entity-filters.js +64 -0
  212. package/dist/tools/linked-entity-filters.js.map +1 -0
  213. package/dist/tools/notifications.d.ts.map +1 -1
  214. package/dist/tools/notifications.js +0 -5
  215. package/dist/tools/notifications.js.map +1 -1
  216. package/dist/tools/orders.d.ts.map +1 -1
  217. package/dist/tools/orders.js +33 -4
  218. package/dist/tools/orders.js.map +1 -1
  219. package/dist/tools/parameter-disclosure.js.map +1 -1
  220. package/dist/tools/payments.d.ts +3 -0
  221. package/dist/tools/payments.d.ts.map +1 -1
  222. package/dist/tools/payments.js +43 -109
  223. package/dist/tools/payments.js.map +1 -1
  224. package/dist/tools/positionings.d.ts.map +1 -1
  225. package/dist/tools/positionings.js +9 -39
  226. package/dist/tools/positionings.js.map +1 -1
  227. package/dist/tools/projects.d.ts.map +1 -1
  228. package/dist/tools/projects.js +5 -12
  229. package/dist/tools/projects.js.map +1 -1
  230. package/dist/tools/provider-invoices.d.ts +2 -0
  231. package/dist/tools/provider-invoices.d.ts.map +1 -1
  232. package/dist/tools/provider-invoices.js +30 -111
  233. package/dist/tools/provider-invoices.js.map +1 -1
  234. package/dist/tools/purchases.d.ts +2 -0
  235. package/dist/tools/purchases.d.ts.map +1 -1
  236. package/dist/tools/purchases.js +50 -104
  237. package/dist/tools/purchases.js.map +1 -1
  238. package/dist/tools/registration-decorators.d.ts +28 -0
  239. package/dist/tools/registration-decorators.d.ts.map +1 -1
  240. package/dist/tools/registration-decorators.js +107 -0
  241. package/dist/tools/registration-decorators.js.map +1 -1
  242. package/dist/tools/resources.d.ts.map +1 -1
  243. package/dist/tools/resources.js +27 -5
  244. package/dist/tools/resources.js.map +1 -1
  245. package/dist/tools/rights.d.ts +101 -0
  246. package/dist/tools/rights.d.ts.map +1 -0
  247. package/dist/tools/rights.js +75 -0
  248. package/dist/tools/rights.js.map +1 -0
  249. package/dist/tools/tab-tools.js +3 -3
  250. package/dist/tools/tab-tools.js.map +1 -1
  251. package/dist/tools/timesheets.d.ts +29 -0
  252. package/dist/tools/timesheets.d.ts.map +1 -1
  253. package/dist/tools/timesheets.js +196 -126
  254. package/dist/tools/timesheets.js.map +1 -1
  255. package/dist/tools/todolists.d.ts +2 -0
  256. package/dist/tools/todolists.d.ts.map +1 -1
  257. package/dist/tools/todolists.js +64 -1
  258. package/dist/tools/todolists.js.map +1 -1
  259. package/dist/tools/validations.d.ts +9 -0
  260. package/dist/tools/validations.d.ts.map +1 -1
  261. package/dist/tools/validations.js +86 -9
  262. package/dist/tools/validations.js.map +1 -1
  263. package/dist/tools/workflows.js +1 -1
  264. package/dist/tools/workflows.js.map +1 -1
  265. package/dist/transports/http.d.ts +116 -0
  266. package/dist/transports/http.d.ts.map +1 -1
  267. package/dist/transports/http.js +422 -92
  268. package/dist/transports/http.js.map +1 -1
  269. package/dist/types.d.ts +3 -3
  270. package/dist/types.d.ts.map +1 -1
  271. package/manifest.json +7 -7
  272. package/package.json +14 -10
@@ -1,10 +1,12 @@
1
1
  import pino from "pino";
2
+ import { readString } from "../config/env.js";
2
3
  import { randomUUID } from "node:crypto";
4
+ import { createRequire } from "node:module";
3
5
  /**
4
6
  * Redaction paths for the structured logger. Defence-in-depth: nothing in the
5
7
  * codebase currently logs auth material, but if a request/error object that
6
8
  * carries credentials is ever passed to the logger, these paths censor it
7
- * before it reaches stdout / a log aggregator. Covers the BoondManager JWT
9
+ * before it reaches stderr / a log aggregator. Covers the BoondManager JWT
8
10
  * header, OAuth Bearer headers, and raw access tokens at one level of nesting.
9
11
  */
10
12
  export const REDACT_PATHS = [
@@ -20,42 +22,101 @@ export const REDACT_PATHS = [
20
22
  "accessToken",
21
23
  "*.accessToken",
22
24
  ];
25
+ /**
26
+ * File descriptor every log line is written to: **stderr, on every transport**.
27
+ *
28
+ * On the stdio transport stdout *is* the JSON-RPC channel — anything that is
29
+ * not an MCP message corrupts the stream and the client drops the connection.
30
+ * Pino defaults to fd 1, and so does the pino-pretty transport when no
31
+ * `destination` is given, which is exactly how the "Access policy active"
32
+ * line, the update notice and the dictionary-override warnings used to land in
33
+ * the middle of the protocol (issue #225). stderr is what Claude Desktop
34
+ * captures into its log viewer, and it is equally fine for the HTTP transport,
35
+ * so there is one destination rather than one per transport.
36
+ */
37
+ export const LOG_DESTINATION_FD = 2;
38
+ const VALID_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"];
23
39
  /**
24
40
  * Read the log level from env, falling back to 'info' for production-friendly
25
41
  * defaults. DEBUG / trace logs are useful during development but too noisy
26
42
  * in production. Pino's level hierarchy: trace < debug < info < warn < error < fatal.
27
43
  */
28
- function resolveLogLevel() {
29
- const raw = process.env.LOG_LEVEL?.toLowerCase();
30
- const valid = ["trace", "debug", "info", "warn", "error", "fatal"];
31
- if (raw && valid.includes(raw))
44
+ export function resolveLogLevel(env = process.env) {
45
+ const raw = readString("LOG_LEVEL", env)?.trim().toLowerCase();
46
+ if (raw && VALID_LEVELS.includes(raw))
32
47
  return raw;
33
48
  return "info";
34
49
  }
50
+ /** Human-readable (pretty) output in dev, JSON in prod. Override via LOG_FORMAT. */
51
+ export function usePrettyOutput(env = process.env) {
52
+ return readString("LOG_FORMAT", env)?.trim() !== "json" && readString("NODE_ENV", env)?.trim() !== "production";
53
+ }
54
+ /**
55
+ * `pino-pretty` is a **devDependency** (issue #246): it only serves the
56
+ * human-readable output of a development shell, and the `.mcpb` bundle and
57
+ * the Docker image are built with `--omit=dev`. Pino loads a transport by
58
+ * module name in a worker thread and throws at logger creation when the
59
+ * target is missing, so the pretty branch is taken only when the module
60
+ * resolves — otherwise the logger silently falls back to JSON on stderr,
61
+ * which is the production shape anyway.
62
+ */
63
+ export function isPrettyTransportAvailable() {
64
+ try {
65
+ createRequire(import.meta.url).resolve("pino-pretty");
66
+ return true;
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ /**
73
+ * The two shapes the logger can take, as data — so a test can assert where each
74
+ * one writes without spawning a process. Pino refuses a stream argument when
75
+ * `transport` is set (the transport runs in a worker thread), which is why the
76
+ * destination travels inside the pino-pretty options on one branch and as a
77
+ * `pino.destination()` on the other.
78
+ */
79
+ export function resolveLoggerConfig(env = process.env, prettyAvailable = isPrettyTransportAvailable()) {
80
+ const base = {
81
+ level: resolveLogLevel(env),
82
+ redact: { paths: REDACT_PATHS, censor: "[Redacted]" },
83
+ };
84
+ if (usePrettyOutput(env) && prettyAvailable) {
85
+ return {
86
+ format: "pretty",
87
+ options: {
88
+ ...base,
89
+ transport: {
90
+ target: "pino-pretty",
91
+ options: {
92
+ colorize: true,
93
+ translateTime: "SYS:standard",
94
+ ignore: "pid,hostname",
95
+ destination: LOG_DESTINATION_FD,
96
+ },
97
+ },
98
+ },
99
+ };
100
+ }
101
+ return { format: "json", options: base, destinationFd: LOG_DESTINATION_FD };
102
+ }
103
+ export function createLogger(env = process.env) {
104
+ const config = resolveLoggerConfig(env);
105
+ if (config.format === "pretty")
106
+ return pino(config.options);
107
+ return pino(config.options, pino.destination(config.destinationFd));
108
+ }
35
109
  /**
36
110
  * Centralized structured logger. Use this instead of console.log/error for
37
111
  * all application logging — it provides timestamps, levels, and JSON output
38
- * (when LOG_FORMAT=json) that plays nicely with log aggregators.
112
+ * (when LOG_FORMAT=json) that plays nicely with log aggregators. It always
113
+ * writes to stderr (see `LOG_DESTINATION_FD`).
39
114
  *
40
115
  * Example:
41
116
  * logger.info({ sessionId: "abc", userId: 123 }, "Session initialized");
42
117
  * logger.error({ err, endpoint: "/mcp" }, "HTTP transport error");
43
118
  */
44
- export const logger = pino({
45
- level: resolveLogLevel(),
46
- redact: { paths: REDACT_PATHS, censor: "[Redacted]" },
47
- // Human-readable (pretty) output in dev, JSON in prod. Override via LOG_FORMAT.
48
- transport: process.env.LOG_FORMAT === "json" || process.env.NODE_ENV === "production"
49
- ? undefined
50
- : {
51
- target: "pino-pretty",
52
- options: {
53
- colorize: true,
54
- translateTime: "SYS:standard",
55
- ignore: "pid,hostname",
56
- },
57
- },
58
- });
119
+ export const logger = createLogger();
59
120
  /**
60
121
  * Generate a short correlation ID (8 hex chars) for tracing a single request
61
122
  * through the stack (HTTP handler → tool call → API request). Attach it to
@@ -1 +1 @@
1
- {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/services/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,eAAe;IACf,eAAe;IACf,iBAAiB;IACjB,iBAAiB;IACjB,uBAAuB;IACvB,2BAA2B;IAC3B,2BAA2B;IAC3B,sCAAsC;IACtC,0CAA0C;IAC1C,aAAa;IACb,eAAe;CAChB,CAAC;AAEF;;;;GAIG;AACH,SAAS,eAAe;IACtB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC;IACjD,MAAM,KAAK,GAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACjF,IAAI,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAiB,CAAC;QAAE,OAAO,GAAiB,CAAC;IACvE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,eAAe,EAAE;IACxB,MAAM,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE;IACrD,gFAAgF;IAChF,SAAS,EACP,OAAO,CAAC,GAAG,CAAC,UAAU,KAAK,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;QACxE,CAAC,CAAC,SAAS;QACX,CAAC,CAAC;YACE,MAAM,EAAE,aAAa;YACrB,OAAO,EAAE;gBACP,QAAQ,EAAE,IAAI;gBACd,aAAa,EAAE,cAAc;gBAC7B,MAAM,EAAE,cAAc;aACvB;SACF;CACR,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,qBAAqB;IACnC,qEAAqE;IACrE,wEAAwE;IACxE,uDAAuD;IACvD,OAAO,UAAU,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACpD,CAAC"}
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/services/logger.ts"],"names":[],"mappings":"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"}
@@ -32,6 +32,18 @@ export interface OAuthRequestContext {
32
32
  * on env-var credentials.
33
33
  */
34
34
  export declare const oauthContext: AsyncLocalStorage<OAuthRequestContext>;
35
+ /**
36
+ * Identity of the caller, as far as this process can tell — the key every
37
+ * per-user structure is partitioned on (dictionary cache #226, rate-limit
38
+ * buckets and session ownership #232).
39
+ *
40
+ * - OAuth (HTTP transport): the request's Bearer token, hashed so the raw
41
+ * credential never sits in a long-lived structure. Two users of the same
42
+ * tenant get two identities — the price of not decoding an opaque token.
43
+ * - Everything else (stdio, HTTP static auth): the credentials are process
44
+ * wide, so a single constant identity is exact.
45
+ */
46
+ export declare function currentAuthIdentity(): string;
35
47
  /**
36
48
  * Extract a Bearer token from an HTTP `Authorization` header.
37
49
  * Returns `null` for missing, malformed, or non-Bearer auth schemes.
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/services/oauth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,mBAAmB;IAClC,sFAAsF;IACtF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,wCAA+C,CAAC;AAEzE;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAevF;AAED,gEAAgE;AAChE,eAAO,MAAM,4BAA4B,gCAAgC,CAAC;AAE1E,MAAM,WAAW,gCAAgC;IAC/C,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,mDAAmD;IACnD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,gCAAgC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAU9G;AAQD;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,EAAE,CAIlD"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/services/oauth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAIrD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,mBAAmB;IAClC,sFAAsF;IACtF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,wCAA+C,CAAC;AAEzE;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAI5C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAevF;AAED,gEAAgE;AAChE,eAAO,MAAM,4BAA4B,gCAAgC,CAAC;AAE1E,MAAM,WAAW,gCAAgC;IAC/C,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,mDAAmD;IACnD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,gCAAgC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAU9G;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,EAAE,CAIlD"}
@@ -1,4 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { readString, readUrl } from "../config/env.js";
3
+ import { createHash } from "node:crypto";
2
4
  /**
3
5
  * Per-request context, populated by the HTTP transport before dispatching
4
6
  * to the MCP SDK and read by `oauthContextAuth` (boond-client) when an API
@@ -6,6 +8,23 @@ import { AsyncLocalStorage } from "node:async_hooks";
6
8
  * on env-var credentials.
7
9
  */
8
10
  export const oauthContext = new AsyncLocalStorage();
11
+ /**
12
+ * Identity of the caller, as far as this process can tell — the key every
13
+ * per-user structure is partitioned on (dictionary cache #226, rate-limit
14
+ * buckets and session ownership #232).
15
+ *
16
+ * - OAuth (HTTP transport): the request's Bearer token, hashed so the raw
17
+ * credential never sits in a long-lived structure. Two users of the same
18
+ * tenant get two identities — the price of not decoding an opaque token.
19
+ * - Everything else (stdio, HTTP static auth): the credentials are process
20
+ * wide, so a single constant identity is exact.
21
+ */
22
+ export function currentAuthIdentity() {
23
+ const ctx = oauthContext.getStore();
24
+ if (!ctx)
25
+ return "env";
26
+ return `oauth:${createHash("sha256").update(ctx.accessToken).digest("hex")}`;
27
+ }
9
28
  /**
10
29
  * Extract a Bearer token from an HTTP `Authorization` header.
11
30
  * Returns `null` for missing, malformed, or non-Bearer auth schemes.
@@ -57,26 +76,20 @@ export function buildProtectedResourceMetadata(opts) {
57
76
  }
58
77
  return doc;
59
78
  }
60
- function envOrUndefined(key) {
61
- const v = process.env[key];
62
- if (!v || v.startsWith("${"))
63
- return undefined;
64
- return v;
65
- }
66
79
  /**
67
80
  * Resolve the authorization server URL surfaced in the discovery metadata.
68
81
  * Configurable so dedicated BoondManager instances (custom hostnames) can
69
82
  * advertise the right issuer.
70
83
  */
71
84
  export function resolveAuthorizationServer() {
72
- return envOrUndefined("BOOND_OAUTH_AUTHORIZATION_SERVER") ?? DEFAULT_AUTHORIZATION_SERVER;
85
+ return readUrl("BOOND_OAUTH_AUTHORIZATION_SERVER") ?? DEFAULT_AUTHORIZATION_SERVER;
73
86
  }
74
87
  /**
75
88
  * Optional scope list advertised in the discovery metadata. Lets the MCP
76
89
  * client request appropriate scopes when initiating the OAuth flow.
77
90
  */
78
91
  export function resolveAdvertisedScopes() {
79
- const raw = envOrUndefined("BOOND_OAUTH_SCOPES");
92
+ const raw = readString("BOOND_OAUTH_SCOPES");
80
93
  if (!raw)
81
94
  return [];
82
95
  return raw.split(/[\s,]+/).filter((s) => s.length > 0);
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/services/oauth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AA8BrD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,iBAAiB,EAAuB,CAAC;AAEzE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAqC;IACtE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACzD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,yEAAyE;IACzE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAChE,yEAAyE;IACzE,qEAAqE;IACrE,kEAAkE;IAClE,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAClC,IAAI,GAAG,KAAK,IAAI,CAAC,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAChE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACtC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC;AAED,gEAAgE;AAChE,MAAM,CAAC,MAAM,4BAA4B,GAAG,6BAA6B,CAAC;AAW1E;;;;;GAKG;AACH,MAAM,UAAU,8BAA8B,CAAC,IAAsC;IACnF,MAAM,GAAG,GAA4B;QACnC,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,qBAAqB,EAAE,IAAI,CAAC,oBAAoB;QAChD,wBAAwB,EAAE,CAAC,QAAQ,CAAC;KACrC,CAAC;IACF,IAAI,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAAC,eAAe,CAAC;IACjD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/C,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B;IACxC,OAAO,cAAc,CAAC,kCAAkC,CAAC,IAAI,4BAA4B,CAAC;AAC5F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,GAAG,GAAG,cAAc,CAAC,oBAAoB,CAAC,CAAC;IACjD,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,OAAO,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACzD,CAAC"}
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/services/oauth.ts"],"names":[],"mappings":"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"}
@@ -26,8 +26,15 @@ export declare class TokenBucket {
26
26
  private lastRefill;
27
27
  private chain;
28
28
  constructor(capacity: number, refillPerSec: number, clock?: Clock);
29
- /** Wait until a token is available, then consume it. */
30
- acquire(): Promise<void>;
29
+ /**
30
+ * Wait until a token is available, then consume it.
31
+ *
32
+ * `signal` (issue #231): a caller that has been cancelled must not sit in
33
+ * the queue — the promise rejects with the signal's reason at once if it
34
+ * has already fired, or as soon as it fires while waiting for a refill.
35
+ * The chain is not poisoned: the next acquirer proceeds normally.
36
+ */
37
+ acquire(signal?: AbortSignal): Promise<void>;
31
38
  /**
32
39
  * Number of currently-available tokens (after refill). Visible for tests
33
40
  * and observability; do not use for control flow.
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/services/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,KAAK;IACpB,GAAG,IAAI,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,eAAO,MAAM,SAAS,EAAE,KAIvB,CAAC;AAEF,qBAAa,WAAW;aAMJ,QAAQ,EAAE,MAAM;aAChB,YAAY,EAAE,MAAM;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK;IAPxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,KAAK,CAAoC;gBAG/B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACnB,KAAK,GAAE,KAAiB;IAQ3C,wDAAwD;IACxD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IASxB;;;OAGG;IACH,IAAI,IAAI,MAAM;YAKA,OAAO;IAWrB,OAAO,CAAC,MAAM;CAOf"}
1
+ {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/services/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,WAAW,KAAK;IACpB,GAAG,IAAI,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,eAAO,MAAM,SAAS,EAAE,KAGvB,CAAC;AAEF,qBAAa,WAAW;aAMJ,QAAQ,EAAE,MAAM;aAChB,YAAY,EAAE,MAAM;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK;IAPxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,KAAK,CAAoC;gBAG/B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACnB,KAAK,GAAE,KAAiB;IAQ3C;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5C;;;OAGG;IACH,IAAI,IAAI,MAAM;YAKA,OAAO;IAYrB,OAAO,CAAC,MAAM;CAOf"}
@@ -13,9 +13,10 @@
13
13
  * the same token. The chain forces consume() to run one at a time, so the
14
14
  * in-memory token count is always consistent.
15
15
  */
16
+ import { abortPromise } from "./request-context.js";
16
17
  export const realClock = {
17
18
  now: () => Date.now(),
18
- sleep: (ms) => ms <= 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, ms)),
19
+ sleep: (ms) => (ms <= 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, ms))),
19
20
  };
20
21
  export class TokenBucket {
21
22
  capacity;
@@ -35,9 +36,18 @@ export class TokenBucket {
35
36
  this.tokens = capacity;
36
37
  this.lastRefill = clock.now();
37
38
  }
38
- /** Wait until a token is available, then consume it. */
39
- acquire() {
40
- const next = this.chain.then(() => this.consume());
39
+ /**
40
+ * Wait until a token is available, then consume it.
41
+ *
42
+ * `signal` (issue #231): a caller that has been cancelled must not sit in
43
+ * the queue — the promise rejects with the signal's reason at once if it
44
+ * has already fired, or as soon as it fires while waiting for a refill.
45
+ * The chain is not poisoned: the next acquirer proceeds normally.
46
+ */
47
+ acquire(signal) {
48
+ if (signal?.aborted)
49
+ return Promise.reject(signal.reason ?? new Error("aborted"));
50
+ const next = this.chain.then(() => this.consume(signal));
41
51
  // Swallow the rejection on the chain itself so a single failing consume
42
52
  // (which shouldn't happen, but be defensive) does not poison every later
43
53
  // acquire. Callers still see their own promise resolve/reject normally.
@@ -52,12 +62,14 @@ export class TokenBucket {
52
62
  this.refill();
53
63
  return this.tokens;
54
64
  }
55
- async consume() {
65
+ async consume(signal) {
56
66
  this.refill();
57
67
  while (this.tokens < 1) {
68
+ if (signal?.aborted)
69
+ throw signal.reason ?? new Error("aborted");
58
70
  const deficit = 1 - this.tokens;
59
71
  const waitMs = Math.max(1, Math.ceil((deficit / this.refillPerSec) * 1000));
60
- await this.clock.sleep(waitMs);
72
+ await (signal ? Promise.race([this.clock.sleep(waitMs), abortPromise(signal)]) : this.clock.sleep(waitMs));
61
73
  this.refill();
62
74
  }
63
75
  this.tokens -= 1;
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/services/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,MAAM,CAAC,MAAM,SAAS,GAAU;IAC9B,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;IACrB,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CACZ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;CAClF,CAAC;AAEF,MAAM,OAAO,WAAW;IAMJ;IACA;IACC;IAPX,MAAM,CAAS;IACf,UAAU,CAAS;IACnB,KAAK,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAEjD,YACkB,QAAgB,EAChB,YAAoB,EACnB,QAAe,SAAS;QAFzB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,iBAAY,GAAZ,YAAY,CAAQ;QACnB,UAAK,GAAL,KAAK,CAAmB;QAEzC,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QAC1E,IAAI,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAClF,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC;IAChC,CAAC;IAED,wDAAwD;IACxD,OAAO;QACL,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACnD,wEAAwE;QACxE,yEAAyE;QACzE,wEAAwE;QACxE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,IAAI;QACF,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;YAChC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;YAC5E,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,CAAC;QACD,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;IACnB,CAAC;IAEO,MAAM;QACZ,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/D,IAAI,UAAU,KAAK,CAAC;YAAE,OAAO;QAC7B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;QACpF,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;IACxB,CAAC;CACF"}
1
+ {"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/services/rate-limiter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ export interface RequestContext {
2
+ /** Fires on `notifications/cancelled` or when the transport closes. */
3
+ signal?: AbortSignal;
4
+ /** Correlation id shared by every log line and forwarded as `X-Request-Id`. */
5
+ corrId?: string;
6
+ /** W3C `traceparent` header received from the client, forwarded verbatim. */
7
+ traceparent?: string;
8
+ }
9
+ /** Run `fn` with `context` as the current request's context (replaces any enclosing one). */
10
+ export declare function runWithRequestContext<T>(context: RequestContext, fn: () => T): T;
11
+ /** The context of the request being handled, if a handler is on the stack. */
12
+ export declare function currentRequestContext(): RequestContext | undefined;
13
+ /** Run `fn` with `signal` as the current request's cancellation signal; the rest of the context is kept. */
14
+ export declare function runWithRequestSignal<T>(signal: AbortSignal | undefined, fn: () => T): T;
15
+ /** Run `fn` with no cancellation signal, for work whose result other requests share. Correlation is kept. */
16
+ export declare function withoutRequestSignal<T>(fn: () => T): T;
17
+ /** The signal of the request being handled, if a handler is on the stack. */
18
+ export declare function currentRequestSignal(): AbortSignal | undefined;
19
+ /** The correlation id of the request being handled, if any. */
20
+ export declare function currentCorrId(): string | undefined;
21
+ /** Narrowing for the SDK's `extra`, typed `unknown` at every call site. */
22
+ export declare function requestSignalFrom(extra: unknown): AbortSignal | undefined;
23
+ /** Parse a `traceparent` value; `undefined` when absent or malformed. */
24
+ export declare function parseTraceparent(value: unknown): {
25
+ header: string;
26
+ traceId: string;
27
+ } | undefined;
28
+ /** The `traceparent` the client put in the request's `_meta` (SEP-414), if valid. */
29
+ export declare function traceparentFrom(extra: unknown): {
30
+ header: string;
31
+ traceId: string;
32
+ } | undefined;
33
+ /**
34
+ * Raised when the client cancelled the request. `name` is `AbortError` so it
35
+ * reads like every other abort in Node; the message names the endpoint the
36
+ * cancellation interrupted, which is what a log line needs.
37
+ */
38
+ export declare class RequestCancelledError extends Error {
39
+ readonly method: string;
40
+ readonly path: string;
41
+ constructor(method: string, path: string, cause?: unknown);
42
+ }
43
+ /** Throw `RequestCancelledError` if `signal` has fired. Cheap — call it before every unit of work. */
44
+ export declare function throwIfCancelled(signal: AbortSignal | undefined, method: string, path: string): void;
45
+ /** A promise that rejects with the signal's reason the moment it fires (never resolves). */
46
+ export declare function abortPromise(signal: AbortSignal): Promise<never>;
47
+ //# sourceMappingURL=request-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/services/request-context.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEhF;AAED,8EAA8E;AAC9E,wBAAgB,qBAAqB,IAAI,cAAc,GAAG,SAAS,CAElE;AAED,4GAA4G;AAC5G,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAIvF;AAED,6GAA6G;AAC7G,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEtD;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,IAAI,WAAW,GAAG,SAAS,CAE9D;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAElD;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAIzE;AAQD,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAMhG;AAED,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAK/F;AAED;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBACV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAS1D;AAED,sGAAsG;AACtG,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAEpG;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,CAIhE"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Per-request context: cancellation (issue #231) and correlation (issue #236).
3
+ *
4
+ * The SDK hands every handler an `extra.signal` — an `AbortSignal` fired by
5
+ * `notifications/cancelled` or by the transport closing. Before #231 nothing
6
+ * forwarded it: a chunked `/actions` search (up to 5 sequential calls), a
7
+ * reporting query of tens of seconds or a 5 MiB download ran to completion
8
+ * after the client had given up, spending rate-limit tokens and BoondManager
9
+ * quota on a result nobody would read.
10
+ *
11
+ * Rather than threading `signal` / `corrId` arguments through ~180 handler
12
+ * signatures, the context travels in an `AsyncLocalStorage`:
13
+ * `instrumentHandlers()` (`tools/registration-decorators.ts`) runs each handler
14
+ * inside `runWithRequestContext`, and `send()` (`http/transport.ts`) reads it
15
+ * back — the same pattern as the OAuth token (`oauthContext`). Work that is
16
+ * *shared* between requests (the dictionary cache's in-flight load, #226) opts
17
+ * out of cancellation with `withoutRequestSignal`: one caller cancelling must
18
+ * not fail the load every other caller is waiting on.
19
+ *
20
+ * `corrId` is the correlation id every log line of the request carries and
21
+ * that `send()` forwards to BoondManager as `X-Request-Id`. It is, in order:
22
+ * the W3C `trace-id` of a `_meta.traceparent` the client sent (SEP-414), the
23
+ * id the HTTP transport generated for the connection, or a fresh one (stdio).
24
+ * `traceparent` is forwarded verbatim when present. **`baggage` is never read
25
+ * and never logged** — it may carry end-user identifiers.
26
+ */
27
+ import { AsyncLocalStorage } from "node:async_hooks";
28
+ const storage = new AsyncLocalStorage();
29
+ /** Run `fn` with `context` as the current request's context (replaces any enclosing one). */
30
+ export function runWithRequestContext(context, fn) {
31
+ return storage.run(context, fn);
32
+ }
33
+ /** The context of the request being handled, if a handler is on the stack. */
34
+ export function currentRequestContext() {
35
+ return storage.getStore();
36
+ }
37
+ /** Run `fn` with `signal` as the current request's cancellation signal; the rest of the context is kept. */
38
+ export function runWithRequestSignal(signal, fn) {
39
+ // Drop the enclosing signal rather than storing `undefined` under the key.
40
+ const { signal: _enclosing, ...rest } = storage.getStore() ?? {};
41
+ return storage.run(signal ? { ...rest, signal } : rest, fn);
42
+ }
43
+ /** Run `fn` with no cancellation signal, for work whose result other requests share. Correlation is kept. */
44
+ export function withoutRequestSignal(fn) {
45
+ return runWithRequestSignal(undefined, fn);
46
+ }
47
+ /** The signal of the request being handled, if a handler is on the stack. */
48
+ export function currentRequestSignal() {
49
+ return storage.getStore()?.signal;
50
+ }
51
+ /** The correlation id of the request being handled, if any. */
52
+ export function currentCorrId() {
53
+ return storage.getStore()?.corrId;
54
+ }
55
+ /** Narrowing for the SDK's `extra`, typed `unknown` at every call site. */
56
+ export function requestSignalFrom(extra) {
57
+ if (typeof extra !== "object" || extra === null)
58
+ return undefined;
59
+ const signal = extra.signal;
60
+ return signal instanceof AbortSignal ? signal : undefined;
61
+ }
62
+ /**
63
+ * W3C Trace Context `traceparent`: `version-traceid-parentid-flags`, lower-case
64
+ * hex, trace-id not all zeros. Anything else is ignored rather than trusted.
65
+ */
66
+ const TRACEPARENT_RE = /^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/;
67
+ /** Parse a `traceparent` value; `undefined` when absent or malformed. */
68
+ export function parseTraceparent(value) {
69
+ if (typeof value !== "string")
70
+ return undefined;
71
+ const match = TRACEPARENT_RE.exec(value.trim());
72
+ const traceId = match?.[1];
73
+ if (!match || !traceId || /^0+$/.test(traceId))
74
+ return undefined;
75
+ return { header: match[0], traceId };
76
+ }
77
+ /** The `traceparent` the client put in the request's `_meta` (SEP-414), if valid. */
78
+ export function traceparentFrom(extra) {
79
+ if (typeof extra !== "object" || extra === null)
80
+ return undefined;
81
+ const meta = extra._meta;
82
+ if (typeof meta !== "object" || meta === null)
83
+ return undefined;
84
+ return parseTraceparent(meta.traceparent);
85
+ }
86
+ /**
87
+ * Raised when the client cancelled the request. `name` is `AbortError` so it
88
+ * reads like every other abort in Node; the message names the endpoint the
89
+ * cancellation interrupted, which is what a log line needs.
90
+ */
91
+ export class RequestCancelledError extends Error {
92
+ method;
93
+ path;
94
+ constructor(method, path, cause) {
95
+ super(`BoondManager API request cancelled by the client (notifications/cancelled or transport closed).\nEndpoint: ${method} ${path}`, cause === undefined ? undefined : { cause });
96
+ this.name = "AbortError";
97
+ this.method = method;
98
+ this.path = path;
99
+ }
100
+ }
101
+ /** Throw `RequestCancelledError` if `signal` has fired. Cheap — call it before every unit of work. */
102
+ export function throwIfCancelled(signal, method, path) {
103
+ if (signal?.aborted)
104
+ throw new RequestCancelledError(method, path, signal.reason);
105
+ }
106
+ /** A promise that rejects with the signal's reason the moment it fires (never resolves). */
107
+ export function abortPromise(signal) {
108
+ return new Promise((_, reject) => {
109
+ signal.addEventListener("abort", () => reject(signal.reason ?? new Error("aborted")), { once: true });
110
+ });
111
+ }
112
+ //# sourceMappingURL=request-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-context.js","sourceRoot":"","sources":["../../src/services/request-context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { JsonApiResponse, SearchParams } from "../types.js";
2
+ import { type QueryValue } from "./http/transport.js";
3
+ import type { ProgressReporter } from "./progress.js";
4
+ export declare function buildSearchQuery(params: SearchParams): Record<string, QueryValue>;
5
+ /**
6
+ * Search wrapper around `apiRequest` that enforces BoondManager's per-route
7
+ * `maxResults` ceiling (see `ROUTE_MAX_RESULTS`). When the caller requests more
8
+ * results than the route allows, the request is transparently split into
9
+ * chunks of `cap` records and the pages are merged into a single JSON:API
10
+ * response — the caller still receives the full page, but BoondManager never
11
+ * sees `maxResults` above the cap (which overflows memory on `/actions`).
12
+ *
13
+ * Routes whose ceiling already covers the requested page size take the fast
14
+ * path: a single `apiRequest`, byte-for-byte identical to calling it directly.
15
+ * The chunk count is bounded by `ceil((offset + requested) / cap)`, so there is
16
+ * no unbounded loop; the loop also stops early once a page comes back short
17
+ * (end of the result set on the server).
18
+ *
19
+ * `onProgress` (optional, last position — no existing caller had to change) is
20
+ * invoked **only on the chunked path**: one step per BoondManager page. The
21
+ * fast path stays silent on purpose — a single API call has nothing to report
22
+ * and a "1/1" notification would be pure noise. The reporter is a no-op unless
23
+ * the client sent a `progressToken` (see `services/progress.ts`).
24
+ */
25
+ export declare function apiSearch(path: string, query: Record<string, QueryValue>, onProgress?: ProgressReporter): Promise<JsonApiResponse>;
26
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/services/search.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAwBjF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,UAAU,CAAC,EAAE,gBAAgB,GAC5B,OAAO,CAAC,eAAe,CAAC,CA4C1B"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Collection search: query building from a tool's params and the per-route
3
+ * `maxResults` ceiling with transparent chunking (see `ROUTE_MAX_RESULTS`).
4
+ */
5
+ import { DEFAULT_MAX_RESULTS, DEFAULT_PAGE_SIZE, ROUTE_MAX_RESULTS } from "../constants.js";
6
+ import { apiRequest } from "./http/transport.js";
7
+ export function buildSearchQuery(params) {
8
+ const query = {};
9
+ if (params.keywords)
10
+ query["keywords"] = params.keywords;
11
+ if (params.page !== undefined)
12
+ query["page"] = params.page;
13
+ if (params.pageSize !== undefined)
14
+ query["maxResults"] = params.pageSize;
15
+ // Forward any additional filter params (strings, numbers, or arrays).
16
+ // `fields` is a client-side projection consumed by formatListResponse,
17
+ // never a BoondManager query parameter.
18
+ for (const [key, value] of Object.entries(params)) {
19
+ if (["keywords", "page", "pageSize", "fields"].includes(key))
20
+ continue;
21
+ if (value === undefined || value === null)
22
+ continue;
23
+ if (Array.isArray(value)) {
24
+ // Pass arrays through so apiRequest emits repeated bracket notation
25
+ query[key] = value;
26
+ }
27
+ else if (typeof value === "string" || typeof value === "number") {
28
+ query[key] = value;
29
+ }
30
+ else {
31
+ query[key] = String(value);
32
+ }
33
+ }
34
+ return query;
35
+ }
36
+ /**
37
+ * Search wrapper around `apiRequest` that enforces BoondManager's per-route
38
+ * `maxResults` ceiling (see `ROUTE_MAX_RESULTS`). When the caller requests more
39
+ * results than the route allows, the request is transparently split into
40
+ * chunks of `cap` records and the pages are merged into a single JSON:API
41
+ * response — the caller still receives the full page, but BoondManager never
42
+ * sees `maxResults` above the cap (which overflows memory on `/actions`).
43
+ *
44
+ * Routes whose ceiling already covers the requested page size take the fast
45
+ * path: a single `apiRequest`, byte-for-byte identical to calling it directly.
46
+ * The chunk count is bounded by `ceil((offset + requested) / cap)`, so there is
47
+ * no unbounded loop; the loop also stops early once a page comes back short
48
+ * (end of the result set on the server).
49
+ *
50
+ * `onProgress` (optional, last position — no existing caller had to change) is
51
+ * invoked **only on the chunked path**: one step per BoondManager page. The
52
+ * fast path stays silent on purpose — a single API call has nothing to report
53
+ * and a "1/1" notification would be pure noise. The reporter is a no-op unless
54
+ * the client sent a `progressToken` (see `services/progress.ts`).
55
+ */
56
+ export async function apiSearch(path, query, onProgress) {
57
+ const cap = ROUTE_MAX_RESULTS[path] ?? DEFAULT_MAX_RESULTS;
58
+ const requested = typeof query["maxResults"] === "number" ? query["maxResults"] : DEFAULT_PAGE_SIZE;
59
+ const page = typeof query["page"] === "number" ? query["page"] : 1;
60
+ // Fast path: one call, maxResults left exactly as the caller built it.
61
+ if (requested <= cap) {
62
+ return apiRequest(path, "GET", undefined, query);
63
+ }
64
+ // Chunked path: fetch `requested` records starting at the absolute offset
65
+ // implied by (page, requested), in BoondManager pages of `cap` records.
66
+ const startRow = (page - 1) * requested;
67
+ const firstBoondPage = Math.floor(startRow / cap) + 1;
68
+ const offsetInFirstChunk = startRow % cap;
69
+ const needed = offsetInFirstChunk + requested;
70
+ const collected = [];
71
+ let meta;
72
+ // Upper bound of the loop, and the `total` advertised to the client. It stays
73
+ // constant across the notifications of one call, as the spec requires.
74
+ const totalChunks = Math.ceil(needed / cap);
75
+ let fetchedChunks = 0;
76
+ for (let i = 0; collected.length < needed; i++) {
77
+ const chunkQuery = { ...query, page: firstBoondPage + i, maxResults: cap };
78
+ const response = await apiRequest(path, "GET", undefined, chunkQuery);
79
+ if (meta === undefined)
80
+ meta = response.meta;
81
+ const chunk = Array.isArray(response.data) ? response.data : response.data ? [response.data] : [];
82
+ collected.push(...chunk);
83
+ fetchedChunks = i + 1;
84
+ onProgress?.(fetchedChunks, totalChunks, `Récupération ${path} — page ${fetchedChunks}/${totalChunks}`);
85
+ // A short page means there is no more data on the server — stop early.
86
+ if (chunk.length < cap)
87
+ break;
88
+ }
89
+ const data = collected.slice(offsetInFirstChunk, offsetInFirstChunk + requested);
90
+ // Early stop (result set exhausted): close the bar rather than leaving the
91
+ // client at 2/5 forever. Skipped when the last page already reported `total`,
92
+ // which would repeat a value instead of increasing it.
93
+ if (fetchedChunks < totalChunks) {
94
+ onProgress?.(totalChunks, totalChunks, `Récupération ${path} — terminé (${data.length} résultat(s))`);
95
+ }
96
+ return meta !== undefined ? { data, meta } : { data };
97
+ }
98
+ //# sourceMappingURL=search.js.map