@nimbus-dev/connectors 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +662 -0
- package/NOTICE +40 -0
- package/README.md +111 -0
- package/connectors/airflow/nimbus.extension.json +16 -0
- package/connectors/airflow/src/search-filter.ts +38 -0
- package/connectors/airflow/src/server.ts +91 -0
- package/connectors/apple/nimbus.extension.json +40 -0
- package/connectors/apple/src/apple-mail-core.ts +43 -0
- package/connectors/apple/src/caldav-core.ts +116 -0
- package/connectors/apple/src/calendar-tools.ts +278 -0
- package/connectors/apple/src/server.ts +541 -0
- package/connectors/apple/src/tools.ts +138 -0
- package/connectors/argocd/nimbus.extension.json +16 -0
- package/connectors/argocd/src/search-filter.ts +24 -0
- package/connectors/argocd/src/server.ts +158 -0
- package/connectors/athena/nimbus.extension.json +15 -0
- package/connectors/athena/src/server.ts +6 -0
- package/connectors/athena/src/tools.ts +181 -0
- package/connectors/aws/nimbus.extension.json +21 -0
- package/connectors/aws/src/server.ts +191 -0
- package/connectors/azure/nimbus.extension.json +15 -0
- package/connectors/azure/src/server.ts +139 -0
- package/connectors/bigeye/nimbus.extension.json +16 -0
- package/connectors/bigeye/src/search-filter.ts +22 -0
- package/connectors/bigeye/src/server.ts +161 -0
- package/connectors/bigquery/nimbus.extension.json +15 -0
- package/connectors/bigquery/src/server.ts +6 -0
- package/connectors/bigquery/src/tools.ts +175 -0
- package/connectors/bitbucket/nimbus.extension.json +15 -0
- package/connectors/bitbucket/src/server.ts +252 -0
- package/connectors/bitrise/nimbus.extension.json +15 -0
- package/connectors/bitrise/src/search-filter.ts +11 -0
- package/connectors/bitrise/src/server.ts +83 -0
- package/connectors/canva/nimbus.extension.json +15 -0
- package/connectors/canva/src/search-filter.ts +23 -0
- package/connectors/canva/src/server.ts +77 -0
- package/connectors/circleci/nimbus.extension.json +15 -0
- package/connectors/circleci/src/server.ts +184 -0
- package/connectors/cloud-logging/nimbus.extension.json +15 -0
- package/connectors/cloud-logging/src/server.ts +6 -0
- package/connectors/cloud-logging/src/tools.ts +115 -0
- package/connectors/cloudwatch/nimbus.extension.json +15 -0
- package/connectors/cloudwatch/src/server.ts +6 -0
- package/connectors/cloudwatch/src/tools.ts +144 -0
- package/connectors/codemagic/nimbus.extension.json +15 -0
- package/connectors/codemagic/src/search-filter.ts +11 -0
- package/connectors/codemagic/src/server.ts +74 -0
- package/connectors/confluence/nimbus.extension.json +15 -0
- package/connectors/confluence/src/kb-append.ts +25 -0
- package/connectors/confluence/src/server.ts +309 -0
- package/connectors/dagster/nimbus.extension.json +16 -0
- package/connectors/dagster/src/search-filter.ts +53 -0
- package/connectors/dagster/src/server.ts +218 -0
- package/connectors/databricks/nimbus.extension.json +16 -0
- package/connectors/databricks/src/search-filter.ts +28 -0
- package/connectors/databricks/src/server.ts +81 -0
- package/connectors/datadog/nimbus.extension.json +15 -0
- package/connectors/datadog/src/server.ts +56 -0
- package/connectors/dataprofile/nimbus.extension.json +16 -0
- package/connectors/dataprofile/src/profile.ts +298 -0
- package/connectors/dataprofile/src/server.ts +6 -0
- package/connectors/dataprofile/src/tools.ts +69 -0
- package/connectors/dbt/nimbus.extension.json +16 -0
- package/connectors/dbt/src/search-filter.ts +28 -0
- package/connectors/dbt/src/server.ts +96 -0
- package/connectors/dependencytrack/nimbus.extension.json +16 -0
- package/connectors/dependencytrack/src/search-filter.ts +24 -0
- package/connectors/dependencytrack/src/server.ts +80 -0
- package/connectors/discord/nimbus.extension.json +15 -0
- package/connectors/discord/src/server.ts +93 -0
- package/connectors/elasticsearch/nimbus.extension.json +16 -0
- package/connectors/elasticsearch/src/server.ts +6 -0
- package/connectors/elasticsearch/src/tools.ts +110 -0
- package/connectors/fastmail/nimbus.extension.json +16 -0
- package/connectors/fastmail/src/jmap-core.ts +86 -0
- package/connectors/fastmail/src/server.ts +194 -0
- package/connectors/fastmail/src/tools.ts +143 -0
- package/connectors/figma/nimbus.extension.json +15 -0
- package/connectors/figma/src/search-filter.ts +24 -0
- package/connectors/figma/src/server.ts +117 -0
- package/connectors/firebase/nimbus.extension.json +15 -0
- package/connectors/firebase/src/search-filter.ts +30 -0
- package/connectors/firebase/src/server.ts +94 -0
- package/connectors/firebase/src/token.ts +41 -0
- package/connectors/flagsmith/nimbus.extension.json +15 -0
- package/connectors/flagsmith/src/search-filter.ts +29 -0
- package/connectors/flagsmith/src/server.ts +102 -0
- package/connectors/flux/nimbus.extension.json +16 -0
- package/connectors/flux/src/search-filter.ts +42 -0
- package/connectors/flux/src/server.ts +182 -0
- package/connectors/gcp/nimbus.extension.json +15 -0
- package/connectors/gcp/src/server.ts +140 -0
- package/connectors/github/nimbus.extension.json +15 -0
- package/connectors/github/src/server.ts +347 -0
- package/connectors/github-actions/nimbus.extension.json +15 -0
- package/connectors/github-actions/src/server.ts +213 -0
- package/connectors/gitlab/nimbus.extension.json +15 -0
- package/connectors/gitlab/src/server.ts +392 -0
- package/connectors/gmail/nimbus.extension.json +15 -0
- package/connectors/gmail/src/server.ts +277 -0
- package/connectors/google-drive/nimbus.extension.json +15 -0
- package/connectors/google-drive/src/drive-query.ts +3 -0
- package/connectors/google-drive/src/server.ts +531 -0
- package/connectors/google-meet/nimbus.extension.json +15 -0
- package/connectors/google-meet/src/server.ts +89 -0
- package/connectors/google-photos/nimbus.extension.json +15 -0
- package/connectors/google-photos/src/server.ts +139 -0
- package/connectors/grafana/nimbus.extension.json +15 -0
- package/connectors/grafana/src/server.ts +61 -0
- package/connectors/great-expectations/nimbus.extension.json +16 -0
- package/connectors/great-expectations/src/gx-parse.ts +292 -0
- package/connectors/great-expectations/src/server.ts +6 -0
- package/connectors/great-expectations/src/tools.ts +91 -0
- package/connectors/greenhouse/nimbus.extension.json +15 -0
- package/connectors/greenhouse/src/search-filter.ts +65 -0
- package/connectors/greenhouse/src/server.ts +67 -0
- package/connectors/hubspot/nimbus.extension.json +15 -0
- package/connectors/hubspot/src/search-filter.ts +28 -0
- package/connectors/hubspot/src/server.ts +81 -0
- package/connectors/iac/nimbus.extension.json +14 -0
- package/connectors/iac/src/server.ts +145 -0
- package/connectors/imap/nimbus.extension.json +16 -0
- package/connectors/imap/src/imap-core.ts +110 -0
- package/connectors/imap/src/server.ts +232 -0
- package/connectors/imap/src/tools.ts +40 -0
- package/connectors/intercom/nimbus.extension.json +15 -0
- package/connectors/intercom/src/search-filter.ts +46 -0
- package/connectors/intercom/src/server.ts +65 -0
- package/connectors/jenkins/nimbus.extension.json +15 -0
- package/connectors/jenkins/src/jenkins-api.ts +156 -0
- package/connectors/jenkins/src/server.ts +223 -0
- package/connectors/jira/nimbus.extension.json +15 -0
- package/connectors/jira/src/server.ts +309 -0
- package/connectors/kubernetes/nimbus.extension.json +15 -0
- package/connectors/kubernetes/src/server.ts +163 -0
- package/connectors/launchdarkly/nimbus.extension.json +15 -0
- package/connectors/launchdarkly/src/search-filter.ts +31 -0
- package/connectors/launchdarkly/src/server.ts +79 -0
- package/connectors/lever/nimbus.extension.json +15 -0
- package/connectors/lever/src/search-filter.ts +35 -0
- package/connectors/lever/src/server.ts +64 -0
- package/connectors/linear/nimbus.extension.json +15 -0
- package/connectors/linear/src/server.ts +467 -0
- package/connectors/localdb/nimbus.extension.json +16 -0
- package/connectors/localdb/src/server.ts +6 -0
- package/connectors/localdb/src/sql-scan.ts +158 -0
- package/connectors/localdb/src/tools.ts +67 -0
- package/connectors/looker/nimbus.extension.json +16 -0
- package/connectors/looker/src/search-filter.ts +22 -0
- package/connectors/looker/src/server.ts +254 -0
- package/connectors/mendeley/nimbus.extension.json +15 -0
- package/connectors/mendeley/src/search-filter.ts +68 -0
- package/connectors/mendeley/src/server.ts +59 -0
- package/connectors/mercury/nimbus.extension.json +15 -0
- package/connectors/mercury/src/search-filter.ts +11 -0
- package/connectors/mercury/src/server.ts +61 -0
- package/connectors/metabase/nimbus.extension.json +16 -0
- package/connectors/metabase/src/search-filter.ts +22 -0
- package/connectors/metabase/src/server.ts +83 -0
- package/connectors/miro/nimbus.extension.json +15 -0
- package/connectors/miro/src/search-filter.ts +24 -0
- package/connectors/miro/src/server.ts +76 -0
- package/connectors/mlflow/nimbus.extension.json +16 -0
- package/connectors/mlflow/src/search-filter.ts +38 -0
- package/connectors/mlflow/src/server.ts +167 -0
- package/connectors/monte-carlo/nimbus.extension.json +16 -0
- package/connectors/monte-carlo/src/search-filter.ts +11 -0
- package/connectors/monte-carlo/src/server.ts +241 -0
- package/connectors/netlify/nimbus.extension.json +15 -0
- package/connectors/netlify/src/search-filter.ts +37 -0
- package/connectors/netlify/src/server.ts +64 -0
- package/connectors/newrelic/nimbus.extension.json +15 -0
- package/connectors/newrelic/src/server.ts +48 -0
- package/connectors/notion/nimbus.extension.json +15 -0
- package/connectors/notion/src/kb-append.ts +23 -0
- package/connectors/notion/src/server.ts +343 -0
- package/connectors/obsidian/nimbus.extension.json +19 -0
- package/connectors/obsidian/src/server.ts +433 -0
- package/connectors/onedrive/nimbus.extension.json +15 -0
- package/connectors/onedrive/src/server.ts +256 -0
- package/connectors/outlook/nimbus.extension.json +15 -0
- package/connectors/outlook/src/server.ts +358 -0
- package/connectors/outlook/src/tool-scope-policy.ts +53 -0
- package/connectors/pagerduty/nimbus.extension.json +15 -0
- package/connectors/pagerduty/src/server.ts +140 -0
- package/connectors/pipedrive/nimbus.extension.json +15 -0
- package/connectors/pipedrive/src/search-filter.ts +11 -0
- package/connectors/pipedrive/src/server.ts +62 -0
- package/connectors/powerbi/nimbus.extension.json +16 -0
- package/connectors/powerbi/src/search-filter.ts +22 -0
- package/connectors/powerbi/src/server.ts +249 -0
- package/connectors/prefect/nimbus.extension.json +16 -0
- package/connectors/prefect/src/search-filter.ts +40 -0
- package/connectors/prefect/src/server.ts +94 -0
- package/connectors/protonmail/nimbus.extension.json +16 -0
- package/connectors/protonmail/src/mail-core.ts +96 -0
- package/connectors/protonmail/src/server.ts +231 -0
- package/connectors/protonmail/src/tools.ts +45 -0
- package/connectors/raindrop/nimbus.extension.json +15 -0
- package/connectors/raindrop/src/search-filter.ts +20 -0
- package/connectors/raindrop/src/server.ts +107 -0
- package/connectors/ramp/nimbus.extension.json +15 -0
- package/connectors/ramp/src/search-filter.ts +37 -0
- package/connectors/ramp/src/server.ts +109 -0
- package/connectors/readwise/nimbus.extension.json +15 -0
- package/connectors/readwise/src/search-filter.ts +64 -0
- package/connectors/readwise/src/server.ts +92 -0
- package/connectors/sagemaker/nimbus.extension.json +15 -0
- package/connectors/sagemaker/src/server.ts +6 -0
- package/connectors/sagemaker/src/tools.ts +121 -0
- package/connectors/salesforce/nimbus.extension.json +15 -0
- package/connectors/salesforce/src/search-filter.ts +23 -0
- package/connectors/salesforce/src/server.ts +94 -0
- package/connectors/semgrep/nimbus.extension.json +15 -0
- package/connectors/semgrep/src/search-filter.ts +31 -0
- package/connectors/semgrep/src/server.ts +108 -0
- package/connectors/sentry/nimbus.extension.json +15 -0
- package/connectors/sentry/src/server.ts +66 -0
- package/connectors/slack/nimbus.extension.json +15 -0
- package/connectors/slack/src/server.ts +360 -0
- package/connectors/snowflake/nimbus.extension.json +16 -0
- package/connectors/snowflake/src/search-filter.ts +26 -0
- package/connectors/snowflake/src/server.ts +236 -0
- package/connectors/snyk/nimbus.extension.json +15 -0
- package/connectors/snyk/src/search-filter.ts +28 -0
- package/connectors/snyk/src/server.ts +144 -0
- package/connectors/sonarqube/nimbus.extension.json +15 -0
- package/connectors/sonarqube/src/search-filter.ts +28 -0
- package/connectors/sonarqube/src/server.ts +115 -0
- package/connectors/stackoverflow/nimbus.extension.json +15 -0
- package/connectors/stackoverflow/src/search-filter.ts +52 -0
- package/connectors/stackoverflow/src/server.ts +81 -0
- package/connectors/storybook/nimbus.extension.json +16 -0
- package/connectors/storybook/src/server.ts +6 -0
- package/connectors/storybook/src/storybook-parse.ts +67 -0
- package/connectors/storybook/src/tools.ts +68 -0
- package/connectors/stripe/nimbus.extension.json +15 -0
- package/connectors/stripe/src/search-filter.ts +19 -0
- package/connectors/stripe/src/server.ts +65 -0
- package/connectors/superset/nimbus.extension.json +16 -0
- package/connectors/superset/src/search-filter.ts +22 -0
- package/connectors/superset/src/server.ts +119 -0
- package/connectors/tableau/nimbus.extension.json +16 -0
- package/connectors/tableau/src/search-filter.ts +22 -0
- package/connectors/tableau/src/server.ts +248 -0
- package/connectors/teams/nimbus.extension.json +20 -0
- package/connectors/teams/src/bot-send.ts +72 -0
- package/connectors/teams/src/server.ts +257 -0
- package/connectors/testflight/nimbus.extension.json +15 -0
- package/connectors/testflight/src/jwt.ts +27 -0
- package/connectors/testflight/src/search-filter.ts +30 -0
- package/connectors/testflight/src/server.ts +74 -0
- package/connectors/vercel/nimbus.extension.json +15 -0
- package/connectors/vercel/src/search-filter.ts +34 -0
- package/connectors/vercel/src/server.ts +79 -0
- package/connectors/vertex-ai/nimbus.extension.json +15 -0
- package/connectors/vertex-ai/src/server.ts +6 -0
- package/connectors/vertex-ai/src/tools.ts +145 -0
- package/connectors/wiz/nimbus.extension.json +15 -0
- package/connectors/wiz/src/search-filter.ts +50 -0
- package/connectors/wiz/src/server.ts +192 -0
- package/connectors/workday/nimbus.extension.json +19 -0
- package/connectors/workday/src/search-filter.ts +5 -0
- package/connectors/workday/src/server.ts +78 -0
- package/connectors/workday/src/tools.ts +1 -0
- package/connectors/zendesk/nimbus.extension.json +16 -0
- package/connectors/zendesk/src/search-filter.ts +11 -0
- package/connectors/zendesk/src/server.ts +77 -0
- package/connectors/zoom/nimbus.extension.json +15 -0
- package/connectors/zoom/src/encode-meeting-path.ts +5 -0
- package/connectors/zoom/src/recordings-window.ts +34 -0
- package/connectors/zoom/src/search-filter.ts +42 -0
- package/connectors/zoom/src/server.ts +98 -0
- package/connectors/zotero/nimbus.extension.json +15 -0
- package/connectors/zotero/src/search-filter.ts +81 -0
- package/connectors/zotero/src/server.ts +82 -0
- package/package.json +152 -0
- package/shared/atlassian-json-fetch.ts +42 -0
- package/shared/audit-chain.ts +133 -0
- package/shared/connector-mode.ts +60 -0
- package/shared/consent-kit.ts +323 -0
- package/shared/fetch-bearer-json.ts +18 -0
- package/shared/header-safe.ts +28 -0
- package/shared/imap-bodystructure.ts +113 -0
- package/shared/imap-mail-core.ts +57 -0
- package/shared/imap-tool-kit.ts +293 -0
- package/shared/join-api-path.ts +18 -0
- package/shared/kb-markdown.ts +142 -0
- package/shared/mcp-search-tool.ts +37 -0
- package/shared/mcp-tool-kit.ts +35 -0
- package/shared/nimbus-spawn.ts +128 -0
- package/shared/rest-tool-kit.ts +18 -0
- package/shared/run-cli-json.ts +55 -0
- package/shared/run-read-only-mcp-connector.ts +59 -0
- package/shared/safe-cli-arg.ts +55 -0
- package/shared/search-filter.ts +140 -0
- package/shared/strip-trailing-slashes.ts +7 -0
- package/shared/write-scope.ts +58 -0
- package/standalone/src/bin.ts +19 -0
- package/standalone/src/launcher.ts +186 -0
- package/standalone/tsconfig.json +8 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which entrypoint started this connector process, and therefore whether the connector's own
|
|
3
|
+
* consent gate is in force.
|
|
4
|
+
*
|
|
5
|
+
* `"gateway"` — started by `run-bundled-connector.ts` as the gateway's `__nimbus-connector` role.
|
|
6
|
+
* The gateway's executor provides HITL (I2), so the connector registers its full tool surface.
|
|
7
|
+
*
|
|
8
|
+
* `"standalone"` — anything else. The connector hardens itself: write tools register only behind a
|
|
9
|
+
* client that can prompt a human, and every mutation is scope-checked, budgeted and audited.
|
|
10
|
+
*
|
|
11
|
+
* Standalone is the DEFAULT deliberately. If the gateway's one wiring line is ever lost, the
|
|
12
|
+
* gateway degrades to read-only — loud and safe — rather than the standalone build silently
|
|
13
|
+
* ungating. The failure mode must point that way.
|
|
14
|
+
*
|
|
15
|
+
* The mode is derived from which entrypoint ran, never from an env var: Non-Negotiable #2 forbids a
|
|
16
|
+
* consent gate that can be "configured away", and an env var in a client's JSON config is exactly
|
|
17
|
+
* that.
|
|
18
|
+
*/
|
|
19
|
+
export type ConnectorMode = "gateway" | "standalone";
|
|
20
|
+
|
|
21
|
+
let current: ConnectorMode | undefined;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Lock the mode for this process.
|
|
25
|
+
*
|
|
26
|
+
* Set-once: re-asserting the same value is a no-op, but a CONFLICTING change throws. Reading via
|
|
27
|
+
* `getConnectorMode` also locks, so a gateway wire that runs too late — after a connector module
|
|
28
|
+
* has already registered tools against the default — fails loudly instead of leaving a surface that
|
|
29
|
+
* half-registered under one mode and half under another.
|
|
30
|
+
*/
|
|
31
|
+
export function setConnectorMode(mode: ConnectorMode): void {
|
|
32
|
+
if (current !== undefined && current !== mode) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`connector mode already locked to "${current}"; refusing to change it to "${mode}"`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
current = mode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The active mode, locking in the standalone default on first read. */
|
|
41
|
+
export function getConnectorMode(): ConnectorMode {
|
|
42
|
+
current ??= "standalone";
|
|
43
|
+
return current;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* TEST-ONLY: clear the lock between cases. Never called from production code — the
|
|
48
|
+
* `audit:connector-consent` gate permits `setConnectorMode` only in the two sanctioned entrypoints,
|
|
49
|
+
* and this function exists so tests never need to reach for it.
|
|
50
|
+
*
|
|
51
|
+
* This matters more than it looks. `bun test` runs MANY TEST FILES IN ONE PROCESS — verified: two
|
|
52
|
+
* files sharing a module observed the same pid, and state set by the first was visible to the
|
|
53
|
+
* second. So a file that locks the mode and does not clear it changes the behaviour of every file
|
|
54
|
+
* that runs after it. Any test file that touches the mode must reset in BOTH `beforeEach` and
|
|
55
|
+
* `afterEach`: `beforeEach` protects this file from its predecessors, `afterEach` protects the
|
|
56
|
+
* suite from this one.
|
|
57
|
+
*/
|
|
58
|
+
export function resetConnectorModeForTests(): void {
|
|
59
|
+
current = undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type { McpListResult, ZodObjectSchema } from "@nimbus-dev/sdk/connector-kit";
|
|
2
|
+
|
|
3
|
+
import { type AuditOutcome, appendAuditEntry } from "./audit-chain.ts";
|
|
4
|
+
import { getConnectorMode } from "./connector-mode.ts";
|
|
5
|
+
import { parseWriteScope, scopeAllows } from "./write-scope.ts";
|
|
6
|
+
|
|
7
|
+
/** Mutations allowed per process lifetime when NIMBUS_MCP_WRITE_BUDGET is unset. */
|
|
8
|
+
const DEFAULT_BUDGET = 10;
|
|
9
|
+
|
|
10
|
+
/** The subset of the SDK's `RegisteredTool` this kit needs. */
|
|
11
|
+
export type ToolHandle = { disable(): void };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The structural subset of `McpServer` the consent kit uses.
|
|
15
|
+
*
|
|
16
|
+
* Narrowed deliberately so tests can supply a fake without a transport, and so the kit's real
|
|
17
|
+
* dependencies are visible in one place.
|
|
18
|
+
*/
|
|
19
|
+
export type ConsentServer = {
|
|
20
|
+
readonly server: {
|
|
21
|
+
getClientCapabilities(): { elicitation?: unknown } | undefined;
|
|
22
|
+
/** Fired after the client's `initialize` — the FIRST moment capabilities are knowable. */
|
|
23
|
+
oninitialized?: (() => void) | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* `requestedSchema` is typed to the restricted JSON-Schema subset the MCP spec allows, not
|
|
26
|
+
* `Record<string, unknown>`. The loose form does not satisfy the real `McpServer.elicitInput`,
|
|
27
|
+
* which is only discoverable by passing a real server — a hand-written fake accepts anything.
|
|
28
|
+
*/
|
|
29
|
+
elicitInput(
|
|
30
|
+
params: {
|
|
31
|
+
mode: "form";
|
|
32
|
+
message: string;
|
|
33
|
+
requestedSchema: {
|
|
34
|
+
type: "object";
|
|
35
|
+
properties: Record<
|
|
36
|
+
string,
|
|
37
|
+
{ type: "boolean"; title?: string; description?: string; default?: boolean }
|
|
38
|
+
>;
|
|
39
|
+
required?: string[];
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
options?: { timeout?: number },
|
|
43
|
+
): Promise<{
|
|
44
|
+
action: "accept" | "decline" | "cancel";
|
|
45
|
+
content?: Record<string, unknown> | undefined;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* `inputSchema` is `unknown` on purpose. The real signature wants the SDK's
|
|
50
|
+
* `ZodRawShapeCompat | AnySchema`, and the connector kit's `ZodObjectSchema["shape"]` is
|
|
51
|
+
* `Record<string, unknown>` — neither is assignable to the other, so a concrete type here makes
|
|
52
|
+
* a real `McpServer` fail to satisfy this interface. The value is passed straight through and
|
|
53
|
+
* never inspected by this kit.
|
|
54
|
+
*/
|
|
55
|
+
registerTool(
|
|
56
|
+
name: string,
|
|
57
|
+
config: { description?: string; inputSchema?: unknown },
|
|
58
|
+
cb: (args: unknown) => Promise<McpListResult>,
|
|
59
|
+
): ToolHandle;
|
|
60
|
+
sendToolListChanged(): void;
|
|
61
|
+
sendLoggingMessage(params: { level: "info" | "warning"; data: unknown }): Promise<void>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type WriteToolConfig<T> = {
|
|
65
|
+
/** Action type this tool performs, e.g. "repo.branch.delete". Machine-readable, not prose. */
|
|
66
|
+
readonly mutates: string;
|
|
67
|
+
/** False when the mutation cannot be undone — which is when pre-state capture is mandatory. */
|
|
68
|
+
readonly recoverable: boolean;
|
|
69
|
+
/** Required when `recoverable` is false. Runs BEFORE the mutation; result is audited verbatim. */
|
|
70
|
+
readonly capturePreState?: (args: T) => Promise<Record<string, unknown>>;
|
|
71
|
+
/** The scope target this invocation would touch, checked against the allow-list. */
|
|
72
|
+
readonly scopeTargetOf: (args: T) => { kind: string; value: string };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type WriteToolRegistrar = <T>(
|
|
76
|
+
name: string,
|
|
77
|
+
cfg: WriteToolConfig<T>,
|
|
78
|
+
description: string,
|
|
79
|
+
schema: ZodObjectSchema<T>,
|
|
80
|
+
handler: (args: T) => Promise<McpListResult>,
|
|
81
|
+
) => void;
|
|
82
|
+
|
|
83
|
+
function refused(why: string): McpListResult {
|
|
84
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: why }) }] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Ask the human, through the client, and report whether they said yes.
|
|
89
|
+
*
|
|
90
|
+
* The prompt carries the VERBATIM operation and resolved parameters, never a digest — a digest is
|
|
91
|
+
* a rubber stamp with extra steps, and the human is the entire trust boundary here.
|
|
92
|
+
*/
|
|
93
|
+
async function consented(
|
|
94
|
+
server: ConsentServer,
|
|
95
|
+
mutates: string,
|
|
96
|
+
params: unknown,
|
|
97
|
+
): Promise<boolean> {
|
|
98
|
+
let res: { action: string; content?: Record<string, unknown> | undefined };
|
|
99
|
+
try {
|
|
100
|
+
res = await server.server.elicitInput({
|
|
101
|
+
mode: "form",
|
|
102
|
+
message:
|
|
103
|
+
`Nimbus is about to perform ${mutates} with:\n` +
|
|
104
|
+
`${JSON.stringify(params, null, 2)}\n\nApprove?`,
|
|
105
|
+
requestedSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
confirm: { type: "boolean", title: "Approve this action", description: mutates },
|
|
109
|
+
},
|
|
110
|
+
required: ["confirm"],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
// Timeout, transport failure, or a client that rejects the request: fail CLOSED.
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
// `action: "accept"` alone is not consent — the form's own answer must also be true, so a client
|
|
118
|
+
// that auto-accepts without answering the question does not clear the gate.
|
|
119
|
+
return res.action === "accept" && res.content?.["confirm"] === true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function registerOn<T>(
|
|
123
|
+
server: ConsentServer,
|
|
124
|
+
name: string,
|
|
125
|
+
description: string,
|
|
126
|
+
schema: ZodObjectSchema<T>,
|
|
127
|
+
handler: (args: T) => Promise<McpListResult>,
|
|
128
|
+
handles: ToolHandle[],
|
|
129
|
+
): void {
|
|
130
|
+
const handle = server.registerTool(
|
|
131
|
+
name,
|
|
132
|
+
{ description, inputSchema: schema.shape },
|
|
133
|
+
async (args: unknown): Promise<McpListResult> => {
|
|
134
|
+
const parsed = schema.safeParse(args);
|
|
135
|
+
if (!parsed.success) {
|
|
136
|
+
throw new Error(parsed.error.message);
|
|
137
|
+
}
|
|
138
|
+
return handler(parsed.data);
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
// Collected so budget exhaustion can disable every write tool at once.
|
|
142
|
+
handles.push(handle);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build a connector's write-tool registrar.
|
|
147
|
+
*
|
|
148
|
+
* In `"gateway"` mode the raw handler is registered immediately: `executor.ts` (I2) is the gate
|
|
149
|
+
* there, client capabilities are irrelevant, and the connector adds nothing.
|
|
150
|
+
*
|
|
151
|
+
* In `"standalone"` mode registration is QUEUED and flushed on `oninitialized`, because client
|
|
152
|
+
* capabilities are not knowable at module scope — verified against the real SDK, where
|
|
153
|
+
* `getClientCapabilities()` returns `undefined` before `initialize`. Deciding at registration time
|
|
154
|
+
* would register nothing, for every client, forever.
|
|
155
|
+
*/
|
|
156
|
+
export function createWriteToolRegistrar(
|
|
157
|
+
server: ConsentServer,
|
|
158
|
+
cfg: {
|
|
159
|
+
readonly connector: string;
|
|
160
|
+
readonly scopeEnv: string;
|
|
161
|
+
/**
|
|
162
|
+
* Scope kinds this connector accepts. Declared once per connector rather than per tool: the
|
|
163
|
+
* allow-list is parsed at registrar construction, before any tool has registered, so a
|
|
164
|
+
* per-tool declaration would arrive too late to validate the env value.
|
|
165
|
+
*/
|
|
166
|
+
readonly scopeKinds: readonly string[];
|
|
167
|
+
},
|
|
168
|
+
): WriteToolRegistrar {
|
|
169
|
+
const handles: ToolHandle[] = [];
|
|
170
|
+
const pending: Array<() => void> = [];
|
|
171
|
+
let flushed = false;
|
|
172
|
+
|
|
173
|
+
const scope = parseWriteScope(process.env[cfg.scopeEnv], cfg.scopeKinds);
|
|
174
|
+
const auditLog = process.env["NIMBUS_MCP_AUDIT_LOG"];
|
|
175
|
+
let remaining = Number(process.env["NIMBUS_MCP_WRITE_BUDGET"] ?? DEFAULT_BUDGET);
|
|
176
|
+
|
|
177
|
+
async function record(
|
|
178
|
+
tool: string,
|
|
179
|
+
outcome: AuditOutcome,
|
|
180
|
+
detail: Record<string, unknown>,
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
// Client-visible channel: any MCP client can display or persist this.
|
|
183
|
+
await server.sendLoggingMessage({
|
|
184
|
+
level: outcome === "executed" ? "info" : "warning",
|
|
185
|
+
data: { connector: cfg.connector, tool, outcome },
|
|
186
|
+
});
|
|
187
|
+
// Durable channel: only when the operator configured a path.
|
|
188
|
+
if (auditLog !== undefined && auditLog !== "") {
|
|
189
|
+
await appendAuditEntry(auditLog, {
|
|
190
|
+
ts: new Date().toISOString(),
|
|
191
|
+
connector: cfg.connector,
|
|
192
|
+
tool,
|
|
193
|
+
outcome,
|
|
194
|
+
detail,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Decide the write surface, once, at the first moment client capabilities are knowable.
|
|
201
|
+
*
|
|
202
|
+
* Default-DENY: if the client cannot prompt a human, the queued tools are dropped and never
|
|
203
|
+
* appear in `tools/list` at all. Refusing at call time instead would still advertise a tool the
|
|
204
|
+
* model must not use.
|
|
205
|
+
*/
|
|
206
|
+
function flushWriteTools(): void {
|
|
207
|
+
if (flushed) return;
|
|
208
|
+
flushed = true;
|
|
209
|
+
const caps = server.server.getClientCapabilities();
|
|
210
|
+
if (caps?.elicitation === undefined) {
|
|
211
|
+
pending.length = 0;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
for (const register of pending) register();
|
|
215
|
+
pending.length = 0;
|
|
216
|
+
// The tool list changed after `initialize`, so the client must be told to re-read it.
|
|
217
|
+
if (handles.length > 0) server.sendToolListChanged();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (getConnectorMode() === "standalone") {
|
|
221
|
+
// An empty scope denies every mutation, which is the correct default but looks identical to a
|
|
222
|
+
// broken connector from the outside. Say so on stderr — safe for a stdio server, whose
|
|
223
|
+
// PROTOCOL channel is stdout; writing this to stdout would corrupt the JSON-RPC stream.
|
|
224
|
+
if (scope.length === 0) {
|
|
225
|
+
process.stderr.write(
|
|
226
|
+
`nimbus-connector ${cfg.connector}: ${cfg.scopeEnv} is unset or empty, so every write tool ` +
|
|
227
|
+
`will refuse. Set it to a comma-separated list of ${cfg.scopeKinds.join("|")}:value ` +
|
|
228
|
+
"terms to enable writes.\n",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
// Chain rather than overwrite: another module may already own this hook, and clobbering it
|
|
232
|
+
// would silently disable whatever it did.
|
|
233
|
+
const prev = server.server.oninitialized;
|
|
234
|
+
server.server.oninitialized = (): void => {
|
|
235
|
+
prev?.();
|
|
236
|
+
flushWriteTools();
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return <T>(
|
|
241
|
+
name: string,
|
|
242
|
+
toolCfg: WriteToolConfig<T>,
|
|
243
|
+
description: string,
|
|
244
|
+
schema: ZodObjectSchema<T>,
|
|
245
|
+
handler: (args: T) => Promise<McpListResult>,
|
|
246
|
+
): void => {
|
|
247
|
+
// Checked at REGISTRATION, not on first call: a missing capture on a destructive tool is a
|
|
248
|
+
// programming error, and it must surface at boot rather than on the one call that needed it.
|
|
249
|
+
if (!toolCfg.recoverable && toolCfg.capturePreState === undefined) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`${name}: capturePreState is required when recoverable is false — an unrecoverable ` +
|
|
252
|
+
"mutation must record enough pre-state to be undone",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (getConnectorMode() === "gateway") {
|
|
257
|
+
registerOn(server, name, description, schema, handler, handles);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const guarded = async (args: T): Promise<McpListResult> => {
|
|
262
|
+
const target = toolCfg.scopeTargetOf(args);
|
|
263
|
+
|
|
264
|
+
// 1. SCOPE FIRST — before prompting. Asking a human to approve something the operator
|
|
265
|
+
// already forbade cannot change the outcome, and training people to click through
|
|
266
|
+
// prompts that do not matter is how you make the prompts that do matter ineffective.
|
|
267
|
+
if (!scopeAllows(scope, target.kind, target.value)) {
|
|
268
|
+
await record(name, "refused", { reason: "out of scope", target });
|
|
269
|
+
return refused(`out of scope: ${target.kind}:${target.value} is not in ${cfg.scopeEnv}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 2. BUDGET — same reasoning: a refusal that consent cannot lift comes before consent.
|
|
273
|
+
if (remaining <= 0) {
|
|
274
|
+
await record(name, "refused", { reason: "budget exhausted", target });
|
|
275
|
+
return refused("write budget exhausted for this session");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 3. CONSENT.
|
|
279
|
+
await record(name, "requested", { target, params: args });
|
|
280
|
+
if (!(await consented(server, toolCfg.mutates, args))) {
|
|
281
|
+
await record(name, "declined", { target });
|
|
282
|
+
return refused("not approved: the operation was declined, cancelled, or timed out");
|
|
283
|
+
}
|
|
284
|
+
await record(name, "accepted", { target });
|
|
285
|
+
|
|
286
|
+
// 4. PRE-STATE — after approval, before the mutation, so an unrecoverable action leaves a
|
|
287
|
+
// record of what it destroyed. Capture failure is NOT fatal: refusing here would turn a
|
|
288
|
+
// transient read error into a blocked action the owner already approved.
|
|
289
|
+
let preState: Record<string, unknown> = {};
|
|
290
|
+
if (toolCfg.capturePreState !== undefined) {
|
|
291
|
+
try {
|
|
292
|
+
preState = await toolCfg.capturePreState(args);
|
|
293
|
+
} catch (e) {
|
|
294
|
+
preState = { captureFailed: e instanceof Error ? e.message : String(e) };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 5. MUTATE. Decrement BEFORE the call so a throwing mutation still consumes budget —
|
|
299
|
+
// otherwise a failing destructive tool could be retried without limit.
|
|
300
|
+
remaining -= 1;
|
|
301
|
+
if (remaining <= 0) {
|
|
302
|
+
for (const h of handles) h.disable();
|
|
303
|
+
server.sendToolListChanged();
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const result = await handler(args);
|
|
307
|
+
await record(name, "executed", { target, preState });
|
|
308
|
+
return result;
|
|
309
|
+
} catch (e) {
|
|
310
|
+
await record(name, "failed", {
|
|
311
|
+
target,
|
|
312
|
+
preState,
|
|
313
|
+
error: e instanceof Error ? e.message : String(e),
|
|
314
|
+
});
|
|
315
|
+
throw e;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
pending.push(() => {
|
|
320
|
+
registerOn(server, name, description, schema, guarded, handles);
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fetch-bearer-json — re-export of the Bearer-auth JSON fetch helpers now owned
|
|
3
|
+
* by `@nimbus-dev/sdk/connector-kit`.
|
|
4
|
+
*
|
|
5
|
+
* These helpers originated here and were upstreamed to the SDK in 1.11.0. The
|
|
6
|
+
* SDK is now the single owner; this module stays as the connector-facing import
|
|
7
|
+
* path so the ~99 connectors keep their existing relative imports.
|
|
8
|
+
*
|
|
9
|
+
* Named re-exports, deliberately not `export *` — see the note in
|
|
10
|
+
* `mcp-tool-kit.ts`.
|
|
11
|
+
*
|
|
12
|
+
* `resolveUrlWithBase` remains the SSRF chokepoint that refuses a cross-origin
|
|
13
|
+
* absolute URL before a credential-bearing fetch; that behaviour now lives in
|
|
14
|
+
* the SDK and is covered by `fetch-bearer-json.test.ts` through this re-export.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type { BearerJsonFetchResult } from "@nimbus-dev/sdk/connector-kit";
|
|
18
|
+
export { fetchBearerAuthorizedJson, resolveUrlWithBase } from "@nimbus-dev/sdk/connector-kit";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rejects any string containing a carriage return or line feed. CR/LF in an
|
|
5
|
+
* email header field (`To`/`Cc`/`Bcc`/`Subject`/attendee list) is the classic
|
|
6
|
+
* header / recipient-injection vector: a folded line can smuggle extra headers
|
|
7
|
+
* or recipients into the outgoing message. Body text legitimately contains
|
|
8
|
+
* newlines and must NOT use this guard.
|
|
9
|
+
*/
|
|
10
|
+
const NO_CRLF = /^[^\r\n]*$/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A Zod string constrained to a single header line (no CR/LF), with optional
|
|
14
|
+
* length bounds. Use for every user-supplied field that flows into an email
|
|
15
|
+
* header — `to`, `cc`, `bcc`, `subject`, and comma-separated recipient/attendee
|
|
16
|
+
* lists. The single shared definition keeps the guard consistent across every
|
|
17
|
+
* send/draft connector (imap, protonmail, fastmail, gmail, outlook).
|
|
18
|
+
*/
|
|
19
|
+
export function headerLine(opts?: { min?: number; max?: number }): z.ZodString {
|
|
20
|
+
let s = z.string().regex(NO_CRLF, "must not contain a line break (CR/LF)");
|
|
21
|
+
if (opts?.min !== undefined) {
|
|
22
|
+
s = s.min(opts.min);
|
|
23
|
+
}
|
|
24
|
+
if (opts?.max !== undefined) {
|
|
25
|
+
s = s.max(opts.max);
|
|
26
|
+
}
|
|
27
|
+
return s;
|
|
28
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure BODYSTRUCTURE walkers shared by the IMAP-based mail connectors (imap,
|
|
3
|
+
* protonmail-via-Bridge) — extract attachment METADATA (filename, size,
|
|
4
|
+
* mimetype) and locate the first text/plain part for the preview. NEVER touches
|
|
5
|
+
* attachment bytes; operates only on the structure tree imapflow returns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* imapflow's parsed BODYSTRUCTURE node. Multipart nodes carry `childNodes`; leaf
|
|
10
|
+
* nodes carry `type` (mimetype), `size`, and `disposition`/`dispositionParameters`
|
|
11
|
+
* for attachments. We type the fields we read; the rest is `unknown`.
|
|
12
|
+
*/
|
|
13
|
+
export interface BodyStructureNode {
|
|
14
|
+
readonly part?: string;
|
|
15
|
+
readonly type?: string;
|
|
16
|
+
readonly size?: number;
|
|
17
|
+
readonly disposition?: string | null;
|
|
18
|
+
readonly dispositionParameters?: Record<string, unknown> | null;
|
|
19
|
+
readonly parameters?: Record<string, unknown> | null;
|
|
20
|
+
readonly childNodes?: readonly BodyStructureNode[] | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Attachment METADATA only — filename, size, mimetype. NEVER the bytes. */
|
|
24
|
+
export interface AttachmentMeta {
|
|
25
|
+
readonly filename: string | null;
|
|
26
|
+
readonly sizeBytes: number | null;
|
|
27
|
+
readonly mimeType: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function asString(v: unknown): string | null {
|
|
31
|
+
return typeof v === "string" && v !== "" ? v : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function filenameOf(node: BodyStructureNode): string | null {
|
|
35
|
+
const fromDisp = asString(node.dispositionParameters?.["filename"]);
|
|
36
|
+
if (fromDisp !== null) {
|
|
37
|
+
return fromDisp;
|
|
38
|
+
}
|
|
39
|
+
// Some servers put the name in the content-type `name` parameter instead.
|
|
40
|
+
return asString(node.parameters?.["name"]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isAttachment(node: BodyStructureNode): boolean {
|
|
44
|
+
const disp = (node.disposition ?? "").toLowerCase();
|
|
45
|
+
if (disp === "attachment" || disp === "inline") {
|
|
46
|
+
return filenameOf(node) !== null;
|
|
47
|
+
}
|
|
48
|
+
// No disposition but a filename present → treat as attachment metadata.
|
|
49
|
+
return filenameOf(node) !== null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Walk the BODYSTRUCTURE tree and collect attachment METADATA only — filename,
|
|
54
|
+
* size, mimetype. Never reads or requests the part body.
|
|
55
|
+
*/
|
|
56
|
+
export function extractAttachments(root: BodyStructureNode | null | undefined): AttachmentMeta[] {
|
|
57
|
+
const out: AttachmentMeta[] = [];
|
|
58
|
+
const stack: BodyStructureNode[] = root ? [root] : [];
|
|
59
|
+
while (stack.length > 0) {
|
|
60
|
+
const node = stack.pop();
|
|
61
|
+
if (node === undefined) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const children = node.childNodes ?? null;
|
|
65
|
+
if (children !== null) {
|
|
66
|
+
for (const c of children) {
|
|
67
|
+
stack.push(c);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (isAttachment(node)) {
|
|
72
|
+
out.push({
|
|
73
|
+
filename: filenameOf(node),
|
|
74
|
+
sizeBytes: typeof node.size === "number" && Number.isFinite(node.size) ? node.size : null,
|
|
75
|
+
mimeType: asString(node.type),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Find the IMAP part key of the first `text/plain` leaf that is NOT an
|
|
84
|
+
* attachment. Used to fetch a single truncated body part for the preview.
|
|
85
|
+
* Returns `"1"` as a conservative default when no structure is available
|
|
86
|
+
* (a single-part text message has an implicit part "1" / "TEXT").
|
|
87
|
+
*/
|
|
88
|
+
export function findTextPlainPart(root: BodyStructureNode | null | undefined): string {
|
|
89
|
+
const stack: BodyStructureNode[] = root ? [root] : [];
|
|
90
|
+
let firstAnyText: string | null = null;
|
|
91
|
+
while (stack.length > 0) {
|
|
92
|
+
const node = stack.shift();
|
|
93
|
+
if (node === undefined) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const children = node.childNodes ?? null;
|
|
97
|
+
if (children !== null) {
|
|
98
|
+
for (const c of children) {
|
|
99
|
+
stack.push(c);
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const type = (node.type ?? "").toLowerCase();
|
|
104
|
+
const part = node.part ?? "1";
|
|
105
|
+
if (type === "text/plain" && !isAttachment(node)) {
|
|
106
|
+
return part;
|
|
107
|
+
}
|
|
108
|
+
if (type.startsWith("text/") && firstAnyText === null && !isAttachment(node)) {
|
|
109
|
+
firstAnyText = part;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return firstAnyText ?? "1";
|
|
113
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, transport-agnostic helpers shared by the IMAP-based mail connectors
|
|
3
|
+
* (imap, protonmail-via-Bridge). ProtonMail Bridge exposes a standard IMAP/SMTP
|
|
4
|
+
* interface, so both connectors share the same preview-capping, limit-clamping,
|
|
5
|
+
* and address-formatting logic. No external deps; never touches message bodies
|
|
6
|
+
* or attachment bytes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Max characters of the plain-text body preview that is ever indexed/returned. */
|
|
10
|
+
export const PREVIEW_MAX_CHARS = 2000;
|
|
11
|
+
/** Max bytes of the text/plain body part fetched for the preview (~2 KB). */
|
|
12
|
+
export const PREVIEW_FETCH_BYTES = 2048;
|
|
13
|
+
|
|
14
|
+
/** One parsed address (name optional). */
|
|
15
|
+
export interface MailAddress {
|
|
16
|
+
readonly name?: string;
|
|
17
|
+
readonly address?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_LIST_LIMIT = 50;
|
|
21
|
+
const MAX_LIST_LIMIT = 200;
|
|
22
|
+
|
|
23
|
+
export function clampLimit(limit: number | undefined, fallback = DEFAULT_LIST_LIMIT): number {
|
|
24
|
+
if (limit === undefined || !Number.isFinite(limit)) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
const n = Math.trunc(limit);
|
|
28
|
+
if (n < 1) {
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
return Math.min(n, MAX_LIST_LIMIT);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Cap a body preview to {@link PREVIEW_MAX_CHARS}. Normalizes CRLF and collapses
|
|
36
|
+
* whitespace/blank-line runs so the preview stays compact; never lengthens the
|
|
37
|
+
* input.
|
|
38
|
+
*/
|
|
39
|
+
export function capPreview(text: string): string {
|
|
40
|
+
const normalized = text
|
|
41
|
+
.replaceAll("\r\n", "\n")
|
|
42
|
+
.replace(/[ \t]+/g, " ")
|
|
43
|
+
.replace(/\n{2,}/g, "\n")
|
|
44
|
+
.trim();
|
|
45
|
+
return normalized.length > PREVIEW_MAX_CHARS
|
|
46
|
+
? normalized.slice(0, PREVIEW_MAX_CHARS)
|
|
47
|
+
: normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Format one address as `Name <addr>` / `<addr>` for envelope display. */
|
|
51
|
+
export function formatAddress(a: MailAddress): string {
|
|
52
|
+
const addr = a.address ?? "";
|
|
53
|
+
if (a.name !== undefined && a.name !== "") {
|
|
54
|
+
return addr === "" ? a.name : `${a.name} <${addr}>`;
|
|
55
|
+
}
|
|
56
|
+
return addr;
|
|
57
|
+
}
|