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.
- package/LICENSE +1 -1
- package/NOTICE +16 -2
- package/README.md +60 -15
- package/dist/config/access-policy.d.ts.map +1 -1
- package/dist/config/access-policy.js +6 -18
- package/dist/config/access-policy.js.map +1 -1
- package/dist/config/dictionary-overrides.d.ts.map +1 -1
- package/dist/config/dictionary-overrides.js +2 -12
- package/dist/config/dictionary-overrides.js.map +1 -1
- package/dist/config/env.d.ts +63 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +116 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/profiles.d.ts +22 -8
- package/dist/config/profiles.d.ts.map +1 -1
- package/dist/config/profiles.js +48 -4
- package/dist/config/profiles.js.map +1 -1
- package/dist/constants.d.ts +13 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +25 -1
- package/dist/constants.js.map +1 -1
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +6 -1
- package/dist/icons.js.map +1 -1
- package/dist/index.js +33 -15
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +6 -6
- package/dist/instructions.js.map +1 -1
- package/dist/prompts/index.d.ts +19 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +495 -25
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/periods.d.ts +45 -0
- package/dist/prompts/periods.d.ts.map +1 -0
- package/dist/prompts/periods.js +154 -0
- package/dist/prompts/periods.js.map +1 -0
- package/dist/resources/index.d.ts +14 -0
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +285 -0
- package/dist/resources/index.js.map +1 -1
- package/dist/resources/templates.d.ts +1 -1
- package/dist/resources/templates.d.ts.map +1 -1
- package/dist/resources/templates.js +2 -0
- package/dist/resources/templates.js.map +1 -1
- package/dist/schema-dialect.js.map +1 -1
- package/dist/schemas/filter-aliases.d.ts +1 -1
- package/dist/schemas/filter-aliases.d.ts.map +1 -1
- package/dist/schemas/filter-aliases.js +101 -2
- package/dist/schemas/filter-aliases.js.map +1 -1
- package/dist/schemas/index.d.ts +987 -161
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +853 -323
- package/dist/schemas/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +10 -3
- package/dist/server.js.map +1 -1
- package/dist/services/boond-client.d.ts +29 -213
- package/dist/services/boond-client.d.ts.map +1 -1
- package/dist/services/boond-client.js +29 -1168
- package/dist/services/boond-client.js.map +1 -1
- package/dist/services/dictionary.d.ts +27 -3
- package/dist/services/dictionary.d.ts.map +1 -1
- package/dist/services/dictionary.js +60 -23
- package/dist/services/dictionary.js.map +1 -1
- package/dist/services/document-text.d.ts +32 -0
- package/dist/services/document-text.d.ts.map +1 -0
- package/dist/services/document-text.js +120 -0
- package/dist/services/document-text.js.map +1 -0
- package/dist/services/format/detail.d.ts +16 -0
- package/dist/services/format/detail.d.ts.map +1 -0
- package/dist/services/format/detail.js +36 -0
- package/dist/services/format/detail.js.map +1 -0
- package/dist/services/format/html.d.ts +19 -0
- package/dist/services/format/html.d.ts.map +1 -0
- package/dist/services/format/html.js +87 -0
- package/dist/services/format/html.js.map +1 -0
- package/dist/services/format/list.d.ts +3 -0
- package/dist/services/format/list.d.ts.map +1 -0
- package/dist/services/format/list.js +44 -0
- package/dist/services/format/list.js.map +1 -0
- package/dist/services/format/summary.d.ts +16 -0
- package/dist/services/format/summary.d.ts.map +1 -0
- package/dist/services/format/summary.js +172 -0
- package/dist/services/format/summary.js.map +1 -0
- package/dist/services/format/tab.d.ts +9 -0
- package/dist/services/format/tab.d.ts.map +1 -0
- package/dist/services/format/tab.js +32 -0
- package/dist/services/format/tab.js.map +1 -0
- package/dist/services/http/auth.d.ts +44 -0
- package/dist/services/http/auth.d.ts.map +1 -0
- package/dist/services/http/auth.js +146 -0
- package/dist/services/http/auth.js.map +1 -0
- package/dist/services/http/download.d.ts +60 -0
- package/dist/services/http/download.d.ts.map +1 -0
- package/dist/services/http/download.js +160 -0
- package/dist/services/http/download.js.map +1 -0
- package/dist/services/http/errors.d.ts +45 -0
- package/dist/services/http/errors.d.ts.map +1 -0
- package/dist/services/http/errors.js +188 -0
- package/dist/services/http/errors.js.map +1 -0
- package/dist/services/http/rate-limit.d.ts +40 -0
- package/dist/services/http/rate-limit.d.ts.map +1 -0
- package/dist/services/http/rate-limit.js +92 -0
- package/dist/services/http/rate-limit.js.map +1 -0
- package/dist/services/http/retry.d.ts +46 -0
- package/dist/services/http/retry.d.ts.map +1 -0
- package/dist/services/http/retry.js +101 -0
- package/dist/services/http/retry.js.map +1 -0
- package/dist/services/http/transport.d.ts +73 -0
- package/dist/services/http/transport.d.ts.map +1 -0
- package/dist/services/http/transport.js +272 -0
- package/dist/services/http/transport.js.map +1 -0
- package/dist/services/logger.d.ts +53 -2
- package/dist/services/logger.d.ts.map +1 -1
- package/dist/services/logger.js +82 -21
- package/dist/services/logger.js.map +1 -1
- package/dist/services/oauth.d.ts +12 -0
- package/dist/services/oauth.d.ts.map +1 -1
- package/dist/services/oauth.js +21 -8
- package/dist/services/oauth.js.map +1 -1
- package/dist/services/rate-limiter.d.ts +9 -2
- package/dist/services/rate-limiter.d.ts.map +1 -1
- package/dist/services/rate-limiter.js +18 -6
- package/dist/services/rate-limiter.js.map +1 -1
- package/dist/services/request-context.d.ts +47 -0
- package/dist/services/request-context.d.ts.map +1 -0
- package/dist/services/request-context.js +112 -0
- package/dist/services/request-context.js.map +1 -0
- package/dist/services/search.d.ts +26 -0
- package/dist/services/search.d.ts.map +1 -0
- package/dist/services/search.js +98 -0
- package/dist/services/search.js.map +1 -0
- package/dist/services/update-checker.d.ts.map +1 -1
- package/dist/services/update-checker.js +15 -8
- package/dist/services/update-checker.js.map +1 -1
- package/dist/tools/absences.d.ts +9 -0
- package/dist/tools/absences.d.ts.map +1 -1
- package/dist/tools/absences.js +65 -20
- package/dist/tools/absences.js.map +1 -1
- package/dist/tools/actions.d.ts.map +1 -1
- package/dist/tools/actions.js +11 -27
- package/dist/tools/actions.js.map +1 -1
- package/dist/tools/advantages.d.ts +2 -0
- package/dist/tools/advantages.d.ts.map +1 -1
- package/dist/tools/advantages.js +66 -13
- package/dist/tools/advantages.js.map +1 -1
- package/dist/tools/alerts.d.ts +18 -0
- package/dist/tools/alerts.d.ts.map +1 -0
- package/dist/tools/alerts.js +82 -0
- package/dist/tools/alerts.js.map +1 -0
- package/dist/tools/application.d.ts +2 -1
- package/dist/tools/application.d.ts.map +1 -1
- package/dist/tools/application.js +9 -3
- package/dist/tools/application.js.map +1 -1
- package/dist/tools/contacts.d.ts.map +1 -1
- package/dist/tools/contacts.js +3 -7
- package/dist/tools/contacts.js.map +1 -1
- package/dist/tools/contracts.d.ts +22 -0
- package/dist/tools/contracts.d.ts.map +1 -1
- package/dist/tools/contracts.js +185 -52
- package/dist/tools/contracts.js.map +1 -1
- package/dist/tools/crud-factory.d.ts +33 -2
- package/dist/tools/crud-factory.d.ts.map +1 -1
- package/dist/tools/crud-factory.js +42 -15
- package/dist/tools/crud-factory.js.map +1 -1
- package/dist/tools/deliveries.d.ts +3 -0
- package/dist/tools/deliveries.d.ts.map +1 -1
- package/dist/tools/deliveries.js +50 -94
- package/dist/tools/deliveries.js.map +1 -1
- package/dist/tools/description-builders.d.ts +0 -1
- package/dist/tools/description-builders.d.ts.map +1 -1
- package/dist/tools/description-builders.js +46 -3
- package/dist/tools/description-builders.js.map +1 -1
- package/dist/tools/documents.d.ts.map +1 -1
- package/dist/tools/documents.js +73 -21
- package/dist/tools/documents.js.map +1 -1
- package/dist/tools/expenses.d.ts.map +1 -1
- package/dist/tools/expenses.js +2 -11
- package/dist/tools/expenses.js.map +1 -1
- package/dist/tools/find.d.ts +61 -0
- package/dist/tools/find.d.ts.map +1 -0
- package/dist/tools/find.js +221 -0
- package/dist/tools/find.js.map +1 -0
- package/dist/tools/flags.d.ts +5 -0
- package/dist/tools/flags.d.ts.map +1 -1
- package/dist/tools/flags.js +116 -1
- package/dist/tools/flags.js.map +1 -1
- package/dist/tools/forms.d.ts +5 -0
- package/dist/tools/forms.d.ts.map +1 -0
- package/dist/tools/forms.js +64 -0
- package/dist/tools/forms.js.map +1 -0
- package/dist/tools/groupments.d.ts +5 -0
- package/dist/tools/groupments.d.ts.map +1 -0
- package/dist/tools/groupments.js +68 -0
- package/dist/tools/groupments.js.map +1 -0
- package/dist/tools/inactivities.d.ts +5 -0
- package/dist/tools/inactivities.d.ts.map +1 -0
- package/dist/tools/inactivities.js +59 -0
- package/dist/tools/inactivities.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/invoices.d.ts.map +1 -1
- package/dist/tools/invoices.js +27 -2
- package/dist/tools/invoices.js.map +1 -1
- package/dist/tools/linked-entity-filters.d.ts +48 -0
- package/dist/tools/linked-entity-filters.d.ts.map +1 -0
- package/dist/tools/linked-entity-filters.js +64 -0
- package/dist/tools/linked-entity-filters.js.map +1 -0
- package/dist/tools/notifications.d.ts.map +1 -1
- package/dist/tools/notifications.js +0 -5
- package/dist/tools/notifications.js.map +1 -1
- package/dist/tools/orders.d.ts.map +1 -1
- package/dist/tools/orders.js +33 -4
- package/dist/tools/orders.js.map +1 -1
- package/dist/tools/parameter-disclosure.js.map +1 -1
- package/dist/tools/payments.d.ts +3 -0
- package/dist/tools/payments.d.ts.map +1 -1
- package/dist/tools/payments.js +43 -109
- package/dist/tools/payments.js.map +1 -1
- package/dist/tools/positionings.d.ts.map +1 -1
- package/dist/tools/positionings.js +9 -39
- package/dist/tools/positionings.js.map +1 -1
- package/dist/tools/projects.d.ts.map +1 -1
- package/dist/tools/projects.js +5 -12
- package/dist/tools/projects.js.map +1 -1
- package/dist/tools/provider-invoices.d.ts +2 -0
- package/dist/tools/provider-invoices.d.ts.map +1 -1
- package/dist/tools/provider-invoices.js +30 -111
- package/dist/tools/provider-invoices.js.map +1 -1
- package/dist/tools/purchases.d.ts +2 -0
- package/dist/tools/purchases.d.ts.map +1 -1
- package/dist/tools/purchases.js +50 -104
- package/dist/tools/purchases.js.map +1 -1
- package/dist/tools/registration-decorators.d.ts +28 -0
- package/dist/tools/registration-decorators.d.ts.map +1 -1
- package/dist/tools/registration-decorators.js +107 -0
- package/dist/tools/registration-decorators.js.map +1 -1
- package/dist/tools/resources.d.ts.map +1 -1
- package/dist/tools/resources.js +27 -5
- package/dist/tools/resources.js.map +1 -1
- package/dist/tools/rights.d.ts +101 -0
- package/dist/tools/rights.d.ts.map +1 -0
- package/dist/tools/rights.js +75 -0
- package/dist/tools/rights.js.map +1 -0
- package/dist/tools/tab-tools.js +3 -3
- package/dist/tools/tab-tools.js.map +1 -1
- package/dist/tools/timesheets.d.ts +29 -0
- package/dist/tools/timesheets.d.ts.map +1 -1
- package/dist/tools/timesheets.js +196 -126
- package/dist/tools/timesheets.js.map +1 -1
- package/dist/tools/todolists.d.ts +2 -0
- package/dist/tools/todolists.d.ts.map +1 -1
- package/dist/tools/todolists.js +64 -1
- package/dist/tools/todolists.js.map +1 -1
- package/dist/tools/validations.d.ts +9 -0
- package/dist/tools/validations.d.ts.map +1 -1
- package/dist/tools/validations.js +86 -9
- package/dist/tools/validations.js.map +1 -1
- package/dist/tools/workflows.js +1 -1
- package/dist/tools/workflows.js.map +1 -1
- package/dist/transports/http.d.ts +116 -0
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +422 -92
- package/dist/transports/http.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/manifest.json +7 -7
- package/package.json +14 -10
package/dist/services/logger.js
CHANGED
|
@@ -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
|
|
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 =
|
|
30
|
-
|
|
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 =
|
|
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;
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/services/logger.ts"],"names":[],"mappings":"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"}
|
package/dist/services/oauth.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/services/oauth.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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;
|
|
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
|
-
/**
|
|
30
|
-
|
|
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;
|
|
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
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
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;
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAWrD,MAAM,OAAO,GAAG,IAAI,iBAAiB,EAAkB,CAAC;AAExD,6FAA6F;AAC7F,MAAM,UAAU,qBAAqB,CAAI,OAAuB,EAAE,EAAW;IAC3E,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,qBAAqB;IACnC,OAAO,OAAO,CAAC,QAAQ,EAAE,CAAC;AAC5B,CAAC;AAED,4GAA4G;AAC5G,MAAM,UAAU,oBAAoB,CAAI,MAA+B,EAAE,EAAW;IAClF,2EAA2E;IAC3E,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjE,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED,6GAA6G;AAC7G,MAAM,UAAU,oBAAoB,CAAI,EAAW;IACjD,OAAO,oBAAoB,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC;AACpC,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,aAAa;IAC3B,OAAO,OAAO,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC;AACpC,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAClE,MAAM,MAAM,GAAI,KAA8B,CAAC,MAAM,CAAC;IACtD,OAAO,MAAM,YAAY,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5D,CAAC;AAED;;;GAGG;AACH,MAAM,cAAc,GAAG,uDAAuD,CAAC;AAE/E,yEAAyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IACjE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;AACvC,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAClE,MAAM,IAAI,GAAI,KAA6B,CAAC,KAAK,CAAC;IAClD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAChE,OAAO,gBAAgB,CAAE,IAAkC,CAAC,WAAW,CAAC,CAAC;AAC3E,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IACrC,MAAM,CAAS;IACf,IAAI,CAAS;IACtB,YAAY,MAAc,EAAE,IAAY,EAAE,KAAe;QACvD,KAAK,CACH,8GAA8G,MAAM,IAAI,IAAI,EAAE,EAC9H,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAC5C,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,sGAAsG;AACtG,MAAM,UAAU,gBAAgB,CAAC,MAA+B,EAAE,MAAc,EAAE,IAAY;IAC5F,IAAI,MAAM,EAAE,OAAO;QAAE,MAAM,IAAI,qBAAqB,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;AACpF,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,YAAY,CAAC,MAAmB;IAC9C,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;QAC/B,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACxG,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -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
|