@nimbus-dev/connectors 0.2.1 → 0.2.2
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 +14 -4
- 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 +15 -34
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +197 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +133 -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 +14 -27
- package/connectors/cloudwatch/src/tools.ts +15 -34
- 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 +152 -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 +159 -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 +19 -38
- 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 +13 -6
- package/shared/cli-json-kit.ts +150 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- 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/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/standalone/src/launcher.ts +19 -7
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
|
+
}
|
package/shared/run-cli-json.ts
CHANGED
|
@@ -2,23 +2,36 @@ import { nimbusSpawn } from "./nimbus-spawn.ts";
|
|
|
2
2
|
|
|
3
3
|
export type RunCliJsonResult = { ok: true; data: unknown } | { ok: false; message: string };
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Spawn `command`. An empty command, or a non-zero exit, becomes the one failure message every
|
|
7
|
+
* caller reports: `<binary> exited <code>: <first 500 chars of stderr>`.
|
|
8
|
+
*/
|
|
9
|
+
async function spawnChecked(
|
|
6
10
|
command: readonly string[],
|
|
7
11
|
env: Record<string, string | undefined>,
|
|
8
|
-
): Promise<{ ok: true } | { ok: false; message: string }> {
|
|
12
|
+
): Promise<{ ok: true; stdout: string } | { ok: false; message: string }> {
|
|
9
13
|
if (command.length === 0) {
|
|
10
14
|
return { ok: false, message: "empty command" };
|
|
11
15
|
}
|
|
12
|
-
const { code, stderr
|
|
16
|
+
const { code, stdout, stderr } = await nimbusSpawn(command, env);
|
|
13
17
|
if (code !== 0) {
|
|
14
18
|
return {
|
|
15
19
|
ok: false,
|
|
16
|
-
message: `${command[0] ?? "cli"} exited ${String(code)}: ${
|
|
20
|
+
message: `${command[0] ?? "cli"} exited ${String(code)}: ${stderr.slice(0, 500)}`,
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
|
-
return { ok: true };
|
|
23
|
+
return { ok: true, stdout };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runCliOk(
|
|
27
|
+
command: readonly string[],
|
|
28
|
+
env: Record<string, string | undefined>,
|
|
29
|
+
): Promise<{ ok: true } | { ok: false; message: string }> {
|
|
30
|
+
const r = await spawnChecked(command, env);
|
|
31
|
+
return r.ok ? { ok: true } : r;
|
|
20
32
|
}
|
|
21
33
|
|
|
34
|
+
/** {@link runCliOk}, throwing its failure message instead of returning it. */
|
|
22
35
|
export async function runCliOkThrowing(
|
|
23
36
|
command: readonly string[],
|
|
24
37
|
env: Record<string, string | undefined>,
|
|
@@ -33,23 +46,32 @@ export async function runCliJson(
|
|
|
33
46
|
command: readonly string[],
|
|
34
47
|
env: Record<string, string | undefined>,
|
|
35
48
|
): Promise<RunCliJsonResult> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const { code, stdout: out, stderr: err } = await nimbusSpawn(command, env);
|
|
40
|
-
if (code !== 0) {
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
message: `${command[0] ?? "cli"} exited ${String(code)}: ${err.slice(0, 500)}`,
|
|
44
|
-
};
|
|
49
|
+
const r = await spawnChecked(command, env);
|
|
50
|
+
if (!r.ok) {
|
|
51
|
+
return r;
|
|
45
52
|
}
|
|
46
|
-
const trimmed =
|
|
53
|
+
const trimmed = r.stdout.trim();
|
|
47
54
|
if (trimmed === "") {
|
|
48
55
|
return { ok: true, data: null };
|
|
49
56
|
}
|
|
50
57
|
try {
|
|
51
58
|
return { ok: true, data: JSON.parse(trimmed) as unknown };
|
|
52
59
|
} catch {
|
|
53
|
-
return { ok: false, message: `invalid JSON from CLI: ${
|
|
60
|
+
return { ok: false, message: `invalid JSON from CLI: ${r.stdout.slice(0, 200)}` };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* {@link runCliJson}, throwing its failure message instead of returning it. Resolves to the parsed
|
|
66
|
+
* JSON, or `null` when the command printed nothing.
|
|
67
|
+
*/
|
|
68
|
+
export async function runCliJsonThrowing(
|
|
69
|
+
command: readonly string[],
|
|
70
|
+
env: Record<string, string | undefined>,
|
|
71
|
+
): Promise<unknown> {
|
|
72
|
+
const r = await runCliJson(command, env);
|
|
73
|
+
if (!r.ok) {
|
|
74
|
+
throw new Error(r.message);
|
|
54
75
|
}
|
|
76
|
+
return r.data;
|
|
55
77
|
}
|
package/shared/search-filter.ts
CHANGED
|
@@ -43,21 +43,35 @@ export function tagText(row: Record<string, unknown>): string {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* The non-empty string entries of the array at `row[key]`, space-joined; `""` when it is absent or
|
|
47
|
+
* not an array (Airflow `owners`, Prefect `tags`). Unlike {@link tagText}, an empty-string entry is
|
|
48
|
+
* skipped rather than joined — which shows in the haystack's spacing, so it is a separate rule.
|
|
48
49
|
*/
|
|
49
|
-
export function
|
|
50
|
-
const
|
|
51
|
-
if (!Array.isArray(
|
|
50
|
+
export function nonEmptyStringsText(row: Record<string, unknown>, key: string): string {
|
|
51
|
+
const list = row[key];
|
|
52
|
+
if (!Array.isArray(list)) {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
return list.filter((v): v is string => typeof v === "string" && v !== "").join(" ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The non-empty string `name` of each objectish entry of the array at `row[key]`, space-joined
|
|
60
|
+
* (Wiz `projects`). Returns "" when `row[key]` is absent, not an array, or holds no object entry
|
|
61
|
+
* with a non-empty string `name`.
|
|
62
|
+
*/
|
|
63
|
+
export function objectNamesText(row: Record<string, unknown>, key: string): string {
|
|
64
|
+
const list = row[key];
|
|
65
|
+
if (!Array.isArray(list)) {
|
|
52
66
|
return "";
|
|
53
67
|
}
|
|
54
68
|
const names: string[] = [];
|
|
55
|
-
for (const
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
69
|
+
for (const entry of list) {
|
|
70
|
+
const obj = asObjectish(entry);
|
|
71
|
+
if (obj === undefined) {
|
|
58
72
|
continue;
|
|
59
73
|
}
|
|
60
|
-
const name =
|
|
74
|
+
const name = obj["name"];
|
|
61
75
|
if (typeof name === "string" && name !== "") {
|
|
62
76
|
names.push(name);
|
|
63
77
|
}
|
|
@@ -65,6 +79,14 @@ export function tagNamesFromObjects(row: Record<string, unknown>): string {
|
|
|
65
79
|
return names.join(" ");
|
|
66
80
|
}
|
|
67
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Extract tag names from an array of `{name: string}` tag objects (e.g. Airflow, DependencyTrack).
|
|
84
|
+
* Returns "" when `tags` is absent, not an array, or contains no object entries with a string `name`.
|
|
85
|
+
*/
|
|
86
|
+
export function tagNamesFromObjects(row: Record<string, unknown>): string {
|
|
87
|
+
return objectNamesText(row, "tags");
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
export function filterByQuery<T>(items: readonly T[], options: FilterByQueryOptions<T>): T[] {
|
|
69
91
|
const needle = options.query.toLowerCase();
|
|
70
92
|
const cap = options.limit ?? 50;
|
|
@@ -88,28 +110,46 @@ export function filterByQuery<T>(items: readonly T[], options: FilterByQueryOpti
|
|
|
88
110
|
|
|
89
111
|
export type FieldExtractor = (item: unknown) => readonly (string | null | undefined)[] | null;
|
|
90
112
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* boilerplate `fieldsOf` body shared by the simpler connectors.
|
|
95
|
-
*/
|
|
96
|
-
export function fieldsFromKeys(
|
|
113
|
+
/** The one body behind {@link fieldsFromKeys} and {@link recordFieldsFromKeys}. */
|
|
114
|
+
function keyedFields(
|
|
115
|
+
toRow: (item: unknown) => Record<string, unknown> | undefined,
|
|
97
116
|
keys: readonly string[],
|
|
98
|
-
|
|
117
|
+
tags: boolean,
|
|
99
118
|
): FieldExtractor {
|
|
100
119
|
return (item: unknown) => {
|
|
101
|
-
const row =
|
|
120
|
+
const row = toRow(item);
|
|
102
121
|
if (row === undefined) {
|
|
103
122
|
return null;
|
|
104
123
|
}
|
|
105
124
|
const parts = keys.map((key) => stringField(row, key));
|
|
106
|
-
if (
|
|
125
|
+
if (tags) {
|
|
107
126
|
parts.push(tagText(row));
|
|
108
127
|
}
|
|
109
128
|
return parts;
|
|
110
129
|
};
|
|
111
130
|
}
|
|
112
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Build a {@link FieldExtractor} that reads a fixed list of string keys off each
|
|
134
|
+
* objectish row, optionally appending the standard `tags` text. Collapses the
|
|
135
|
+
* boilerplate `fieldsOf` body shared by the simpler connectors.
|
|
136
|
+
*/
|
|
137
|
+
export function fieldsFromKeys(
|
|
138
|
+
keys: readonly string[],
|
|
139
|
+
opts?: { readonly tags?: boolean },
|
|
140
|
+
): FieldExtractor {
|
|
141
|
+
return keyedFields(asObjectish, keys, opts?.tags === true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* {@link fieldsFromKeys} for rows that must be plain objects: an ARRAY row is skipped
|
|
146
|
+
* ({@link asRecord}) instead of being read as an object that has none of the keys — which a
|
|
147
|
+
* query made only of spaces would still match. The BI connectors' filters use this rule.
|
|
148
|
+
*/
|
|
149
|
+
export function recordFieldsFromKeys(keys: readonly string[]): FieldExtractor {
|
|
150
|
+
return keyedFields(asRecord, keys, false);
|
|
151
|
+
}
|
|
152
|
+
|
|
113
153
|
/**
|
|
114
154
|
* Read a nested string field by key path off an objectish row, returning `""`
|
|
115
155
|
* when any path segment is missing or the leaf is not a string. Shared by the
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* walk-files — the bounded recursive directory walk the local-filesystem
|
|
3
|
+
* connectors share, and the path-traversal guard that goes with it.
|
|
4
|
+
*
|
|
5
|
+
* `great-expectations`, `localdb` and `dataprofile` each scan a configured
|
|
6
|
+
* directory tree for the files they care about (`*.json`, `*.sql`, and a set of
|
|
7
|
+
* tabular extensions), and each had written out the same walk: recurse, stop at
|
|
8
|
+
* a depth limit, stop at a file-count limit, swallow an unreadable directory
|
|
9
|
+
* rather than aborting the scan.
|
|
10
|
+
*
|
|
11
|
+
* Those two limits are why this is worth sharing rather than tolerating. They
|
|
12
|
+
* are the guard against a connector being pointed — by configuration or by a
|
|
13
|
+
* symlink — at a tree deep or wide enough to exhaust the process, and a guard
|
|
14
|
+
* maintained in three places is a guard that can be tightened in one and left
|
|
15
|
+
* alone in the other two. They were already inconsistent: 1000 files for
|
|
16
|
+
* great-expectations against 2000 for the others. That difference is real (a
|
|
17
|
+
* GX run directory is small) so it stays a per-caller argument rather than
|
|
18
|
+
* being averaged away — but it is now visibly an argument rather than three
|
|
19
|
+
* independent constants that happen to differ.
|
|
20
|
+
*
|
|
21
|
+
* The same three connectors each carried a copy of {@link assertWithinDir} too,
|
|
22
|
+
* identical but for the directory named in the error — a second security
|
|
23
|
+
* control in three hand-written copies, shared for the same reason.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { Dirent } from "node:fs";
|
|
27
|
+
import { readdir } from "node:fs/promises";
|
|
28
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
29
|
+
|
|
30
|
+
export interface WalkFilesOptions<T> {
|
|
31
|
+
/** Hard cap on results. The walk stops as soon as it is reached. */
|
|
32
|
+
readonly maxFiles: number;
|
|
33
|
+
/** Hard cap on recursion depth, counted from `root` at 0. */
|
|
34
|
+
readonly maxDepth: number;
|
|
35
|
+
/**
|
|
36
|
+
* What to keep for one file entry, or `undefined` to skip it. Receives the
|
|
37
|
+
* directory entry and its full path, so a caller can select on the name, the
|
|
38
|
+
* extension, or anything else it can decide without reading the file.
|
|
39
|
+
*/
|
|
40
|
+
readonly select: (entry: Dirent, fullPath: string) => T | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Collect what `select` keeps, walking `root` depth-first within the limits.
|
|
45
|
+
*
|
|
46
|
+
* An unreadable directory is SKIPPED rather than fatal: a scan of a
|
|
47
|
+
* user-configured tree meets permission-denied subdirectories routinely, and
|
|
48
|
+
* failing the whole tool call for one of them would make the connector useless
|
|
49
|
+
* on a real machine. A caller that needs to know a directory was unreadable
|
|
50
|
+
* should stat it itself.
|
|
51
|
+
*
|
|
52
|
+
* Symlinks are not followed — `readdir` reports a link as neither a file nor a
|
|
53
|
+
* directory here, so a cycle cannot make the walk diverge.
|
|
54
|
+
*/
|
|
55
|
+
export async function walkFiles<T>(root: string, options: WalkFilesOptions<T>): Promise<T[]> {
|
|
56
|
+
const found: T[] = [];
|
|
57
|
+
|
|
58
|
+
async function walk(dir: string, depth: number): Promise<void> {
|
|
59
|
+
if (depth > options.maxDepth || found.length >= options.maxFiles) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let entries: Dirent[];
|
|
63
|
+
try {
|
|
64
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (found.length >= options.maxFiles) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const full = join(dir, entry.name);
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
await walk(full, depth + 1); // NOSONAR S9382: depth-first under one shared maxFiles cap — a subtree must finish before the next starts or the cap stops at a nondeterministic point, and concurrent readdirs over a wide tree are the exhaustion this walk exists to bound.
|
|
75
|
+
} else if (entry.isFile()) {
|
|
76
|
+
const kept = options.select(entry, full);
|
|
77
|
+
if (kept !== undefined) {
|
|
78
|
+
found.push(kept);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await walk(root, 0);
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A {@link WalkFilesOptions.select} that keeps the full path of every file with
|
|
90
|
+
* one of `extensions`, compared case-insensitively.
|
|
91
|
+
*/
|
|
92
|
+
export function byExtension(
|
|
93
|
+
...extensions: readonly string[]
|
|
94
|
+
): (e: Dirent, full: string) => string | undefined {
|
|
95
|
+
const wanted = extensions.map((x) => x.toLowerCase());
|
|
96
|
+
return (entry: Dirent, full: string): string | undefined =>
|
|
97
|
+
wanted.some((x) => entry.name.toLowerCase().endsWith(x)) ? full : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The path-traversal guard: throws `"path escapes <what>"` unless `candidate` (an
|
|
102
|
+
* absolute, resolved path) is `root` itself or a descendant of it. `what` names the
|
|
103
|
+
* directory for the message, e.g. `"the configured local DB scripts dir"`.
|
|
104
|
+
*
|
|
105
|
+
* A `relative()` that starts with `..` climbed out of `root` (this also covers
|
|
106
|
+
* `..<sep>`); one that is absolute could not be expressed relative to it at all — a
|
|
107
|
+
* different drive on Windows. It fails closed: a child whose own name begins with
|
|
108
|
+
* `..` (such as `..cache`) is refused too.
|
|
109
|
+
*/
|
|
110
|
+
export function assertWithinDir(candidate: string, root: string, what: string): void {
|
|
111
|
+
const rel = relative(root, candidate);
|
|
112
|
+
if (rel === "") {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
116
|
+
throw new Error(`path escapes ${what}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -10,7 +10,10 @@ const ID_RE = /^[a-z0-9-]+$/;
|
|
|
10
10
|
*
|
|
11
11
|
* A registration CALL, or the registrar handed to a shared kit — not a bare substring, which the
|
|
12
12
|
* registrar's own `const registerWriteTool = ...` would satisfy even with nothing registered.
|
|
13
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* The ONE copy. `audit:connector-consent` (scripts/check-connector-consent.ts) imports it rather
|
|
15
|
+
* than keeping a twin, so the launcher's runtime verdict and the audit's static one cannot drift.
|
|
16
|
+
* They had: the audit's twin learned to tolerate a carriage return (below) and this copy never did.
|
|
14
17
|
*
|
|
15
18
|
* Deliberately NOT a regular expression. The previous pattern was
|
|
16
19
|
* `^\s*register[A-Za-z]*WriteTool\(` under `/m`, and it drew two rounds of ReDoS reports. The
|
|
@@ -25,9 +28,15 @@ const ID_RE = /^[a-z0-9-]+$/;
|
|
|
25
28
|
* bounded work, no star sits next to an overlapping literal, and the accepted language is
|
|
26
29
|
* unchanged — including the trailing-comma form, which still requires the comma to end the line.
|
|
27
30
|
*/
|
|
28
|
-
function registersWriteTool(src: string): boolean {
|
|
31
|
+
export function registersWriteTool(src: string): boolean {
|
|
29
32
|
for (const line of src.split("\n")) {
|
|
30
|
-
|
|
33
|
+
// trim(), not trimStart(): the equality below is exact, and a CRLF checkout leaves a trailing
|
|
34
|
+
// carriage return that breaks it. Observed in the consent audit on the first run of this repo
|
|
35
|
+
// on its own, before .gitattributes existed: it reported imap and protonmail as declaring
|
|
36
|
+
// ungated writes while both register through the kit, on a line reading `registerWriteTool,`
|
|
37
|
+
// plus a CR. That failed SAFE (a false finding there, a false refusal here — never a false
|
|
38
|
+
// green), but a verdict that depends on the checkout's line endings is waiting to be wrong.
|
|
39
|
+
const t = line.trim();
|
|
31
40
|
if (t === "registerWriteTool,") return true;
|
|
32
41
|
if (!t.startsWith("register")) continue;
|
|
33
42
|
const at = t.indexOf("WriteTool(");
|
|
@@ -57,13 +66,13 @@ function connectorsDir(): string {
|
|
|
57
66
|
* The id is validated against a strict allow-list BEFORE being joined into a path. A separator or
|
|
58
67
|
* `..` would otherwise let the id escape the connectors directory and import an arbitrary module.
|
|
59
68
|
*/
|
|
60
|
-
export function resolveConnectorEntry(id: string): string {
|
|
69
|
+
export function resolveConnectorEntry(id: string, root: string = connectorsDir()): string {
|
|
61
70
|
if (!ID_RE.test(id)) {
|
|
62
71
|
throw new Error(
|
|
63
72
|
`invalid connector id ${JSON.stringify(id)}: expected only lowercase letters, digits and hyphens`,
|
|
64
73
|
);
|
|
65
74
|
}
|
|
66
|
-
return join(
|
|
75
|
+
return join(root, id, "src", "server.ts");
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
export type Eligibility =
|
|
@@ -154,6 +163,9 @@ export type ConnectorImporter = (entry: string) => Promise<{
|
|
|
154
163
|
export async function runStandalone(
|
|
155
164
|
argv: readonly string[],
|
|
156
165
|
importConnector: ConnectorImporter = (entry) => import(entry),
|
|
166
|
+
// Injectable for the same reason as standaloneEligibility's root: every real connector is
|
|
167
|
+
// migrated, so only a fixture can show the refusal path end to end.
|
|
168
|
+
root: string = connectorsDir(),
|
|
157
169
|
): Promise<number> {
|
|
158
170
|
const id = argv[0];
|
|
159
171
|
if (id === undefined) {
|
|
@@ -162,7 +174,7 @@ export async function runStandalone(
|
|
|
162
174
|
}
|
|
163
175
|
let entry: string;
|
|
164
176
|
try {
|
|
165
|
-
entry = resolveConnectorEntry(id);
|
|
177
|
+
entry = resolveConnectorEntry(id, root);
|
|
166
178
|
} catch (e) {
|
|
167
179
|
process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`);
|
|
168
180
|
return 2;
|
|
@@ -172,7 +184,7 @@ export async function runStandalone(
|
|
|
172
184
|
return 2;
|
|
173
185
|
}
|
|
174
186
|
|
|
175
|
-
const verdict = standaloneEligibility(id);
|
|
187
|
+
const verdict = standaloneEligibility(id, root);
|
|
176
188
|
if (!verdict.eligible) {
|
|
177
189
|
process.stderr.write(`${verdict.reason}\n`);
|
|
178
190
|
return 3;
|