@nimbus-dev/connectors 0.2.1 → 0.2.3
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/README.md +19 -7
- package/connectors/airflow/src/search-filter.ts +2 -15
- package/connectors/airflow/src/server.ts +2 -87
- package/connectors/airflow/src/tools.ts +74 -0
- package/connectors/apple/src/adapters.ts +326 -0
- package/connectors/apple/src/apple-mail-core.ts +4 -8
- package/connectors/apple/src/calendar-tools.ts +28 -8
- package/connectors/apple/src/server.ts +23 -512
- package/connectors/apple/src/tools.ts +18 -44
- package/connectors/argocd/src/server.ts +25 -38
- package/connectors/athena/src/tools.ts +26 -43
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +199 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +134 -0
- package/connectors/bigeye/src/search-filter.ts +4 -15
- package/connectors/bigeye/src/server.ts +38 -38
- package/connectors/bigquery/src/tools.ts +1 -26
- package/connectors/bitbucket/src/server.ts +2 -244
- package/connectors/bitbucket/src/tools.ts +241 -0
- package/connectors/bitrise/src/server.ts +2 -79
- package/connectors/bitrise/src/tools.ts +76 -0
- package/connectors/canva/src/search-filter.ts +2 -11
- package/connectors/canva/src/server.ts +2 -73
- package/connectors/canva/src/tools.ts +66 -0
- package/connectors/circleci/src/server.ts +2 -176
- package/connectors/circleci/src/tools.ts +193 -0
- package/connectors/cloud-logging/src/tools.ts +19 -35
- package/connectors/cloudwatch/src/tools.ts +20 -38
- package/connectors/codemagic/src/server.ts +2 -70
- package/connectors/codemagic/src/tools.ts +71 -0
- package/connectors/confluence/src/server.ts +2 -301
- package/connectors/confluence/src/tools.ts +320 -0
- package/connectors/dagster/src/server.ts +2 -214
- package/connectors/dagster/src/tools.ts +187 -0
- package/connectors/databricks/src/search-filter.ts +7 -7
- package/connectors/databricks/src/server.ts +2 -77
- package/connectors/databricks/src/tools.ts +66 -0
- package/connectors/datadog/src/server.ts +2 -48
- package/connectors/datadog/src/tools.ts +48 -0
- package/connectors/dataprofile/src/profile.ts +16 -46
- package/connectors/dbt/src/server.ts +2 -92
- package/connectors/dbt/src/tools.ts +80 -0
- package/connectors/dependencytrack/src/server.ts +2 -76
- package/connectors/dependencytrack/src/tools.ts +65 -0
- package/connectors/discord/src/server.ts +2 -85
- package/connectors/discord/src/tools.ts +82 -0
- package/connectors/elasticsearch/src/tools.ts +7 -37
- package/connectors/fastmail/src/jmap-client.ts +202 -0
- package/connectors/fastmail/src/jmap-core.ts +8 -21
- package/connectors/fastmail/src/server.ts +2 -187
- package/connectors/fastmail/src/tools.ts +4 -23
- package/connectors/figma/src/search-filter.ts +2 -11
- package/connectors/figma/src/server.ts +2 -113
- package/connectors/figma/src/tools.ts +99 -0
- package/connectors/firebase/src/server.ts +2 -90
- package/connectors/firebase/src/token.ts +2 -5
- package/connectors/firebase/src/tools.ts +106 -0
- package/connectors/flagsmith/src/server.ts +2 -98
- package/connectors/flagsmith/src/tools.ts +95 -0
- package/connectors/flux/src/server.ts +52 -53
- package/connectors/gcp/src/server.ts +2 -132
- package/connectors/gcp/src/tools.ts +123 -0
- package/connectors/github/src/server.ts +2 -328
- package/connectors/github/src/tools.ts +311 -0
- package/connectors/github-actions/src/server.ts +2 -205
- package/connectors/github-actions/src/tools.ts +209 -0
- package/connectors/gitlab/src/server.ts +2 -384
- package/connectors/gitlab/src/tools.ts +392 -0
- package/connectors/gmail/src/server.ts +2 -270
- package/connectors/gmail/src/tools.ts +209 -0
- package/connectors/google-drive/src/server.ts +2 -524
- package/connectors/google-drive/src/tools.ts +529 -0
- package/connectors/google-meet/src/server.ts +2 -82
- package/connectors/google-meet/src/tools.ts +87 -0
- package/connectors/google-photos/src/server.ts +2 -132
- package/connectors/google-photos/src/tools.ts +138 -0
- package/connectors/grafana/src/server.ts +2 -53
- package/connectors/grafana/src/tools.ts +53 -0
- package/connectors/great-expectations/src/gx-parse.ts +14 -45
- package/connectors/greenhouse/src/server.ts +2 -63
- package/connectors/greenhouse/src/tools.ts +68 -0
- package/connectors/hubspot/src/server.ts +2 -77
- package/connectors/hubspot/src/tools.ts +70 -0
- package/connectors/iac/src/server.ts +2 -137
- package/connectors/iac/src/tools.ts +153 -0
- package/connectors/imap/src/imap-core.ts +29 -82
- package/connectors/imap/src/server.ts +33 -220
- package/connectors/intercom/src/server.ts +2 -61
- package/connectors/intercom/src/tools.ts +58 -0
- package/connectors/jenkins/src/jenkins-api.ts +2 -6
- package/connectors/jenkins/src/server.ts +2 -215
- package/connectors/jenkins/src/tools.ts +240 -0
- package/connectors/jira/src/server.ts +2 -301
- package/connectors/jira/src/tools.ts +325 -0
- package/connectors/kubernetes/src/server.ts +2 -155
- package/connectors/kubernetes/src/tools.ts +160 -0
- package/connectors/launchdarkly/src/server.ts +2 -75
- package/connectors/launchdarkly/src/tools.ts +72 -0
- package/connectors/lever/src/search-filter.ts +4 -12
- package/connectors/lever/src/server.ts +2 -60
- package/connectors/lever/src/tools.ts +61 -0
- package/connectors/linear/src/server.ts +2 -459
- package/connectors/linear/src/tools.ts +482 -0
- package/connectors/localdb/src/sql-scan.ts +13 -41
- package/connectors/localdb/src/tools.ts +1 -1
- package/connectors/looker/src/search-filter.ts +2 -15
- package/connectors/looker/src/server.ts +35 -70
- package/connectors/mendeley/src/server.ts +2 -55
- package/connectors/mendeley/src/tools.ts +54 -0
- package/connectors/mercury/src/server.ts +2 -57
- package/connectors/mercury/src/tools.ts +50 -0
- package/connectors/metabase/src/search-filter.ts +4 -15
- package/connectors/metabase/src/server.ts +2 -79
- package/connectors/metabase/src/tools.ts +64 -0
- package/connectors/miro/src/server.ts +2 -72
- package/connectors/miro/src/tools.ts +65 -0
- package/connectors/mlflow/src/search-filter.ts +3 -7
- package/connectors/mlflow/src/server.ts +20 -40
- package/connectors/monte-carlo/src/server.ts +50 -57
- package/connectors/netlify/src/search-filter.ts +6 -14
- package/connectors/netlify/src/server.ts +2 -60
- package/connectors/netlify/src/tools.ts +53 -0
- package/connectors/newrelic/src/server.ts +2 -40
- package/connectors/newrelic/src/tools.ts +37 -0
- package/connectors/notion/src/server.ts +2 -335
- package/connectors/notion/src/tools.ts +344 -0
- package/connectors/obsidian/src/server.ts +2 -425
- package/connectors/obsidian/src/tools.ts +466 -0
- package/connectors/onedrive/src/server.ts +2 -249
- package/connectors/onedrive/src/tools.ts +253 -0
- package/connectors/outlook/src/server.ts +2 -351
- package/connectors/outlook/src/tools.ts +357 -0
- package/connectors/pagerduty/src/server.ts +2 -132
- package/connectors/pagerduty/src/tools.ts +126 -0
- package/connectors/pipedrive/src/server.ts +2 -58
- package/connectors/pipedrive/src/tools.ts +63 -0
- package/connectors/powerbi/src/search-filter.ts +2 -15
- package/connectors/powerbi/src/server.ts +43 -68
- package/connectors/prefect/src/search-filter.ts +3 -16
- package/connectors/prefect/src/server.ts +2 -90
- package/connectors/prefect/src/tools.ts +83 -0
- package/connectors/protonmail/src/mail-core.ts +16 -75
- package/connectors/protonmail/src/server.ts +34 -214
- package/connectors/raindrop/src/server.ts +2 -103
- package/connectors/raindrop/src/tools.ts +103 -0
- package/connectors/ramp/src/server.ts +2 -105
- package/connectors/ramp/src/tools.ts +98 -0
- package/connectors/readwise/src/server.ts +2 -88
- package/connectors/readwise/src/tools.ts +81 -0
- package/connectors/sagemaker/src/tools.ts +21 -40
- package/connectors/salesforce/src/search-filter.ts +4 -11
- package/connectors/salesforce/src/server.ts +2 -90
- package/connectors/salesforce/src/tools.ts +75 -0
- package/connectors/semgrep/src/search-filter.ts +3 -10
- package/connectors/semgrep/src/server.ts +2 -104
- package/connectors/semgrep/src/tools.ts +92 -0
- package/connectors/sentry/src/server.ts +2 -58
- package/connectors/sentry/src/tools.ts +53 -0
- package/connectors/slack/src/server.ts +2 -352
- package/connectors/slack/src/tools.ts +376 -0
- package/connectors/snowflake/src/search-filter.ts +4 -19
- package/connectors/snowflake/src/server.ts +6 -25
- package/connectors/snyk/src/server.ts +2 -140
- package/connectors/snyk/src/tools.ts +136 -0
- package/connectors/sonarqube/src/server.ts +2 -111
- package/connectors/sonarqube/src/tools.ts +98 -0
- package/connectors/stackoverflow/src/server.ts +2 -77
- package/connectors/stackoverflow/src/tools.ts +66 -0
- package/connectors/storybook/src/storybook-parse.ts +3 -6
- package/connectors/stripe/src/server.ts +2 -61
- package/connectors/stripe/src/tools.ts +54 -0
- package/connectors/superset/src/search-filter.ts +4 -15
- package/connectors/superset/src/server.ts +2 -115
- package/connectors/superset/src/tools.ts +98 -0
- package/connectors/tableau/src/search-filter.ts +2 -15
- package/connectors/tableau/src/server.ts +13 -65
- package/connectors/teams/src/server.ts +2 -249
- package/connectors/teams/src/tools.ts +283 -0
- package/connectors/testflight/src/server.ts +2 -70
- package/connectors/testflight/src/tools.ts +79 -0
- package/connectors/vercel/src/search-filter.ts +2 -10
- package/connectors/vercel/src/server.ts +2 -75
- package/connectors/vercel/src/tools.ts +73 -0
- package/connectors/vertex-ai/src/tools.ts +23 -33
- package/connectors/wiz/src/search-filter.ts +6 -30
- package/connectors/wiz/src/server.ts +2 -188
- package/connectors/wiz/src/tools.ts +168 -0
- package/connectors/zendesk/src/server.ts +2 -73
- package/connectors/zendesk/src/tools.ts +60 -0
- package/connectors/zoom/src/search-filter.ts +1 -7
- package/connectors/zoom/src/server.ts +2 -94
- package/connectors/zoom/src/tools.ts +93 -0
- package/connectors/zotero/src/server.ts +2 -78
- package/connectors/zotero/src/tools.ts +67 -0
- package/package.json +14 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +601 -34
- package/shared/cli-json-kit.ts +191 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/consent-kit.ts +81 -26
- package/shared/cursor-list-tool.ts +29 -0
- package/shared/env-json-api.ts +209 -0
- package/shared/fetch-json-text.ts +27 -0
- package/shared/github-rest.ts +62 -0
- package/shared/graphql-json.ts +60 -0
- package/shared/imap-mail-core.ts +91 -0
- package/shared/imap-tool-kit.ts +57 -23
- package/shared/imapflow-adapter.ts +417 -0
- package/shared/nimbus-spawn.ts +84 -27
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/safe-cli-arg.ts +139 -17
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/shared/windows-batch-args.ts +273 -0
- package/shared/write-budget.ts +100 -0
- package/standalone/src/launcher.ts +19 -7
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* imapflow-adapter — the real IMAP client + SMTP mailer over `imapflow` and
|
|
3
|
+
* `nodemailer`, shared by every IMAP-based mail connector (imap, protonmail).
|
|
4
|
+
*
|
|
5
|
+
* These two class bodies were previously written out in full in each
|
|
6
|
+
* connector's `server.ts`. `imap-tool-kit.ts` deliberately left them alone at
|
|
7
|
+
* the time ("class bodies … are out of scope per the dedup brief"), and the
|
|
8
|
+
* cost of that was measurable on both axes Sonar reports:
|
|
9
|
+
*
|
|
10
|
+
* - duplication: `imap/src/server.ts` 41.2% and `protonmail/src/server.ts`
|
|
11
|
+
* 38.8%, 186 duplicated lines between them — the largest pair in the repo;
|
|
12
|
+
* - coverage: both files were 0%. An entry point that constructs its adapters
|
|
13
|
+
* at module scope cannot be imported by a test without opening real
|
|
14
|
+
* sockets, so every line of transport logic in them was unasserted.
|
|
15
|
+
*
|
|
16
|
+
* Extracting the classes here fixes both: one copy, and one that a test can
|
|
17
|
+
* construct because the `imapflow` client and the `nodemailer` transport are
|
|
18
|
+
* injected seams rather than direct `new` calls. The two connectors differ only
|
|
19
|
+
* in configuration (TLS mode, ports, env var names, mailbox default), so all of
|
|
20
|
+
* it is passed in.
|
|
21
|
+
*
|
|
22
|
+
* HARD SCOPE CONSTRAINT (security), inherited from both connectors: the fetch
|
|
23
|
+
* query below requests ONLY `envelope` + `bodyStructure` + a `PREVIEW_FETCH_BYTES`-
|
|
24
|
+
* capped text part. There is no surface here to request `BODY[]` or an
|
|
25
|
+
* attachment part, so no caller can make this adapter download message bodies
|
|
26
|
+
* or attachment bytes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
type FetchMessageObject,
|
|
31
|
+
type FetchQueryObject,
|
|
32
|
+
ImapFlow,
|
|
33
|
+
type MessageAddressObject,
|
|
34
|
+
} from "imapflow";
|
|
35
|
+
import { createTransport, type Transporter } from "nodemailer";
|
|
36
|
+
import {
|
|
37
|
+
type BodyStructureNode,
|
|
38
|
+
extractAttachments,
|
|
39
|
+
findTextPlainPart,
|
|
40
|
+
} from "./imap-bodystructure.ts";
|
|
41
|
+
import { clampLimit, type MailAddress, PREVIEW_FETCH_BYTES } from "./imap-mail-core.ts";
|
|
42
|
+
import {
|
|
43
|
+
type EmailMessageMeta,
|
|
44
|
+
type EmailReadClient,
|
|
45
|
+
type EmailSendMailer,
|
|
46
|
+
previewFromParts,
|
|
47
|
+
} from "./imap-tool-kit.ts";
|
|
48
|
+
|
|
49
|
+
/** Mailbox used when a tool call does not name one. */
|
|
50
|
+
export const DEFAULT_MAILBOX = "INBOX";
|
|
51
|
+
|
|
52
|
+
/** Connection settings for one IMAP endpoint. Read from `process.env` by the caller. */
|
|
53
|
+
export interface ImapEndpointConfig {
|
|
54
|
+
readonly host: string;
|
|
55
|
+
readonly port: number;
|
|
56
|
+
readonly user: string;
|
|
57
|
+
readonly pass: string;
|
|
58
|
+
/**
|
|
59
|
+
* Implicit TLS on connect. `true` for a normal IMAPS endpoint on 993; `false`
|
|
60
|
+
* for the ProtonMail Bridge, which speaks STARTTLS on loopback.
|
|
61
|
+
*/
|
|
62
|
+
readonly secure: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Chain verification. Omit (the default) for a real endpoint. Only the
|
|
65
|
+
* ProtonMail Bridge passes `false`, for its self-signed loopback certificate;
|
|
66
|
+
* the connection never leaves 127.0.0.1.
|
|
67
|
+
*/
|
|
68
|
+
readonly rejectUnauthorized?: boolean;
|
|
69
|
+
/** Mailbox used when a call does not name one. Defaults to {@link DEFAULT_MAILBOX}. */
|
|
70
|
+
readonly defaultMailbox?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Connection settings for one SMTP endpoint. */
|
|
74
|
+
export interface SmtpEndpointConfig {
|
|
75
|
+
readonly host: string;
|
|
76
|
+
readonly port: number;
|
|
77
|
+
readonly user: string;
|
|
78
|
+
readonly pass: string;
|
|
79
|
+
readonly secure: boolean;
|
|
80
|
+
readonly rejectUnauthorized?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* There is deliberately no `requireTLS` option.
|
|
85
|
+
*
|
|
86
|
+
* When `secure` is false this adapter ALWAYS demands STARTTLS, and that is not
|
|
87
|
+
* a default a caller can override — an unencrypted SMTP session is not a
|
|
88
|
+
* configuration this module offers. Without it nodemailer falls back to
|
|
89
|
+
* plaintext when the server does not advertise STARTTLS, and the password goes
|
|
90
|
+
* out in the clear.
|
|
91
|
+
*
|
|
92
|
+
* The gap was live, not hypothetical: `imap` computes `secure: port === 465`,
|
|
93
|
+
* so an operator setting `IMAP_SMTP_PORT=587` — the standard submission port —
|
|
94
|
+
* got `secure: false` with no `requireTLS` at all. Of the three copies this
|
|
95
|
+
* adapter replaced, only Apple's set it.
|
|
96
|
+
|
|
97
|
+
/** Constructs the `imapflow` client. Overridden in tests so no socket is opened. */
|
|
98
|
+
export type ImapFlowFactory = (options: ConstructorParameters<typeof ImapFlow>[0]) => ImapFlow;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The transport options this adapter builds. Declared here rather than reused
|
|
102
|
+
* from nodemailer: `Parameters<typeof createTransport>[0]` resolves to the
|
|
103
|
+
* union `Transport | TransportOptions`, and a plain SMTP option literal is not
|
|
104
|
+
* assignable to that union.
|
|
105
|
+
*/
|
|
106
|
+
export interface SmtpTransportOptions {
|
|
107
|
+
readonly host: string;
|
|
108
|
+
readonly port: number;
|
|
109
|
+
readonly secure: boolean;
|
|
110
|
+
readonly auth: { readonly user: string; readonly pass: string };
|
|
111
|
+
/** Always set by this module, and always `true` when `secure` is false. */
|
|
112
|
+
readonly requireTLS: boolean;
|
|
113
|
+
readonly tls?: { readonly rejectUnauthorized: boolean };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Constructs the `nodemailer` transport. Overridden in tests so no socket is opened. */
|
|
117
|
+
export type TransportFactory = (options: SmtpTransportOptions) => Transporter;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a {@link MailAddress} from an imapflow envelope address, omitting
|
|
121
|
+
* `name`/`address` when absent — the interface uses `exactOptionalPropertyTypes`,
|
|
122
|
+
* so an explicit `undefined` is not assignable.
|
|
123
|
+
*
|
|
124
|
+
* The parameter is imapflow's own `MessageAddressObject`, not a restated
|
|
125
|
+
* `{ name?: string; address?: string }`. imapflow 2 generates its declarations,
|
|
126
|
+
* which spell the fields `name?: string | undefined`, and under
|
|
127
|
+
* `exactOptionalPropertyTypes` a hand-written shape without the `| undefined`
|
|
128
|
+
* stops accepting the library's value.
|
|
129
|
+
*/
|
|
130
|
+
function toMailAddress(a: MessageAddressObject): MailAddress {
|
|
131
|
+
const out: { name?: string; address?: string } = {};
|
|
132
|
+
if (a.name !== undefined) {
|
|
133
|
+
out.name = a.name;
|
|
134
|
+
}
|
|
135
|
+
if (a.address !== undefined) {
|
|
136
|
+
out.address = a.address;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function envelopeFromImap(env: FetchMessageObject["envelope"]): EmailMessageMeta["envelope"] {
|
|
142
|
+
if (env === undefined) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
date: env.date ?? null,
|
|
147
|
+
subject: env.subject ?? null,
|
|
148
|
+
messageId: env.messageId ?? null,
|
|
149
|
+
from: (env.from ?? []).map(toMailAddress),
|
|
150
|
+
to: (env.to ?? []).map(toMailAddress),
|
|
151
|
+
cc: (env.cc ?? []).map(toMailAddress),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Reduce one fetched message to the header + attachment-metadata + preview view. */
|
|
156
|
+
export function toMessageMeta(
|
|
157
|
+
msg: FetchMessageObject,
|
|
158
|
+
mailbox: string,
|
|
159
|
+
uidValidity: string | null,
|
|
160
|
+
): EmailMessageMeta {
|
|
161
|
+
const structure = (msg.bodyStructure ?? null) as BodyStructureNode | null;
|
|
162
|
+
const partKey = findTextPlainPart(structure);
|
|
163
|
+
return {
|
|
164
|
+
uid: msg.uid,
|
|
165
|
+
mailbox,
|
|
166
|
+
uidValidity,
|
|
167
|
+
envelope: envelopeFromImap(msg.envelope),
|
|
168
|
+
attachments: extractAttachments(structure),
|
|
169
|
+
preview: previewFromParts(msg.bodyParts, partKey),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fetch query that requests ONLY headers + structure + a truncated text body
|
|
175
|
+
* part. There is no `source` / full-body request here by construction.
|
|
176
|
+
*/
|
|
177
|
+
export function previewFetchQuery(): FetchQueryObject {
|
|
178
|
+
return {
|
|
179
|
+
uid: true,
|
|
180
|
+
envelope: true,
|
|
181
|
+
bodyStructure: true,
|
|
182
|
+
internalDate: true,
|
|
183
|
+
// Two candidate text part keys, each capped to PREVIEW_FETCH_BYTES bytes.
|
|
184
|
+
bodyParts: [
|
|
185
|
+
{ key: "1", start: 0, maxLength: PREVIEW_FETCH_BYTES },
|
|
186
|
+
{ key: "TEXT", start: 0, maxLength: PREVIEW_FETCH_BYTES },
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* imapflow answers `undefined` from `fetchOne` and `search` when it has no
|
|
193
|
+
* mailbox selected — reachable under the lock `withMailbox` holds only if the
|
|
194
|
+
* connection closed after it was taken.
|
|
195
|
+
*
|
|
196
|
+
* imapflow 1.x did the same at runtime but declared no such return, so the
|
|
197
|
+
* value reached `toMessageMeta` / `.length` and failed as a bare TypeError. The
|
|
198
|
+
* generated declarations of 2.x surface it. It stays a FAILURE, now one that
|
|
199
|
+
* says what happened — not "no such message" or "no results", which would
|
|
200
|
+
* report a dropped connection as an answer.
|
|
201
|
+
*/
|
|
202
|
+
function mailboxNotSelected(mailbox: string): Error {
|
|
203
|
+
return new Error(`imap: mailbox "${mailbox}" is no longer selected (connection closed?)`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Real IMAP client over imapflow. Per-call connect/lock/logout keeps it
|
|
208
|
+
* stateless, so nothing is held open between tool calls.
|
|
209
|
+
*/
|
|
210
|
+
class ImapFlowClient implements EmailReadClient {
|
|
211
|
+
private readonly defaultMailbox: string;
|
|
212
|
+
|
|
213
|
+
constructor(
|
|
214
|
+
private readonly config: ImapEndpointConfig,
|
|
215
|
+
private readonly newImapFlow: ImapFlowFactory,
|
|
216
|
+
) {
|
|
217
|
+
this.defaultMailbox = config.defaultMailbox ?? DEFAULT_MAILBOX;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private newClient(): ImapFlow {
|
|
221
|
+
const { host, port, secure, user, pass, rejectUnauthorized } = this.config;
|
|
222
|
+
return this.newImapFlow({
|
|
223
|
+
host,
|
|
224
|
+
port,
|
|
225
|
+
secure,
|
|
226
|
+
auth: { user, pass },
|
|
227
|
+
logger: false,
|
|
228
|
+
// Only set when the caller asked to relax it — see ImapEndpointConfig.
|
|
229
|
+
...(rejectUnauthorized === undefined ? {} : { tls: { rejectUnauthorized } }),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async withMailbox<T>(
|
|
234
|
+
mailbox: string,
|
|
235
|
+
fn: (client: ImapFlow, uidValidity: string | null) => Promise<T>,
|
|
236
|
+
): Promise<T> {
|
|
237
|
+
const client = this.newClient();
|
|
238
|
+
await client.connect();
|
|
239
|
+
// `logout` sits in an OUTER finally so the connection is closed even when
|
|
240
|
+
// `getMailboxLock` throws. Acquiring the lock BEFORE the try — which is how
|
|
241
|
+
// imap and protonmail both had it — leaks the connected socket on a locking
|
|
242
|
+
// failure (a mailbox that does not exist, or one another client holds), and
|
|
243
|
+
// this client connects per call, so every such failure leaks another.
|
|
244
|
+
// Apple's copy already nested it this way; the correction travelled the
|
|
245
|
+
// wrong direction when these were three separate copies.
|
|
246
|
+
try {
|
|
247
|
+
const lock = await client.getMailboxLock(mailbox);
|
|
248
|
+
try {
|
|
249
|
+
const mb = client.mailbox;
|
|
250
|
+
const uidValidity = mb === false ? null : String(mb.uidValidity);
|
|
251
|
+
return await fn(client, uidValidity);
|
|
252
|
+
} finally {
|
|
253
|
+
lock.release();
|
|
254
|
+
}
|
|
255
|
+
} finally {
|
|
256
|
+
await client.logout();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async list(options: { mailbox?: string; limit?: number }): Promise<EmailMessageMeta[]> {
|
|
261
|
+
const mailbox = options.mailbox ?? this.defaultMailbox;
|
|
262
|
+
const limit = clampLimit(options.limit);
|
|
263
|
+
return this.withMailbox(mailbox, async (client, uidValidity) => {
|
|
264
|
+
const out: EmailMessageMeta[] = [];
|
|
265
|
+
const status = await client.status(mailbox, { messages: true });
|
|
266
|
+
// `false` is a STATUS the server rejected, read as an empty mailbox. That
|
|
267
|
+
// is what this did under imapflow 1.x too, which returned the same `false`
|
|
268
|
+
// but declared only `StatusObject`, so `status.messages ?? 0` made it 0.
|
|
269
|
+
const total = status === false ? 0 : (status.messages ?? 0);
|
|
270
|
+
if (total === 0) {
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
const start = Math.max(1, total - limit + 1);
|
|
274
|
+
const range = `${start}:${total}`;
|
|
275
|
+
for await (const msg of client.fetch(range, previewFetchQuery())) {
|
|
276
|
+
out.push(toMessageMeta(msg, mailbox, uidValidity));
|
|
277
|
+
}
|
|
278
|
+
// Most-recent first.
|
|
279
|
+
out.sort((a, b) => b.uid - a.uid);
|
|
280
|
+
return out;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async get(uid: number, mailbox?: string): Promise<EmailMessageMeta | null> {
|
|
285
|
+
const box = mailbox ?? this.defaultMailbox;
|
|
286
|
+
return this.withMailbox(box, async (client, uidValidity) => {
|
|
287
|
+
const msg = await client.fetchOne(String(uid), previewFetchQuery(), { uid: true });
|
|
288
|
+
if (msg === undefined) {
|
|
289
|
+
throw mailboxNotSelected(box);
|
|
290
|
+
}
|
|
291
|
+
return msg === false ? null : toMessageMeta(msg, box, uidValidity);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async search(options: {
|
|
296
|
+
query: string;
|
|
297
|
+
mailbox?: string;
|
|
298
|
+
limit?: number;
|
|
299
|
+
}): Promise<EmailMessageMeta[]> {
|
|
300
|
+
const mailbox = options.mailbox ?? this.defaultMailbox;
|
|
301
|
+
const limit = clampLimit(options.limit);
|
|
302
|
+
const q = options.query;
|
|
303
|
+
return this.withMailbox(mailbox, async (client, uidValidity) => {
|
|
304
|
+
const uids = await client.search(
|
|
305
|
+
{ or: [{ subject: q }, { from: q }, { to: q }] },
|
|
306
|
+
{ uid: true },
|
|
307
|
+
);
|
|
308
|
+
if (uids === undefined) {
|
|
309
|
+
throw mailboxNotSelected(mailbox);
|
|
310
|
+
}
|
|
311
|
+
if (uids === false || uids.length === 0) {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
const picked = uids.slice(-limit).sort((a, b) => b - a);
|
|
315
|
+
const out: EmailMessageMeta[] = [];
|
|
316
|
+
for await (const msg of client.fetch(picked, previewFetchQuery(), { uid: true })) {
|
|
317
|
+
out.push(toMessageMeta(msg, mailbox, uidValidity));
|
|
318
|
+
}
|
|
319
|
+
out.sort((a, b) => b.uid - a.uid);
|
|
320
|
+
return out;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Real SMTP mailer over nodemailer.
|
|
327
|
+
*
|
|
328
|
+
* The envelope `from` is PINNED to the authenticated user and is not a `send`
|
|
329
|
+
* parameter, so no caller can send as someone else (the forced-sender contract
|
|
330
|
+
* the mail connectors document).
|
|
331
|
+
*/
|
|
332
|
+
class NodemailerMailer implements EmailSendMailer {
|
|
333
|
+
private readonly transport: Transporter;
|
|
334
|
+
private readonly from: string;
|
|
335
|
+
|
|
336
|
+
constructor(config: SmtpEndpointConfig, makeTransport: TransportFactory) {
|
|
337
|
+
const { host, port, secure, user, pass, rejectUnauthorized } = config;
|
|
338
|
+
this.from = user;
|
|
339
|
+
this.transport = makeTransport({
|
|
340
|
+
host,
|
|
341
|
+
port,
|
|
342
|
+
secure,
|
|
343
|
+
auth: { user, pass },
|
|
344
|
+
// Implicit TLS already encrypts the session; otherwise STARTTLS is
|
|
345
|
+
// required. Not a default — there is no input to this mailer that
|
|
346
|
+
// produces a transport willing to send in the clear.
|
|
347
|
+
requireTLS: !secure,
|
|
348
|
+
...(rejectUnauthorized === undefined ? {} : { tls: { rejectUnauthorized } }),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async send(input: {
|
|
353
|
+
to: string;
|
|
354
|
+
subject: string;
|
|
355
|
+
body: string;
|
|
356
|
+
cc?: string;
|
|
357
|
+
bcc?: string;
|
|
358
|
+
}): Promise<{
|
|
359
|
+
messageId: string | null;
|
|
360
|
+
accepted: readonly string[];
|
|
361
|
+
rejected: readonly string[];
|
|
362
|
+
}> {
|
|
363
|
+
const info = await this.transport.sendMail({
|
|
364
|
+
from: this.from,
|
|
365
|
+
to: input.to,
|
|
366
|
+
subject: input.subject,
|
|
367
|
+
text: input.body,
|
|
368
|
+
...(input.cc === undefined ? {} : { cc: input.cc }),
|
|
369
|
+
...(input.bcc === undefined ? {} : { bcc: input.bcc }),
|
|
370
|
+
});
|
|
371
|
+
return {
|
|
372
|
+
messageId: info.messageId ?? null,
|
|
373
|
+
accepted: (info.accepted ?? []).map(String),
|
|
374
|
+
rejected: (info.rejected ?? []).map(String),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The IMAP read client for `config`. `newImapFlow` defaults to the real
|
|
381
|
+
* `imapflow` constructor; tests pass a fake so no socket is opened.
|
|
382
|
+
*/
|
|
383
|
+
export function createImapFlowClient(
|
|
384
|
+
config: ImapEndpointConfig,
|
|
385
|
+
newImapFlow: ImapFlowFactory = (options) => new ImapFlow(options),
|
|
386
|
+
): EmailReadClient {
|
|
387
|
+
return new ImapFlowClient(config, newImapFlow);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Construct the real nodemailer transport.
|
|
392
|
+
*
|
|
393
|
+
* Written as two explicit branches rather than passing `options` straight
|
|
394
|
+
* through. `NodemailerMailer` has already computed the guarantee, but a
|
|
395
|
+
* computed value is not something a reader — or a static analyser — can check
|
|
396
|
+
* at the call that actually opens the socket. Here it is a literal on both
|
|
397
|
+
* paths: implicit TLS, or STARTTLS required. There is no third branch.
|
|
398
|
+
*/
|
|
399
|
+
function realTransport(options: SmtpTransportOptions): Transporter {
|
|
400
|
+
const { host, port, auth } = options;
|
|
401
|
+
const tls = options.tls === undefined ? {} : { tls: options.tls };
|
|
402
|
+
if (options.secure) {
|
|
403
|
+
return createTransport({ host, port, auth, ...tls, secure: true });
|
|
404
|
+
}
|
|
405
|
+
return createTransport({ host, port, auth, ...tls, secure: false, requireTLS: true });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The SMTP mailer for `config`. `makeTransport` defaults to the real nodemailer
|
|
410
|
+
* transport; tests pass a fake so no socket is opened.
|
|
411
|
+
*/
|
|
412
|
+
export function createNodemailerMailer(
|
|
413
|
+
config: SmtpEndpointConfig,
|
|
414
|
+
makeTransport: TransportFactory = realTransport,
|
|
415
|
+
): EmailSendMailer {
|
|
416
|
+
return new NodemailerMailer(config, makeTransport);
|
|
417
|
+
}
|
package/shared/nimbus-spawn.ts
CHANGED
|
@@ -1,7 +1,70 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* nimbus-spawn — the one place this package starts a process.
|
|
3
|
+
*
|
|
4
|
+
* Every connector that reaches its service through a CLI spawns it through {@link nimbusSpawn},
|
|
5
|
+
* directly or by way of `run-cli-json.ts` and `cli-json-kit.ts`, and `scripts/spawn-chokepoint.test.ts`
|
|
6
|
+
* fails any other shipped file that imports a module able to start a process, in any import form,
|
|
7
|
+
* or refers to the `Bun` global at all. That is what makes a rule enforced here hold for every
|
|
8
|
+
* connector: today, `windows-batch-args.ts`'s refusal of an argument cmd.exe would act on, when the
|
|
9
|
+
* program may start a Windows batch file. Both implementations build the child's environment once
|
|
10
|
+
* and hand the SAME object to that check and to the spawn, so the environment that was checked is
|
|
11
|
+
* the one the child gets.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
15
|
+
import { batchArgumentRefusal } from "./windows-batch-args.ts";
|
|
2
16
|
|
|
3
17
|
export type SpawnResult = { code: number; stdout: string; stderr: string };
|
|
4
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The child's environment for `env`, and why `command` must not be spawned with it, if it must
|
|
21
|
+
* not. Nothing is spawned for a refused command: it resolves as a failure whose stderr says so.
|
|
22
|
+
*/
|
|
23
|
+
function prepareSpawn(
|
|
24
|
+
command: readonly string[],
|
|
25
|
+
env: Record<string, string | undefined>,
|
|
26
|
+
): { childEnv: Record<string, string | undefined>; refusal: SpawnResult | undefined } {
|
|
27
|
+
const childEnv = { ...process.env, ...env };
|
|
28
|
+
const refusal = batchArgumentRefusal(command, childEnv);
|
|
29
|
+
return {
|
|
30
|
+
childEnv,
|
|
31
|
+
refusal: refusal === undefined ? undefined : { code: 1, stdout: "", stderr: refusal },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Collect a child's output and resolve with it once the child closes or fails to start.
|
|
37
|
+
*
|
|
38
|
+
* Output is accumulated as raw `Buffer`s and decoded ONCE at the end. Decoding each chunk with
|
|
39
|
+
* `chunk.toString("utf8")` corrupts any multi-byte character that straddles a chunk boundary, and
|
|
40
|
+
* chunk boundaries are a function of pipe timing — so it fails intermittently, on non-ASCII data,
|
|
41
|
+
* in production.
|
|
42
|
+
*/
|
|
43
|
+
function collectOutput(
|
|
44
|
+
child: ChildProcessWithoutNullStreams,
|
|
45
|
+
resolveP: (result: SpawnResult) => void,
|
|
46
|
+
): void {
|
|
47
|
+
const outChunks: Buffer[] = [];
|
|
48
|
+
const errChunks: Buffer[] = [];
|
|
49
|
+
child.stdout.on("data", (c: Buffer) => {
|
|
50
|
+
outChunks.push(c);
|
|
51
|
+
});
|
|
52
|
+
child.stderr.on("data", (c: Buffer) => {
|
|
53
|
+
errChunks.push(c);
|
|
54
|
+
});
|
|
55
|
+
const decode = (): { stdout: string; stderr: string } => ({
|
|
56
|
+
stdout: Buffer.concat(outChunks).toString("utf8"),
|
|
57
|
+
stderr: Buffer.concat(errChunks).toString("utf8"),
|
|
58
|
+
});
|
|
59
|
+
child.on("error", (e: Error) => {
|
|
60
|
+
const d = decode();
|
|
61
|
+
resolveP({ code: 1, stdout: d.stdout, stderr: `${d.stderr}${e.message}` });
|
|
62
|
+
});
|
|
63
|
+
child.on("close", (code: number | null) => {
|
|
64
|
+
resolveP({ code: code ?? 1, ...decode() });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
5
68
|
/**
|
|
6
69
|
* Node implementation. Exported so it can be tested directly: the suite runs under Bun, so
|
|
7
70
|
* `nimbusSpawn` would otherwise never exercise this branch and it would ship unverified.
|
|
@@ -15,11 +78,6 @@ export type SpawnResult = { code: number; stdout: string; stderr: string };
|
|
|
15
78
|
* `Bun.spawn` path reads stdout uncapped, and `aws logs` / `gcloud logging` JSON routinely
|
|
16
79
|
* exceeds 1 MB, so `execFile` would be a silent-truncation regression dressed up as a
|
|
17
80
|
* portability fix.
|
|
18
|
-
*
|
|
19
|
-
* Output is accumulated as raw `Buffer`s and decoded ONCE at the end. Decoding each chunk with
|
|
20
|
-
* `chunk.toString("utf8")` corrupts any multi-byte character that straddles a chunk boundary, and
|
|
21
|
-
* chunk boundaries are a function of pipe timing — so it fails intermittently, on non-ASCII data,
|
|
22
|
-
* in production.
|
|
23
81
|
*/
|
|
24
82
|
export function spawnViaNode(
|
|
25
83
|
command: readonly string[],
|
|
@@ -29,27 +87,20 @@ export function spawnViaNode(
|
|
|
29
87
|
if (bin === undefined) {
|
|
30
88
|
return Promise.resolve({ code: 1, stdout: "", stderr: "empty command" });
|
|
31
89
|
}
|
|
90
|
+
const { childEnv, refusal } = prepareSpawn(command, env);
|
|
91
|
+
if (refusal !== undefined) {
|
|
92
|
+
return Promise.resolve(refusal);
|
|
93
|
+
}
|
|
32
94
|
return new Promise((resolveP) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
const decode = (): { stdout: string; stderr: string } => ({
|
|
43
|
-
stdout: Buffer.concat(outChunks).toString("utf8"),
|
|
44
|
-
stderr: Buffer.concat(errChunks).toString("utf8"),
|
|
45
|
-
});
|
|
46
|
-
child.on("error", (e: Error) => {
|
|
47
|
-
const d = decode();
|
|
48
|
-
resolveP({ code: 1, stdout: d.stdout, stderr: `${d.stderr}${e.message}` });
|
|
49
|
-
});
|
|
50
|
-
child.on("close", (code: number | null) => {
|
|
51
|
-
resolveP({ code: code ?? 1, ...decode() });
|
|
52
|
-
});
|
|
95
|
+
// `spawn` THROWS, rather than emitting "error", for a command it cannot start at all: an
|
|
96
|
+
// argument holding a NUL byte, or — on Node, though not in Bun's `node:child_process` — a
|
|
97
|
+
// `.cmd` or `.bat` without a shell, which Node refuses with EINVAL. Uncaught, the throw would
|
|
98
|
+
// REJECT this promise, against the contract both implementations keep.
|
|
99
|
+
try {
|
|
100
|
+
collectOutput(spawn(bin, args, { env: childEnv }), resolveP);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
resolveP({ code: 1, stdout: "", stderr: e instanceof Error ? e.message : String(e) });
|
|
103
|
+
}
|
|
53
104
|
});
|
|
54
105
|
}
|
|
55
106
|
|
|
@@ -61,12 +112,16 @@ export async function spawnViaBun(
|
|
|
61
112
|
if (command.length === 0) {
|
|
62
113
|
return { code: 1, stdout: "", stderr: "empty command" };
|
|
63
114
|
}
|
|
115
|
+
const { childEnv, refusal } = prepareSpawn(command, env);
|
|
116
|
+
if (refusal !== undefined) {
|
|
117
|
+
return refusal;
|
|
118
|
+
}
|
|
64
119
|
// `Bun.spawn` THROWS synchronously when the binary does not exist, where `child_process` emits
|
|
65
120
|
// an "error" event instead. Both branches must honour the same contract — resolve with a
|
|
66
121
|
// non-zero code, never reject — or a caller's behaviour would depend on the runtime.
|
|
67
122
|
try {
|
|
68
123
|
const proc = Bun.spawn([...command], {
|
|
69
|
-
env:
|
|
124
|
+
env: childEnv,
|
|
70
125
|
stdout: "pipe",
|
|
71
126
|
stderr: "pipe",
|
|
72
127
|
});
|
|
@@ -119,6 +174,8 @@ export function selectSpawnImpl(g: unknown = globalThis): SpawnImpl {
|
|
|
119
174
|
* routing unconditionally through Node made it spawn a real `aws` and hang the suite.
|
|
120
175
|
*
|
|
121
176
|
* Never rejects: a spawn failure resolves with a non-zero code, matching the previous behaviour.
|
|
177
|
+
* So does a REFUSED spawn — on Windows, an argument cmd.exe would act on, for a program that may
|
|
178
|
+
* start a batch file — whose stderr says why and that nothing was run.
|
|
122
179
|
*/
|
|
123
180
|
export function nimbusSpawn(
|
|
124
181
|
command: readonly string[],
|
package/shared/rest-tool-kit.ts
CHANGED
|
@@ -1,18 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* rest-tool-kit —
|
|
3
|
-
* connectors, now owned by `@nimbus-dev/sdk/connector-kit`.
|
|
2
|
+
* rest-tool-kit — the REST tool plumbing for token-auth connectors.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* SDK is now
|
|
7
|
-
*
|
|
4
|
+
* `makeRestFetcher` and `makeRestToolRegistrar` originated here and were upstreamed
|
|
5
|
+
* to the SDK in 1.11.0. The SDK is now their single owner; this module re-exports
|
|
6
|
+
* them so the ~99 connectors keep their existing relative imports. Named
|
|
7
|
+
* re-exports, deliberately not `export *` — see the note in `mcp-tool-kit.ts`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* The two helpers defined BELOW are this repository's own and stay here:
|
|
10
|
+
* {@link makeRestWriteToolRegistrar} registers through the consent kit, which the
|
|
11
|
+
* SDK does not have, and {@link toRestFetchResult} is the tail of the connector
|
|
12
|
+
* fetches whose credential is not a Bearer token, which the SDK's fetch cannot send.
|
|
11
13
|
*/
|
|
12
14
|
|
|
15
|
+
import {
|
|
16
|
+
type HttpJsonBodyResponse,
|
|
17
|
+
mcpJsonResultIfOk,
|
|
18
|
+
type RestFetchResult,
|
|
19
|
+
requireProcessEnv,
|
|
20
|
+
type ZodObjectSchema,
|
|
21
|
+
} from "@nimbus-dev/sdk/connector-kit";
|
|
22
|
+
import type { WriteToolConfig, WriteToolRegistrar } from "./consent-kit.ts";
|
|
23
|
+
|
|
13
24
|
export type {
|
|
14
25
|
RestFetcherConfig,
|
|
15
26
|
RestFetchResult,
|
|
16
27
|
RestToolRegistrar,
|
|
17
28
|
} from "@nimbus-dev/sdk/connector-kit";
|
|
18
29
|
export { makeRestFetcher, makeRestToolRegistrar } from "@nimbus-dev/sdk/connector-kit";
|
|
30
|
+
|
|
31
|
+
/** The per-connector registrar {@link makeRestWriteToolRegistrar} builds. */
|
|
32
|
+
export type RestWriteToolRegistrar = <T>(
|
|
33
|
+
name: string,
|
|
34
|
+
cfg: WriteToolConfig<T>,
|
|
35
|
+
description: string,
|
|
36
|
+
schema: ZodObjectSchema<T>,
|
|
37
|
+
buildPath: (args: T) => string,
|
|
38
|
+
buildInit?: (args: T) => RequestInit,
|
|
39
|
+
) => void;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The write twin of `makeRestToolRegistrar`: the same standard tool body —
|
|
43
|
+
* `token = requireProcessEnv(<env>)`, `res = await <fetch>(token, buildPath(args)
|
|
44
|
+
* [, buildInit(args)])`, `return mcpJsonResultIfOk(<label>, res, <snippetMax>)` —
|
|
45
|
+
* registered through the connector's consent-kit write registrar instead of its
|
|
46
|
+
* read-only one, so every mutation it registers is gated.
|
|
47
|
+
*
|
|
48
|
+
* The connector passes its OWN `registerWriteTool` in rather than this kit building
|
|
49
|
+
* one: the standalone launcher decides whether a connector's mutations are gated by
|
|
50
|
+
* reading that connector's own `server.ts`/`tools.ts`, and a registrar constructed in
|
|
51
|
+
* here would be invisible to it.
|
|
52
|
+
*/
|
|
53
|
+
export function makeRestWriteToolRegistrar(cfg: {
|
|
54
|
+
readonly registerWriteTool: WriteToolRegistrar;
|
|
55
|
+
readonly tokenEnv: string;
|
|
56
|
+
readonly serviceLabel: string;
|
|
57
|
+
readonly fetch: (
|
|
58
|
+
token: string,
|
|
59
|
+
pathOrUrl: string,
|
|
60
|
+
init?: RequestInit,
|
|
61
|
+
) => Promise<HttpJsonBodyResponse>;
|
|
62
|
+
/** Body-snippet length for the `<label> <status>: <snippet>` error (mcpJsonResultIfOk default 300). */
|
|
63
|
+
readonly snippetMax?: number;
|
|
64
|
+
}): RestWriteToolRegistrar {
|
|
65
|
+
return (name, toolCfg, description, schema, buildPath, buildInit) => {
|
|
66
|
+
cfg.registerWriteTool(name, toolCfg, description, schema, async (args) => {
|
|
67
|
+
const token = requireProcessEnv(cfg.tokenEnv);
|
|
68
|
+
const res = await cfg.fetch(token, buildPath(args), buildInit?.(args));
|
|
69
|
+
return mcpJsonResultIfOk(cfg.serviceLabel, res, cfg.snippetMax);
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read a response into the {@link RestFetchResult} shape a REST tool's fetch
|
|
76
|
+
* returns. `json` is the parsed body, or `null` when the body is not JSON (an empty
|
|
77
|
+
* 204, an HTML error page): what that means is the tool's decision, not this one's.
|
|
78
|
+
*/
|
|
79
|
+
export async function toRestFetchResult(res: Response): Promise<RestFetchResult> {
|
|
80
|
+
const text = await res.text();
|
|
81
|
+
let json: unknown;
|
|
82
|
+
try {
|
|
83
|
+
json = JSON.parse(text) as unknown;
|
|
84
|
+
} catch {
|
|
85
|
+
json = null;
|
|
86
|
+
}
|
|
87
|
+
return { ok: res.ok, status: res.status, json, text };
|
|
88
|
+
}
|