@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,232 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { type FetchMessageObject, type FetchQueryObject, ImapFlow } from "imapflow";
|
|
4
|
+
import { createTransport, type Transporter } from "nodemailer";
|
|
5
|
+
import {
|
|
6
|
+
type BodyStructureNode,
|
|
7
|
+
extractAttachments,
|
|
8
|
+
findTextPlainPart,
|
|
9
|
+
} from "../../../shared/imap-bodystructure.ts";
|
|
10
|
+
import { envInt, previewFromParts } from "../../../shared/imap-tool-kit.ts";
|
|
11
|
+
import { requireProcessEnv } from "../../../shared/mcp-tool-kit.ts";
|
|
12
|
+
import {
|
|
13
|
+
clampLimit,
|
|
14
|
+
type ImapAddress,
|
|
15
|
+
type ImapClient,
|
|
16
|
+
type ImapEnvelope,
|
|
17
|
+
type ImapListOptions,
|
|
18
|
+
type ImapMessageMeta,
|
|
19
|
+
type ImapSearchOptions,
|
|
20
|
+
PREVIEW_FETCH_BYTES,
|
|
21
|
+
type SendMailInput,
|
|
22
|
+
type SendMailResult,
|
|
23
|
+
type SmtpMailer,
|
|
24
|
+
} from "./imap-core.ts";
|
|
25
|
+
import { registerImapTools } from "./tools.ts";
|
|
26
|
+
|
|
27
|
+
const DEFAULT_IMAP_PORT = 993;
|
|
28
|
+
const DEFAULT_SMTP_PORT = 465;
|
|
29
|
+
const DEFAULT_MAILBOX = "INBOX";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build an {@link ImapAddress} from an imapflow envelope address, omitting
|
|
33
|
+
* `name`/`address` when absent (the interface uses `exactOptionalPropertyTypes`,
|
|
34
|
+
* so an explicit `undefined` is not assignable).
|
|
35
|
+
*/
|
|
36
|
+
function toImapAddress(a: { name?: string; address?: string }): ImapAddress {
|
|
37
|
+
const out: { name?: string; address?: string } = {};
|
|
38
|
+
if (a.name !== undefined) {
|
|
39
|
+
out.name = a.name;
|
|
40
|
+
}
|
|
41
|
+
if (a.address !== undefined) {
|
|
42
|
+
out.address = a.address;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function envelopeFromImap(env: FetchMessageObject["envelope"]): ImapEnvelope {
|
|
48
|
+
if (env === undefined) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
date: env.date ?? null,
|
|
53
|
+
subject: env.subject ?? null,
|
|
54
|
+
messageId: env.messageId ?? null,
|
|
55
|
+
from: (env.from ?? []).map(toImapAddress),
|
|
56
|
+
to: (env.to ?? []).map(toImapAddress),
|
|
57
|
+
cc: (env.cc ?? []).map(toImapAddress),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toMessageMeta(
|
|
62
|
+
msg: FetchMessageObject,
|
|
63
|
+
mailbox: string,
|
|
64
|
+
uidValidity: string | null,
|
|
65
|
+
): ImapMessageMeta {
|
|
66
|
+
const structure = (msg.bodyStructure ?? null) as BodyStructureNode | null;
|
|
67
|
+
const partKey = findTextPlainPart(structure);
|
|
68
|
+
return {
|
|
69
|
+
uid: msg.uid,
|
|
70
|
+
mailbox,
|
|
71
|
+
uidValidity,
|
|
72
|
+
envelope: envelopeFromImap(msg.envelope),
|
|
73
|
+
attachments: extractAttachments(structure),
|
|
74
|
+
preview: previewFromParts(msg.bodyParts, partKey),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fetch query that requests ONLY headers + structure + a truncated text body
|
|
80
|
+
* part. There is no `source` / full-body request here by construction.
|
|
81
|
+
*/
|
|
82
|
+
function previewFetchQuery(): FetchQueryObject {
|
|
83
|
+
return {
|
|
84
|
+
uid: true,
|
|
85
|
+
envelope: true,
|
|
86
|
+
bodyStructure: true,
|
|
87
|
+
internalDate: true,
|
|
88
|
+
// Two candidate text part keys, each capped to PREVIEW_FETCH_BYTES bytes.
|
|
89
|
+
bodyParts: [
|
|
90
|
+
{ key: "1", start: 0, maxLength: PREVIEW_FETCH_BYTES },
|
|
91
|
+
{ key: "TEXT", start: 0, maxLength: PREVIEW_FETCH_BYTES },
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Real IMAP client over imapflow. Per-call connect/lock/logout keeps it stateless. */
|
|
97
|
+
class ImapFlowClient implements ImapClient {
|
|
98
|
+
private readonly config: {
|
|
99
|
+
host: string;
|
|
100
|
+
port: number;
|
|
101
|
+
user: string;
|
|
102
|
+
pass: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
constructor() {
|
|
106
|
+
this.config = {
|
|
107
|
+
host: requireProcessEnv("IMAP_HOST"),
|
|
108
|
+
port: envInt("IMAP_PORT", DEFAULT_IMAP_PORT),
|
|
109
|
+
user: requireProcessEnv("IMAP_USERNAME"),
|
|
110
|
+
pass: requireProcessEnv("IMAP_PASSWORD"),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private newClient(): ImapFlow {
|
|
115
|
+
return new ImapFlow({
|
|
116
|
+
host: this.config.host,
|
|
117
|
+
port: this.config.port,
|
|
118
|
+
secure: true,
|
|
119
|
+
auth: { user: this.config.user, pass: this.config.pass },
|
|
120
|
+
logger: false,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async withMailbox<T>(
|
|
125
|
+
mailbox: string,
|
|
126
|
+
fn: (client: ImapFlow, uidValidity: string | null) => Promise<T>,
|
|
127
|
+
): Promise<T> {
|
|
128
|
+
const client = this.newClient();
|
|
129
|
+
await client.connect();
|
|
130
|
+
const lock = await client.getMailboxLock(mailbox);
|
|
131
|
+
try {
|
|
132
|
+
const mb = client.mailbox;
|
|
133
|
+
const uidValidity = mb === false ? null : String(mb.uidValidity);
|
|
134
|
+
return await fn(client, uidValidity);
|
|
135
|
+
} finally {
|
|
136
|
+
lock.release();
|
|
137
|
+
await client.logout();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async list(options: ImapListOptions): Promise<ImapMessageMeta[]> {
|
|
142
|
+
const mailbox = options.mailbox ?? DEFAULT_MAILBOX;
|
|
143
|
+
const limit = clampLimit(options.limit);
|
|
144
|
+
return this.withMailbox(mailbox, async (client, uidValidity) => {
|
|
145
|
+
const out: ImapMessageMeta[] = [];
|
|
146
|
+
const status = await client.status(mailbox, { messages: true });
|
|
147
|
+
const total = status.messages ?? 0;
|
|
148
|
+
if (total === 0) {
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
const start = Math.max(1, total - limit + 1);
|
|
152
|
+
const range = `${start}:${total}`;
|
|
153
|
+
for await (const msg of client.fetch(range, previewFetchQuery())) {
|
|
154
|
+
out.push(toMessageMeta(msg, mailbox, uidValidity));
|
|
155
|
+
}
|
|
156
|
+
// Most-recent first.
|
|
157
|
+
out.sort((a, b) => b.uid - a.uid);
|
|
158
|
+
return out;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async get(uid: number, mailbox?: string): Promise<ImapMessageMeta | null> {
|
|
163
|
+
const box = mailbox ?? DEFAULT_MAILBOX;
|
|
164
|
+
return this.withMailbox(box, async (client, uidValidity) => {
|
|
165
|
+
const msg = await client.fetchOne(String(uid), previewFetchQuery(), { uid: true });
|
|
166
|
+
return msg === false ? null : toMessageMeta(msg, box, uidValidity);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async search(options: ImapSearchOptions): Promise<ImapMessageMeta[]> {
|
|
171
|
+
const mailbox = options.mailbox ?? DEFAULT_MAILBOX;
|
|
172
|
+
const limit = clampLimit(options.limit);
|
|
173
|
+
const q = options.query;
|
|
174
|
+
return this.withMailbox(mailbox, async (client, uidValidity) => {
|
|
175
|
+
const uids = await client.search(
|
|
176
|
+
{ or: [{ subject: q }, { from: q }, { to: q }] },
|
|
177
|
+
{ uid: true },
|
|
178
|
+
);
|
|
179
|
+
if (uids === false || uids.length === 0) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const picked = uids.slice(-limit).sort((a, b) => b - a);
|
|
183
|
+
const out: ImapMessageMeta[] = [];
|
|
184
|
+
for await (const msg of client.fetch(picked, previewFetchQuery(), { uid: true })) {
|
|
185
|
+
out.push(toMessageMeta(msg, mailbox, uidValidity));
|
|
186
|
+
}
|
|
187
|
+
out.sort((a, b) => b.uid - a.uid);
|
|
188
|
+
return out;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Real SMTP mailer over nodemailer. */
|
|
194
|
+
class NodemailerMailer implements SmtpMailer {
|
|
195
|
+
private readonly transport: Transporter;
|
|
196
|
+
private readonly from: string;
|
|
197
|
+
|
|
198
|
+
constructor() {
|
|
199
|
+
const host = requireProcessEnv("IMAP_SMTP_HOST");
|
|
200
|
+
const port = envInt("IMAP_SMTP_PORT", DEFAULT_SMTP_PORT);
|
|
201
|
+
const user = requireProcessEnv("IMAP_SMTP_USERNAME");
|
|
202
|
+
const pass = requireProcessEnv("IMAP_SMTP_PASSWORD");
|
|
203
|
+
this.from = user;
|
|
204
|
+
this.transport = createTransport({
|
|
205
|
+
host,
|
|
206
|
+
port,
|
|
207
|
+
secure: port === 465,
|
|
208
|
+
auth: { user, pass },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async send(input: SendMailInput): Promise<SendMailResult> {
|
|
213
|
+
const info = await this.transport.sendMail({
|
|
214
|
+
from: this.from,
|
|
215
|
+
to: input.to,
|
|
216
|
+
subject: input.subject,
|
|
217
|
+
text: input.body,
|
|
218
|
+
...(input.cc === undefined ? {} : { cc: input.cc }),
|
|
219
|
+
...(input.bcc === undefined ? {} : { bcc: input.bcc }),
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
messageId: info.messageId ?? null,
|
|
223
|
+
accepted: (info.accepted ?? []).map(String),
|
|
224
|
+
rejected: (info.rejected ?? []).map(String),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const server = new McpServer({ name: "nimbus-imap", version: "0.1.0" });
|
|
230
|
+
registerImapTools(server, new ImapFlowClient(), new NodemailerMailer());
|
|
231
|
+
|
|
232
|
+
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
|
|
2
|
+
import { registerEmailConnectorTools } from "../../../shared/imap-tool-kit.ts";
|
|
3
|
+
import { formatAddress, type ImapClient, type SmtpMailer } from "./imap-core.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Register the IMAP read tools + the SMTP send tool onto an MCP server. The
|
|
7
|
+
* IMAP client and SMTP mailer are injected so tests exercise the tool surface
|
|
8
|
+
* without opening real sockets.
|
|
9
|
+
*/
|
|
10
|
+
export function registerImapTools(
|
|
11
|
+
// Widened: the consent kit needs the real server surface, not just the `.tool` shim.
|
|
12
|
+
server: ConsentServer & { tool: (...args: never) => unknown },
|
|
13
|
+
client: ImapClient,
|
|
14
|
+
mailer: SmtpMailer,
|
|
15
|
+
): void {
|
|
16
|
+
const registerWriteTool = createWriteToolRegistrar(server, {
|
|
17
|
+
connector: "imap",
|
|
18
|
+
scopeEnv: "NIMBUS_MCP_IMAP_WRITE_SCOPE",
|
|
19
|
+
scopeKinds: ["recipient"],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
registerEmailConnectorTools({
|
|
23
|
+
server,
|
|
24
|
+
registerWriteTool,
|
|
25
|
+
toolPrefix: "imap",
|
|
26
|
+
descriptions: {
|
|
27
|
+
list: "List recent mailbox messages — HEADERS + attachment METADATA + a short capped text preview ONLY. Returns subject, from, to/cc, date, message-id, attachment {filename,size,mimetype}, and a <=2000-char plain-text body preview. NEVER returns attachment bytes or the full message body.",
|
|
28
|
+
get: "Fetch one message by uid — HEADERS + attachment METADATA + a short capped text preview ONLY. NEVER returns attachment bytes or the full message body.",
|
|
29
|
+
search:
|
|
30
|
+
"Substring search over message HEADERS (subject/from/to) — returns the same header + attachment-metadata + preview view as imap_list. Searches headers only; NEVER scans or returns document/body content beyond the capped preview.",
|
|
31
|
+
send: "Send a new email over SMTP.",
|
|
32
|
+
},
|
|
33
|
+
client,
|
|
34
|
+
mailer,
|
|
35
|
+
formatAddr: formatAddress,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Tool names exposed by this connector — for contract/introspection tests. */
|
|
40
|
+
export const IMAP_TOOL_NAMES = ["imap_list", "imap_get", "imap_search", "imap_mail_send"] as const;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "com.nimbus.intercom",
|
|
3
|
+
"displayName": "Intercom",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Intercom support connector (read-only). Surfaces the user's recent Intercom conversations as `intercom:conversation` items in the local index — the conversation subject/title, state (open/closed/snoozed), priority, the read flag, the source message author + first message body, the linked contact ids, the admin + team assignees, tags, and the created / updated dates. Useful for answering support questions (\"what conversations are still open?\", \"find the thread about the billing bug\") without leaving Nimbus. Bearer access-token auth (`Authorization: Bearer <token>` — an Intercom Access Token; never logged) plus the `Intercom-Version: 2.11` and `Accept: application/json` request headers. The SaaS host (api.intercom.io, US) is fixed — EU/AU regional hosts are a deferred follow-up. Intercom paginates the `/conversations` endpoint with a cursor (`pages.next.starting_after`, per_page=150), returning the `{ type: \"conversation.list\", conversations, pages, total_count }` envelope. `created_at` / `updated_at` are epoch SECONDS (converted to epoch-ms on index). Contacts / companies / tickets / admins-as-items and write tools are deferred follow-ups; v1 indexes conversations only.",
|
|
6
|
+
"author": "Nimbus",
|
|
7
|
+
"entrypoint": "dist/server.js",
|
|
8
|
+
"runtime": "bun",
|
|
9
|
+
"permissions": {
|
|
10
|
+
"network": ["api.intercom.io"]
|
|
11
|
+
},
|
|
12
|
+
"hitlRequired": [],
|
|
13
|
+
"syncInterval": 600,
|
|
14
|
+
"minNimbusVersion": "0.2.0"
|
|
15
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asObjectish,
|
|
3
|
+
asRecord,
|
|
4
|
+
makeQueryFilter,
|
|
5
|
+
type SearchMatchOptions,
|
|
6
|
+
stringField,
|
|
7
|
+
} from "../../../shared/search-filter.ts";
|
|
8
|
+
|
|
9
|
+
export type IntercomSearchMatchOptions = SearchMatchOptions;
|
|
10
|
+
|
|
11
|
+
function tagText(row: Record<string, unknown>): string {
|
|
12
|
+
const tags = asRecord(row["tags"]);
|
|
13
|
+
const list = tags === undefined ? undefined : tags["tags"];
|
|
14
|
+
if (!Array.isArray(list)) {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
const names: string[] = [];
|
|
18
|
+
for (const t of list) {
|
|
19
|
+
const obj = asRecord(t);
|
|
20
|
+
if (obj === undefined) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const name = obj["name"];
|
|
24
|
+
if (typeof name === "string") {
|
|
25
|
+
names.push(name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return names.join(" ");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fieldsOf(item: unknown): readonly string[] | null {
|
|
32
|
+
const row = asObjectish(item);
|
|
33
|
+
if (row === undefined) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const state = stringField(row, "state");
|
|
37
|
+
const source = asRecord(row["source"]) ?? {};
|
|
38
|
+
const subject = stringField(source, "subject");
|
|
39
|
+
const body = stringField(source, "body");
|
|
40
|
+
const author = asRecord(source["author"]) ?? {};
|
|
41
|
+
const authorName = stringField(author, "name");
|
|
42
|
+
const authorEmail = stringField(author, "email");
|
|
43
|
+
return [subject, body, state, authorName, authorEmail, tagText(row)];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const filterIntercomConversations = makeQueryFilter(fieldsOf);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { matchesResult, searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
|
|
3
|
+
import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
|
|
4
|
+
import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
|
|
5
|
+
import { filterIntercomConversations } from "./search-filter.ts";
|
|
6
|
+
|
|
7
|
+
const BASE = "https://api.intercom.io";
|
|
8
|
+
|
|
9
|
+
function apiToken(): string {
|
|
10
|
+
const t = process.env["INTERCOM_TOKEN"]?.trim();
|
|
11
|
+
if (t === undefined || t === "") {
|
|
12
|
+
throw new Error("INTERCOM_TOKEN is not set");
|
|
13
|
+
}
|
|
14
|
+
return t;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function authHeader(): Record<string, string> {
|
|
18
|
+
return {
|
|
19
|
+
Authorization: `Bearer ${apiToken()}`,
|
|
20
|
+
"Intercom-Version": "2.11",
|
|
21
|
+
Accept: "application/json",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function intercomGet(path: string): Promise<unknown> {
|
|
26
|
+
const res = await fetch(`${BASE}${path}`, { headers: authHeader() });
|
|
27
|
+
const text = await res.text();
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
throw new Error(`Intercom ${String(res.status)}: ${text.slice(0, 400)}`);
|
|
30
|
+
}
|
|
31
|
+
return JSON.parse(text) as unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await runReadOnlyMcpConnector("nimbus-intercom", (reg) => {
|
|
35
|
+
reg(
|
|
36
|
+
"intercom_list",
|
|
37
|
+
'List the user\'s Intercom conversations (`GET /conversations?per_page=150`). Returns the `{ type: "conversation.list", conversations: [...], pages, total_count }` envelope — `conversations` holds the conversation objects.',
|
|
38
|
+
z.object({}),
|
|
39
|
+
async () => {
|
|
40
|
+
return jsonResult(await intercomGet(`/conversations?per_page=150`));
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
reg(
|
|
45
|
+
"intercom_get",
|
|
46
|
+
"Fetch one Intercom conversation by its id (`GET /conversations/{id}` — note the PLURAL `conversations` in the get-by-id path). Returns the conversation object directly. Throws when no match is found.",
|
|
47
|
+
z.object({
|
|
48
|
+
id: z.string().min(1),
|
|
49
|
+
}),
|
|
50
|
+
async (p) => {
|
|
51
|
+
return jsonResult(await intercomGet(`/conversations/${encodeURIComponent(p.id)}`));
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
reg(
|
|
56
|
+
"intercom_search",
|
|
57
|
+
"Substring search across the user's Intercom conversations (first page only). Matches the query against the conversation subject, the source message body, the state, the source author name + email, and the tags (case-insensitive). Returns a `{ matches: [...] }` envelope.",
|
|
58
|
+
searchToolInputSchema(100),
|
|
59
|
+
async (p) => {
|
|
60
|
+
const root = await intercomGet(`/conversations?per_page=150`);
|
|
61
|
+
const conversations = (root as { conversations?: unknown[] } | null)?.conversations;
|
|
62
|
+
return matchesResult(conversations, filterIntercomConversations, p);
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "com.nimbus.jenkins",
|
|
3
|
+
"displayName": "Jenkins",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Jenkins CI connector (jobs, builds, queue). Always self-hosted — the target host is read from jenkins.base_url at spawn time and merged into permissions.network by the Gateway. Mutations require Gateway HITL.",
|
|
6
|
+
"author": "Nimbus",
|
|
7
|
+
"entrypoint": "dist/server.js",
|
|
8
|
+
"runtime": "bun",
|
|
9
|
+
"permissions": {
|
|
10
|
+
"network": []
|
|
11
|
+
},
|
|
12
|
+
"hitlRequired": ["write", "delete"],
|
|
13
|
+
"syncInterval": 120,
|
|
14
|
+
"minNimbusVersion": "0.2.0"
|
|
15
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { encodeBasicAuthHeader } from "../../../shared/mcp-tool-kit.ts";
|
|
2
|
+
import { stripTrailingSlashes } from "../../../shared/strip-trailing-slashes.ts";
|
|
3
|
+
|
|
4
|
+
export function jenkinsBaseUrl(): string {
|
|
5
|
+
const raw = process.env["JENKINS_BASE_URL"]?.trim() ?? "";
|
|
6
|
+
if (raw === "") {
|
|
7
|
+
throw new Error("JENKINS_BASE_URL is not set");
|
|
8
|
+
}
|
|
9
|
+
return stripTrailingSlashes(raw);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function jenkinsAuthHeader(): string {
|
|
13
|
+
const user = process.env["JENKINS_USERNAME"]?.trim() ?? "";
|
|
14
|
+
const token = process.env["JENKINS_API_TOKEN"]?.trim() ?? "";
|
|
15
|
+
if (user === "" || token === "") {
|
|
16
|
+
throw new Error("JENKINS_USERNAME and JENKINS_API_TOKEN must be set");
|
|
17
|
+
}
|
|
18
|
+
return encodeBasicAuthHeader(user, token);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type JenkinsCrumb = { field: string; value: string };
|
|
22
|
+
|
|
23
|
+
/** RFC 7230 `token`: the only characters an HTTP header field name may contain. */
|
|
24
|
+
const HEADER_NAME_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether `name` is safe to use as a header field name.
|
|
28
|
+
*
|
|
29
|
+
* The crumb field name is chosen by the REMOTE Jenkins (`crumbRequestField` in the crumb-issuer
|
|
30
|
+
* response), so it is attacker-controlled input on a compromised or hostile server — and it lands
|
|
31
|
+
* in a computed property write. Two things are refused:
|
|
32
|
+
*
|
|
33
|
+
* - anything that is not an RFC 7230 token, which is what stops a name carrying `:` or CR/LF from
|
|
34
|
+
* injecting extra headers into an authenticated POST;
|
|
35
|
+
* - `__proto__` / `constructor` / `prototype`, which reach `Object.prototype` through a computed
|
|
36
|
+
* assignment rather than creating an own property.
|
|
37
|
+
*
|
|
38
|
+
* No real crumb field is named any of those, so nothing legitimate is lost.
|
|
39
|
+
*/
|
|
40
|
+
export function isSafeHeaderName(name: string): boolean {
|
|
41
|
+
const lower = name.toLowerCase();
|
|
42
|
+
if (lower === "__proto__" || lower === "constructor" || lower === "prototype") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return HEADER_NAME_RE.test(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let crumbCache: JenkinsCrumb | null | undefined;
|
|
49
|
+
|
|
50
|
+
export function __resetJenkinsCrumbCacheForTests(): void {
|
|
51
|
+
crumbCache = undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function getJenkinsCrumb(
|
|
55
|
+
base: string,
|
|
56
|
+
authHeader: string,
|
|
57
|
+
): Promise<JenkinsCrumb | null> {
|
|
58
|
+
if (crumbCache !== undefined) {
|
|
59
|
+
return crumbCache;
|
|
60
|
+
}
|
|
61
|
+
const res = await fetch(`${base}/crumbIssuer/api/json`, {
|
|
62
|
+
headers: { Authorization: authHeader, Accept: "application/json" },
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
crumbCache = null;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
let parsed: unknown;
|
|
69
|
+
try {
|
|
70
|
+
parsed = (await res.json()) as unknown;
|
|
71
|
+
} catch {
|
|
72
|
+
crumbCache = null;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
76
|
+
crumbCache = null;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const o = parsed as Record<string, unknown>;
|
|
80
|
+
const crumb = typeof o["crumb"] === "string" ? o["crumb"] : "";
|
|
81
|
+
const field = typeof o["crumbRequestField"] === "string" ? o["crumbRequestField"] : "";
|
|
82
|
+
// A field name the crumb cannot legally carry is treated exactly like a missing one: no crumb.
|
|
83
|
+
// The POST then goes out without it and Jenkins answers 403, which is the safe outcome — far
|
|
84
|
+
// better than letting the server name an arbitrary header on our authenticated request.
|
|
85
|
+
if (crumb === "" || !isSafeHeaderName(field)) {
|
|
86
|
+
crumbCache = null;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
crumbCache = { field, value: crumb };
|
|
90
|
+
return crumbCache;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function jobPathFromFullName(fullName: string): string {
|
|
94
|
+
const segs = fullName
|
|
95
|
+
.split("/")
|
|
96
|
+
.map((s) => s.trim())
|
|
97
|
+
.filter((s) => s.length > 0);
|
|
98
|
+
if (segs.length === 0) {
|
|
99
|
+
throw new Error("job fullName is empty");
|
|
100
|
+
}
|
|
101
|
+
return segs.map((s) => encodeURIComponent(s)).join("/job/");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function jobApiRoot(base: string, fullName: string): string {
|
|
105
|
+
return `${base}/job/${jobPathFromFullName(fullName)}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function jenkinsFetchJson(
|
|
109
|
+
url: string,
|
|
110
|
+
init: RequestInit & { authHeader: string },
|
|
111
|
+
): Promise<{ ok: boolean; status: number; text: string; json: unknown }> {
|
|
112
|
+
const { authHeader, ...rest } = init;
|
|
113
|
+
const res = await fetch(url, {
|
|
114
|
+
...rest,
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: authHeader,
|
|
117
|
+
Accept: "application/json",
|
|
118
|
+
...(rest.headers as Record<string, string>),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
const text = await res.text();
|
|
122
|
+
let parsedBody: unknown = null;
|
|
123
|
+
try {
|
|
124
|
+
parsedBody = JSON.parse(text) as unknown;
|
|
125
|
+
} catch {
|
|
126
|
+
parsedBody = null;
|
|
127
|
+
}
|
|
128
|
+
return { ok: res.ok, status: res.status, text, json: parsedBody };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function jenkinsPost(
|
|
132
|
+
url: string,
|
|
133
|
+
authHeader: string,
|
|
134
|
+
crumb: JenkinsCrumb | null,
|
|
135
|
+
): Promise<{ ok: boolean; status: number; text: string }> {
|
|
136
|
+
// A `Headers` instance, NOT a plain object with a computed write.
|
|
137
|
+
//
|
|
138
|
+
// The crumb field name is chosen by the REMOTE Jenkins, so it is attacker-controlled on a hostile
|
|
139
|
+
// server. Writing it as `obj[name] = value` on an object literal reaches `Object.prototype`
|
|
140
|
+
// through the accessor for `__proto__` — the shape `tssecurity:S6109` exists to catch, and which
|
|
141
|
+
// it still reported against the guarded version of this code because a taint analyser cannot see
|
|
142
|
+
// that `isSafeHeaderName` sanitises.
|
|
143
|
+
//
|
|
144
|
+
// `Headers.set` cannot write a prototype: the name goes into the header map, never onto an
|
|
145
|
+
// object, so the class of bug is gone by construction rather than by a guard the analyser has to
|
|
146
|
+
// recognise. It also validates the name itself per RFC 7230 and THROWS on an invalid one, which
|
|
147
|
+
// is why `isSafeHeaderName` stays in front of it — a hostile crumb should mean "no crumb", not a
|
|
148
|
+
// thrown TypeError out of `jenkinsPost`.
|
|
149
|
+
const headers = new Headers({ Authorization: authHeader });
|
|
150
|
+
if (crumb !== null && isSafeHeaderName(crumb.field)) {
|
|
151
|
+
headers.set(crumb.field, crumb.value);
|
|
152
|
+
}
|
|
153
|
+
const res = await fetch(url, { method: "POST", headers });
|
|
154
|
+
const text = await res.text();
|
|
155
|
+
return { ok: res.ok, status: res.status, text };
|
|
156
|
+
}
|