@outcrawl/sdk 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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The connector registry: what a workspace has connected, and what it cannot.
3
+ *
4
+ * Outcrawl is an MCP **server** inbound — `packages/mcp` — and this package is
5
+ * the outbound half. A workspace connects Gmail, Slack or Twilio and a run
6
+ * calls those tools mid-task. The two directions share nothing but the
7
+ * protocol; the inbound server is untouched by any of this.
8
+ *
9
+ * This module holds the row and the rules that decide whether a row may exist
10
+ * at all. The rules are the security surface of the whole feature, so they are
11
+ * here, in one place, with their reasoning, rather than distributed across a
12
+ * handler and a client.
13
+ */
14
+ /**
15
+ * How we reach a connected MCP server. **HTTPS, and there is no second arm.**
16
+ *
17
+ * ## Why stdio is refused, and why the refusal is a union arm and not an omission
18
+ *
19
+ * The MCP ecosystem's default transport is stdio: a client spawns
20
+ * `npx -y @modelcontextprotocol/server-gmail` and speaks JSON-RPC over its
21
+ * pipes. Supporting that here would mean `POST /v1/integrations` accepts a
22
+ * command line and a fleet Mac executes it.
23
+ *
24
+ * A fleet Mac is multi-tenant. It holds `TICKET_SECRET`, profile blobs, and
25
+ * other accounts' live browser contexts, and there is no sandbox between a
26
+ * spawned child process and any of that. A `stdio` field on this type is
27
+ * therefore a JSON-schema'd remote code execution endpoint running as the
28
+ * worker user, reachable by anyone with an API key.
29
+ *
30
+ * An allowlist does not fix it. The canonical invocation is `npx -y <package>`,
31
+ * which is `npx` fetching arbitrary code off the network at call time — **an
32
+ * allowlist of names, not of code.** Pinning versions moves the trust to a
33
+ * registry we do not control; vendoring the servers means shipping a Gmail
34
+ * client in our worker bundle and calling it an integration.
35
+ *
36
+ * And it buys nothing we need. Gmail, Slack and Twilio — the three the product
37
+ * was scoped around — all publish hosted HTTP MCP endpoints. Every use case
38
+ * the feature exists for is served without ever spawning a process.
39
+ *
40
+ * So this is a discriminated union with exactly one arm. That is the same
41
+ * discipline `mcpTool: string | null` uses in the capability registry: a
42
+ * declined option must not be spellable the same way as a forgotten one.
43
+ * Adding a `stdio` arm later — when there is a per-run sandbox to run it in —
44
+ * is a compile error at every `switch` in this package until somebody has
45
+ * handled it deliberately.
46
+ */
47
+ export type ConnectorTransport = {
48
+ readonly kind: 'http';
49
+ /** Absolute `https://` endpoint speaking MCP over HTTP. */
50
+ readonly url: string;
51
+ };
52
+ export declare const CONNECTOR_TRANSPORT_KINDS: readonly ["http"];
53
+ /**
54
+ * How we authenticate to the remote server — **by handle, never by value.**
55
+ *
56
+ * `secret` is a handle into the workspace secrets store (`@outcrawl/core`'s
57
+ * `secrets.*` capabilities, bare string, `^[a-z0-9][a-z0-9_-]{0,63}$`). It is
58
+ * what the row stores, what `integrations.list` returns and what appears in
59
+ * any log. The plaintext is resolved at the instant of a call, by the tier
60
+ * that makes the call, and nothing here ever holds one.
61
+ *
62
+ * There is no second credential store. A connector that wanted its own would
63
+ * be a second place a customer's Gmail token lives, with its own encryption
64
+ * decision, its own rotation story and its own bugs.
65
+ */
66
+ export type ConnectorAuth = {
67
+ readonly kind: 'none';
68
+ } | {
69
+ readonly kind: 'bearer';
70
+ readonly secret: string;
71
+ } | {
72
+ readonly kind: 'header';
73
+ readonly header: string;
74
+ readonly secret: string;
75
+ };
76
+ export declare const CONNECTOR_AUTH_KINDS: readonly ["none", "bearer", "header"];
77
+ /**
78
+ * THE ROW IS THE RESPONSE. `integrations.list` answers these verbatim and
79
+ * there is no projection in front of it, unlike `secrets.list`, which has to
80
+ * withhold a ciphertext.
81
+ *
82
+ * That is not a shortcut, it is the property: a connector row holds a secrets
83
+ * store HANDLE and never a value, so there is nothing on it to withhold. A
84
+ * `ConnectorView` that stripped fields would imply the row has some — and the
85
+ * day somebody adds a field worth hiding, they will add it to a type nobody
86
+ * reads twice. Making the row publishable in full is what keeps it honest.
87
+ */
88
+ export interface Connector {
89
+ readonly id: string;
90
+ /**
91
+ * The name the model addresses this connector by, and the left half of every
92
+ * `<connector>.<tool>` the prompt shows. Lowercase, stable, unique per
93
+ * account: `gmail`, `slack`, `twilio`.
94
+ */
95
+ readonly name: string;
96
+ readonly transport: ConnectorTransport;
97
+ readonly auth: ConnectorAuth;
98
+ /**
99
+ * The tools this connector may expose, or `null` for every tool the remote
100
+ * advertises.
101
+ *
102
+ * Required-and-nullable rather than optional, on the registry's own rule: "I
103
+ * reviewed the catalogue and all of it is fine" and "nobody said" are
104
+ * different facts and must not be the same bytes. This is the field a
105
+ * customer reads to answer *what can an agent reach outside the browser*,
106
+ * and an undefined that renders as "everything" is the one answer nobody
107
+ * consented to.
108
+ */
109
+ readonly tools: readonly string[] | null;
110
+ readonly createdAt: string;
111
+ }
112
+ /** A connector on its way in. No id and no timestamp: both are the store's. */
113
+ export interface ConnectorInput {
114
+ readonly name: string;
115
+ readonly transport: ConnectorTransport;
116
+ readonly auth: ConnectorAuth;
117
+ readonly tools: readonly string[] | null;
118
+ }
119
+ /**
120
+ * Whether a hostname is an IP literal in a range that is not on the public
121
+ * internet.
122
+ *
123
+ * ## What this closes, and — as importantly — what it does not
124
+ *
125
+ * It closes the direct attack: `POST /v1/integrations` with
126
+ * `https://169.254.169.254/latest/meta-data/` or `https://10.0.0.5/`, which
127
+ * would otherwise make our own egress into a reader for whatever is on the
128
+ * other side. That is worth closing on its own and it is cheap.
129
+ *
130
+ * It does NOT close DNS rebinding — a name that resolves publicly when this
131
+ * runs and privately when the call is made. Closing that needs the resolved
132
+ * address at connect time and at call time, and the tier that makes the call
133
+ * is a Cloudflare Worker, whose runtime exposes no resolver and no socket. So
134
+ * this is stated as a partial defence rather than asserted as a complete one.
135
+ *
136
+ * The residual risk is bounded by WHERE the call runs, which is the second
137
+ * reason the tool call is proxied through the edge rather than made from a
138
+ * fleet Mac: a Mac sits on a private network with the rest of the fleet on it,
139
+ * and the Worker does not.
140
+ */
141
+ export declare function isPrivateAddressLiteral(host: string): boolean;
142
+ /**
143
+ * The one rule for "an endpoint this product will make an authenticated
144
+ * outbound request to". Throws `BadRequestError` naming the field.
145
+ */
146
+ export declare function connectorUrl(value: unknown): string;
147
+ /**
148
+ * Read a connector off a request body. Throws `BadRequestError` naming the
149
+ * field, which is what the API surfaces as a 400.
150
+ */
151
+ export declare function parseConnectorInput(body: Readonly<Record<string, unknown>>): ConnectorInput;
152
+ /** Every field `integrations.connect` accepts. */
153
+ export declare const CONNECTOR_FIELDS: readonly ["name", "transport", "auth", "tools"];