@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
package/NOTICE
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Nimbus first-party MCP connectors
|
|
2
|
+
Copyright (C) Nimbus contributors
|
|
3
|
+
Licensed under the GNU Affero General Public License v3.0 only (AGPL-3.0-only).
|
|
4
|
+
|
|
5
|
+
SECURITY TIERING — please preserve this notice.
|
|
6
|
+
|
|
7
|
+
Run STANDALONE (outside the Nimbus gateway), these connectors provide:
|
|
8
|
+
|
|
9
|
+
* consent for every mutating tool, via MCP elicitation, rendered by YOUR MCP
|
|
10
|
+
client;
|
|
11
|
+
* a server-enforced write-scope allow-list and a per-session mutation budget,
|
|
12
|
+
which hold regardless of client behaviour;
|
|
13
|
+
* a local, hash-chained, append-only audit log.
|
|
14
|
+
|
|
15
|
+
Run standalone, they DO NOT provide:
|
|
16
|
+
|
|
17
|
+
* the process sandbox (network allow-list and filesystem confinement) — this
|
|
18
|
+
is a property of how the Nimbus gateway spawns a connector, and no package
|
|
19
|
+
can supply it;
|
|
20
|
+
* OS-keychain credential storage — standalone credentials come from
|
|
21
|
+
environment variables supplied by whoever configures the MCP client;
|
|
22
|
+
* the egress ledger — the tamper-evident record of every outbound action;
|
|
23
|
+
* owner-controlled consent — standalone consent is mediated by your MCP
|
|
24
|
+
client, which may be configured to answer automatically.
|
|
25
|
+
|
|
26
|
+
Those four require the Nimbus gateway:
|
|
27
|
+
|
|
28
|
+
https://github.com/nimbus-agent/Nimbus
|
|
29
|
+
|
|
30
|
+
If a client does not advertise the MCP `elicitation` capability, mutating tools
|
|
31
|
+
are NOT REGISTERED AT ALL. That is deliberate, not a defect: a tool the model
|
|
32
|
+
cannot see is a tool it cannot call without a human.
|
|
33
|
+
|
|
34
|
+
Connectors whose mutating tools have not yet been routed through the consent
|
|
35
|
+
kit refuse to start standalone. They remain fully available through the
|
|
36
|
+
gateway, which gates them.
|
|
37
|
+
|
|
38
|
+
"Nimbus" is a trademark of the Nimbus project. This licence grants no trademark
|
|
39
|
+
rights; a modified version that removes these protections must not be described
|
|
40
|
+
as Nimbus-grade or imply endorsement by the Nimbus project.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Nimbus MCP Servers
|
|
2
|
+
|
|
3
|
+
Standalone, MCP-standard [**Nimbus**](https://github.com/nimbus-agent/Nimbus) connectors — usable by
|
|
4
|
+
**any** MCP client (Claude Code, Cursor, Claude Desktop, …), not just the Nimbus gateway.
|
|
5
|
+
|
|
6
|
+
All **94** connectors ship in one package, `@nimbus-dev/connectors`, each runnable standalone over
|
|
7
|
+
stdio with credentials supplied from the environment.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @nimbus-dev/connectors github
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What you get, and what you do not
|
|
14
|
+
|
|
15
|
+
Run standalone, a connector gives you:
|
|
16
|
+
|
|
17
|
+
- **Consent before every mutation.** A write tool asks your MCP client to put the exact operation and
|
|
18
|
+
parameters in front of you, and does nothing unless you approve.
|
|
19
|
+
- **A write-scope allow-list**, enforced by the server and unreachable by the model.
|
|
20
|
+
- **A mutation budget** per session, which caps a runaway agent loop.
|
|
21
|
+
- **A local, hash-chained, append-only audit log**, when `NIMBUS_MCP_AUDIT_LOG` is set.
|
|
22
|
+
|
|
23
|
+
It does **not** give you the process sandbox, OS-keychain credential storage, the egress ledger, or
|
|
24
|
+
owner-controlled consent. Those are properties of the Nimbus gateway and no published package can
|
|
25
|
+
supply them. [`NOTICE`](./NOTICE) states the tiering that notice asks you to preserve.
|
|
26
|
+
|
|
27
|
+
## Writes depend on your client
|
|
28
|
+
|
|
29
|
+
Reads work everywhere. **Writes require your client to implement the MCP `elicitation` capability**,
|
|
30
|
+
because that is the only way a server can put a consent prompt in front of you. Without it, write
|
|
31
|
+
tools are **not registered at all** rather than offered ungated — deliberate, not a defect: a tool
|
|
32
|
+
the model cannot see is one it cannot call without a human.
|
|
33
|
+
|
|
34
|
+
**Claude Desktop does not implement elicitation today**, so you get reads only there. Claude Code and
|
|
35
|
+
Cursor do, per their vendor docs.
|
|
36
|
+
|
|
37
|
+
Checking your own client takes one query: ask it to list the connector's tools. If the write tools are
|
|
38
|
+
absent, your client does not implement elicitation.
|
|
39
|
+
|
|
40
|
+
Full matrix, versions tested, and the provenance of each claim:
|
|
41
|
+
[Client support](./docs/client-support.md).
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"nimbus-github": {
|
|
49
|
+
"command": "npx",
|
|
50
|
+
"args": ["-y", "@nimbus-dev/connectors", "github"],
|
|
51
|
+
"env": {
|
|
52
|
+
"GITHUB_PAT": "ghp_...",
|
|
53
|
+
"NIMBUS_MCP_GITHUB_WRITE_SCOPE": "repo:acme/api",
|
|
54
|
+
"NIMBUS_MCP_AUDIT_LOG": "/absolute/path/to/nimbus-mcp-audit.jsonl"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Credentials come from the environment. There is no Vault outside the gateway, so whoever writes this
|
|
62
|
+
config holds the secret. Every variable, and the two behaviours that look like bugs and are not, are
|
|
63
|
+
in [Configuration](./docs/configuration.md).
|
|
64
|
+
|
|
65
|
+
Each connector documents its own tools and credentials at `connectors/<id>/README.md`.
|
|
66
|
+
|
|
67
|
+
## Documentation
|
|
68
|
+
|
|
69
|
+
| Page | Read it when |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| [Architecture](./docs/architecture.md) | You want the layout, or how a connector is put together. |
|
|
72
|
+
| [Client support](./docs/client-support.md) | Write tools are missing, or you need to know if your client can approve a mutation. |
|
|
73
|
+
| [Configuration](./docs/configuration.md) | You are wiring a connector into a client. |
|
|
74
|
+
| [Standalone launcher](./docs/standalone-launcher.md) | You want `nimbus-connector`: eligibility, exit codes, refusals. |
|
|
75
|
+
| [Adding a connector](./docs/adding-a-connector.md) | You are writing or changing a connector. |
|
|
76
|
+
| [Publishing](./docs/publishing.md) | You are cutting a release. |
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bun install
|
|
82
|
+
bun run check # lint, typecheck, consent audit, full suite
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
CI runs those four on Ubuntu, macOS and Windows — platform equality is a Nimbus non-negotiable.
|
|
86
|
+
|
|
87
|
+
## Relationship to the Nimbus monorepo
|
|
88
|
+
|
|
89
|
+
The connectors are developed here and consumed by the gateway from npm. The gateway's per-connector
|
|
90
|
+
**sync and indexing** intelligence stays in
|
|
91
|
+
[nimbus-agent/Nimbus](https://github.com/nimbus-agent/Nimbus) — this repo holds the MCP tool surface,
|
|
92
|
+
which is the part that is useful without a gateway. Adding a connector therefore touches both repos.
|
|
93
|
+
|
|
94
|
+
Not to be confused with [`nimbus-mcp`](https://github.com/nimbus-agent/nimbus-mcp), which exposes
|
|
95
|
+
your local Nimbus **index and agents** to an MCP client. This repo is the other direction: the
|
|
96
|
+
connectors that reach your tools.
|
|
97
|
+
|
|
98
|
+
## History
|
|
99
|
+
|
|
100
|
+
This repo was a scaffold from 2026-06-18 until the connectors landed. Its original README posed three
|
|
101
|
+
"decisions to make first" — share-vs-vendor-vs-fork, the credential model outside the Vault, and the
|
|
102
|
+
AGPL implications for downstream clients. All three were answered before the move: the connectors are
|
|
103
|
+
consent-gated standalone, credentials come from the environment, and `NOTICE` states the tiering. It
|
|
104
|
+
also proposed a package per connector; one package was chosen instead, because 94 packages means 94
|
|
105
|
+
releases and forces `shared/` to become a versioned dependency that 190 files import by relative path.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
[AGPL-3.0-only](./LICENSE), and [`NOTICE`](./NOTICE) records the security tiering — please preserve
|
|
110
|
+
it. "Nimbus" is a trademark of the Nimbus project; this licence grants no trademark rights, and a
|
|
111
|
+
modified version that removes these protections must not be described as Nimbus-grade.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "com.nimbus.airflow",
|
|
3
|
+
"displayName": "Apache Airflow",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Apache Airflow connector (read-only). Surfaces the user's data-orchestration DAGs from a self-hosted Airflow instance as `airflow:dag` items in the local index — the dag_id, paused/active flags, owners, description, schedule interval, tags, source file location, next scheduled run, and last-parsed timestamp. Useful for answering orchestration questions (\"which DAGs are paused?\", \"what's the schedule for the nightly ETL?\") without leaving Nimbus. Airflow HTTP Basic auth (username + password). Airflow has no universal SaaS host: the server host is supplied via the airflow base-url vault key and added to the sandbox network list at spawn time by phase3AddAirflowMcp, so the static manifest network list is intentionally empty. The stable REST API v1 /api/v1/dags endpoint paginates by limit / offset with a total_entries count in the response body; the connector walks a single forward offset pass per cycle, capped. v1 indexes DAG definitions only — individual DAG runs and task instances are a deferred follow-up. No write tools.",
|
|
6
|
+
"author": "Nimbus",
|
|
7
|
+
"entrypoint": "dist/server.js",
|
|
8
|
+
"runtime": "bun",
|
|
9
|
+
"permissions": {
|
|
10
|
+
"network": [],
|
|
11
|
+
"filesystem": { "read": [], "write": [] }
|
|
12
|
+
},
|
|
13
|
+
"hitlRequired": [],
|
|
14
|
+
"syncInterval": 600,
|
|
15
|
+
"minNimbusVersion": "0.2.0"
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asObjectish,
|
|
3
|
+
makeQueryFilter,
|
|
4
|
+
type SearchMatchOptions,
|
|
5
|
+
stringField,
|
|
6
|
+
tagNamesFromObjects,
|
|
7
|
+
} from "../../../shared/search-filter.ts";
|
|
8
|
+
|
|
9
|
+
export type AirflowSearchMatchOptions = SearchMatchOptions;
|
|
10
|
+
|
|
11
|
+
function ownerNames(row: Record<string, unknown>): string {
|
|
12
|
+
const owners = row["owners"];
|
|
13
|
+
if (!Array.isArray(owners)) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
const names: string[] = [];
|
|
17
|
+
for (const o of owners) {
|
|
18
|
+
if (typeof o === "string" && o !== "") {
|
|
19
|
+
names.push(o);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return names.join(" ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fieldsOf(item: unknown): readonly string[] | null {
|
|
26
|
+
const row = asObjectish(item);
|
|
27
|
+
if (row === undefined) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
stringField(row, "dag_id"),
|
|
32
|
+
stringField(row, "description"),
|
|
33
|
+
ownerNames(row),
|
|
34
|
+
tagNamesFromObjects(row),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const filterAirflowDags = makeQueryFilter(fieldsOf);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
|
|
3
|
+
import {
|
|
4
|
+
encodeBasicAuthHeader,
|
|
5
|
+
mcpJsonResult as jsonResult,
|
|
6
|
+
} from "../../../shared/mcp-tool-kit.ts";
|
|
7
|
+
import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
|
|
8
|
+
import { filterAirflowDags } from "./search-filter.ts";
|
|
9
|
+
|
|
10
|
+
function trimTrailingSlash(s: string): string {
|
|
11
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function apiBase(): string {
|
|
15
|
+
const v = process.env["AIRFLOW_URL"]?.trim();
|
|
16
|
+
if (v === undefined || v === "") {
|
|
17
|
+
throw new Error("AIRFLOW_URL is not set");
|
|
18
|
+
}
|
|
19
|
+
return trimTrailingSlash(v);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function authHeader(): Record<string, string> {
|
|
23
|
+
const user = process.env["AIRFLOW_USERNAME"]?.trim();
|
|
24
|
+
if (user === undefined || user === "") {
|
|
25
|
+
throw new Error("AIRFLOW_USERNAME is not set");
|
|
26
|
+
}
|
|
27
|
+
const password = process.env["AIRFLOW_PASSWORD"]?.trim();
|
|
28
|
+
if (password === undefined || password === "") {
|
|
29
|
+
throw new Error("AIRFLOW_PASSWORD is not set");
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
Authorization: encodeBasicAuthHeader(user, password),
|
|
33
|
+
Accept: "application/json",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function airflowGet(path: string): Promise<unknown> {
|
|
38
|
+
const res = await fetch(`${apiBase()}${path}`, { headers: authHeader() });
|
|
39
|
+
const text = await res.text();
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
throw new Error(`Airflow ${String(res.status)}: ${text.slice(0, 400)}`);
|
|
42
|
+
}
|
|
43
|
+
return JSON.parse(text) as unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function dagsFrom(root: unknown): unknown[] {
|
|
47
|
+
const dags = (root as { dags?: unknown } | null)?.dags;
|
|
48
|
+
return Array.isArray(dags) ? dags : [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await runReadOnlyMcpConnector("nimbus-airflow", (reg) => {
|
|
52
|
+
reg(
|
|
53
|
+
"airflow_list",
|
|
54
|
+
"List Airflow DAGs (GET /api/v1/dags?limit=100&offset=<n>). The Airflow stable REST API v1 returns the envelope { dags: [...], total_entries: <int> } — `dags` holds the DAG objects, `total_entries` the total across all pages. The optional offset arg (default 0) selects the page start; limit is fixed at 100.",
|
|
55
|
+
z.object({
|
|
56
|
+
offset: z.number().int().min(0).optional(),
|
|
57
|
+
}),
|
|
58
|
+
async (p) => {
|
|
59
|
+
const offset = p.offset ?? 0;
|
|
60
|
+
const root = await airflowGet(
|
|
61
|
+
`/api/v1/dags?limit=100&offset=${encodeURIComponent(String(offset))}`,
|
|
62
|
+
);
|
|
63
|
+
return jsonResult({ items: dagsFrom(root) });
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
reg(
|
|
68
|
+
"airflow_get",
|
|
69
|
+
"Fetch one Airflow DAG by its dag_id (GET /api/v1/dags/{dag_id}). Returns the DAG object directly (NOT wrapped in an envelope). Throws when no DAG with that id exists.",
|
|
70
|
+
z.object({
|
|
71
|
+
dag_id: z.string().min(1),
|
|
72
|
+
}),
|
|
73
|
+
async (p) => {
|
|
74
|
+
return jsonResult(await airflowGet(`/api/v1/dags/${encodeURIComponent(p.dag_id)}`));
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
reg(
|
|
79
|
+
"airflow_search",
|
|
80
|
+
"Substring search across the first page of Airflow DAGs. Matches the query (case-insensitive) against the dag_id, description, owners, and tag names. Returns a matches envelope.",
|
|
81
|
+
searchToolInputSchema(100),
|
|
82
|
+
async (p) => {
|
|
83
|
+
const root = await airflowGet("/api/v1/dags?limit=100&offset=0");
|
|
84
|
+
const matches = filterAirflowDags(dagsFrom(root), {
|
|
85
|
+
query: p.query,
|
|
86
|
+
limit: p.limit,
|
|
87
|
+
});
|
|
88
|
+
return jsonResult({ matches });
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "com.nimbus.apple",
|
|
3
|
+
"displayName": "iCloud Mail + Calendar",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "iCloud Mail (IMAP/SMTP) + iCloud Calendar (CalDAV) connector. Indexes iCloud Mail as `apple:email` items — message HEADERS (subject, from, to/cc, date, message-id), a short capped plain-text body PREVIEW (~2 KB / 2000 chars), and attachment METADATA only (filename, size, mimetype). Indexes iCloud Calendar as `apple:event` items — summary, start/end, location, organizer, status, recurrence, attendee emails, and a ≤2000-char notes preview. It NEVER downloads attachment bytes, never fetches full message bodies, and never returns full event descriptions beyond the capped preview. Read tools: `apple_list`, `apple_get`, `apple_search` (mail) and `apple_calendar_list` (calendar). Four HITL-gated write tools: `apple_mail_send` (SMTP), `apple_mail_draft_create` (IMAP APPEND to Drafts), `apple_calendar_event_create` (CalDAV PUT), `apple_calendar_event_delete` (CalDAV DELETE). Credentials are a single Apple ID email + app-specific password authenticating IMAP (imap.mail.me.com:993 TLS), SMTP (smtp.mail.me.com:587 STARTTLS), and CalDAV (caldav.icloud.com). The sender From address is always pinned to the authenticated icloud_email (callers cannot override it). Credentials arrive as scoped environment variables injected at spawn time (APPLE_ICLOUD_EMAIL, APPLE_ICLOUD_APP_PASSWORD) — the connector never reads the vault.",
|
|
6
|
+
"author": "Nimbus",
|
|
7
|
+
"entrypoint": "dist/server.js",
|
|
8
|
+
"runtime": "bun",
|
|
9
|
+
"permissions": {
|
|
10
|
+
"network": [
|
|
11
|
+
"imap.mail.me.com:993",
|
|
12
|
+
"smtp.mail.me.com:587",
|
|
13
|
+
"caldav.icloud.com:443",
|
|
14
|
+
"p01-caldav.icloud.com:443",
|
|
15
|
+
"p02-caldav.icloud.com:443",
|
|
16
|
+
"p03-caldav.icloud.com:443",
|
|
17
|
+
"p04-caldav.icloud.com:443",
|
|
18
|
+
"p05-caldav.icloud.com:443",
|
|
19
|
+
"p06-caldav.icloud.com:443",
|
|
20
|
+
"p07-caldav.icloud.com:443",
|
|
21
|
+
"p08-caldav.icloud.com:443",
|
|
22
|
+
"p09-caldav.icloud.com:443",
|
|
23
|
+
"p10-caldav.icloud.com:443"
|
|
24
|
+
],
|
|
25
|
+
"filesystem": { "read": [], "write": [] }
|
|
26
|
+
},
|
|
27
|
+
"tools": {
|
|
28
|
+
"apple_list": { "hitl": false },
|
|
29
|
+
"apple_get": { "hitl": false },
|
|
30
|
+
"apple_search": { "hitl": false },
|
|
31
|
+
"apple_mail_send": { "hitl": true, "hitlAction": "email.send" },
|
|
32
|
+
"apple_mail_draft_create": { "hitl": true, "hitlAction": "email.draft.create" },
|
|
33
|
+
"apple_calendar_list": { "hitl": false },
|
|
34
|
+
"apple_calendar_event_create": { "hitl": true, "hitlAction": "calendar.event.create" },
|
|
35
|
+
"apple_calendar_event_delete": { "hitl": true, "hitlAction": "calendar.event.delete" }
|
|
36
|
+
},
|
|
37
|
+
"hitlRequired": ["write"],
|
|
38
|
+
"syncInterval": 300,
|
|
39
|
+
"minNimbusVersion": "0.2.0"
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail-layer re-exports + Drafts-append interface for the iCloud Mail connector.
|
|
3
|
+
*
|
|
4
|
+
* Helpers + client/mailer types come ONLY from the shared connector toolkit
|
|
5
|
+
* (shared/*) — NEVER cross-import a sibling connector's
|
|
6
|
+
* src (e.g. ../../imap/src/...): that breaks per-workspace tsc/bundling output.
|
|
7
|
+
*
|
|
8
|
+
* The shared kit already defines the structural client/mailer/message contracts
|
|
9
|
+
* the connector must satisfy (EmailReadClient/EmailSendMailer/EmailMessageMeta).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
capPreview,
|
|
14
|
+
clampLimit,
|
|
15
|
+
formatAddress,
|
|
16
|
+
PREVIEW_FETCH_BYTES,
|
|
17
|
+
PREVIEW_MAX_CHARS,
|
|
18
|
+
} from "../../../shared/imap-mail-core.ts";
|
|
19
|
+
|
|
20
|
+
// The shared kit already defines the structural client/mailer/message contracts
|
|
21
|
+
// the connector must satisfy (EmailReadClient/EmailSendMailer/EmailMessageMeta).
|
|
22
|
+
export type {
|
|
23
|
+
EmailMessageMeta,
|
|
24
|
+
EmailReadClient,
|
|
25
|
+
EmailSendMailer,
|
|
26
|
+
} from "../../../shared/imap-tool-kit.ts";
|
|
27
|
+
|
|
28
|
+
export interface DraftInput {
|
|
29
|
+
readonly to: string;
|
|
30
|
+
readonly subject: string;
|
|
31
|
+
readonly body: string;
|
|
32
|
+
readonly cc?: string;
|
|
33
|
+
readonly bcc?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DraftResult {
|
|
37
|
+
readonly uid: number | null;
|
|
38
|
+
readonly mailbox: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DraftAppender {
|
|
42
|
+
appendDraft(input: DraftInput): Promise<DraftResult>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CalDAV client interface + pure calendar selection/normalization helpers
|
|
3
|
+
* for the iCloud Calendar connector.
|
|
4
|
+
*
|
|
5
|
+
* Design: the CalDavClient interface is the injectable seam — the real (network)
|
|
6
|
+
* implementation lives in server.ts (coverage-excluded). Pure helpers are
|
|
7
|
+
* separately tested here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ParsedEvent } from "@nimbus-dev/sdk";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// CalendarRef — a discovered calendar
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface CalendarRef {
|
|
17
|
+
readonly url: string;
|
|
18
|
+
readonly displayName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// EventWindow — a UTC time range for fetching events
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export interface EventWindow {
|
|
26
|
+
readonly startUtc: string;
|
|
27
|
+
readonly endUtc: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// CalDavClient — injectable transport interface
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export interface CalDavClient {
|
|
35
|
+
listCalendars(): Promise<CalendarRef[]>;
|
|
36
|
+
listEvents(
|
|
37
|
+
cal: CalendarRef,
|
|
38
|
+
window: EventWindow,
|
|
39
|
+
): Promise<{ href: string; event: ParsedEvent }[]>;
|
|
40
|
+
putEvent(cal: CalendarRef, uid: string, ics: string): Promise<{ href: string }>;
|
|
41
|
+
deleteEvent(href: string): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// selectCalendars — pure calendar filter helper
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Filter the full list of calendars by include/exclude display-name lists.
|
|
50
|
+
*
|
|
51
|
+
* Rules (in priority order):
|
|
52
|
+
* 1. If `include` is non-empty, return only calendars whose `displayName`
|
|
53
|
+
* appears in the include list (exact match).
|
|
54
|
+
* 2. Else if `exclude` is non-empty, return all calendars whose `displayName`
|
|
55
|
+
* does NOT appear in the exclude list.
|
|
56
|
+
* 3. Both empty (or both absent) → return all calendars.
|
|
57
|
+
*/
|
|
58
|
+
export function selectCalendars(
|
|
59
|
+
all: CalendarRef[],
|
|
60
|
+
cfg: { include?: readonly string[] | undefined; exclude?: readonly string[] | undefined },
|
|
61
|
+
): CalendarRef[] {
|
|
62
|
+
const include = cfg.include ?? [];
|
|
63
|
+
const exclude = cfg.exclude ?? [];
|
|
64
|
+
|
|
65
|
+
if (include.length > 0) {
|
|
66
|
+
const includeSet = new Set(include);
|
|
67
|
+
return all.filter((c) => includeSet.has(c.displayName));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (exclude.length > 0) {
|
|
71
|
+
const excludeSet = new Set(exclude);
|
|
72
|
+
return all.filter((c) => !excludeSet.has(c.displayName));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return all;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// clampInstances — cap the number of events returned per calendar
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Return the first `max` elements from `rows`. When `rows.length <= max`,
|
|
84
|
+
* returns the original array reference unchanged (zero allocation).
|
|
85
|
+
*/
|
|
86
|
+
export function clampInstances<T>(rows: readonly T[], max: number): T[] {
|
|
87
|
+
// A non-positive cap (e.g. when an earlier calendar already filled the budget)
|
|
88
|
+
// means "take nothing" — `slice(0, -n)` would otherwise drop from the end.
|
|
89
|
+
if (max <= 0) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
if (rows.length <= max) {
|
|
93
|
+
return rows as T[];
|
|
94
|
+
}
|
|
95
|
+
return rows.slice(0, max);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// caldavObjectFilename — derive a URL-path-safe .ics filename from a UID
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build a CalDAV object filename (`<safe-uid>.ics`) from an event UID.
|
|
104
|
+
*
|
|
105
|
+
* The UID is embedded verbatim into the CalDAV object URL path segment, so any
|
|
106
|
+
* character that is unsafe in a path segment — `/`, `\`, `?`, `#`, `%`, `:`,
|
|
107
|
+
* whitespace, control chars — must be stripped to avoid a malformed/ambiguous
|
|
108
|
+
* URL (e.g. a summary-derived UID containing `/` would split the path) or path
|
|
109
|
+
* traversal (`../`). Allowed: URL-unreserved chars plus `@` (RFC 3986 §2.3 +
|
|
110
|
+
* the common `uid@host` form). A UID that sanitizes to empty falls back to
|
|
111
|
+
* `event` so a usable filename is always produced.
|
|
112
|
+
*/
|
|
113
|
+
export function caldavObjectFilename(uid: string): string {
|
|
114
|
+
const safe = uid.replace(/[^A-Za-z0-9\-_.~@]/g, "");
|
|
115
|
+
return `${safe === "" ? "event" : safe}.ics`;
|
|
116
|
+
}
|