@hyperdrive.bot/fleet-server 0.3.149 → 0.3.151

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.
Files changed (20) hide show
  1. package/dist/server/server/config.js +4 -0
  2. package/dist/server/server/ingestion/adapters.js +2 -0
  3. package/dist/server/server/ingestion/errors.d.ts +5 -1
  4. package/dist/server/server/ingestion/errors.js +8 -0
  5. package/dist/server/server/ingestion/health.js +10 -0
  6. package/dist/server/server/ingestion/self-hosted/gateway.d.ts +55 -0
  7. package/dist/server/server/ingestion/self-hosted/gateway.js +295 -0
  8. package/dist/server/server/ingestion/types.d.ts +14 -1
  9. package/dist/server/server/ingestion/types.js +1 -1
  10. package/dist/server/web-ui/_expo/static/js/web/{index-8747c529e5cb02149fe51570f7cb697b.js → index-29d10b030c09751bbc26003abd27f547.js} +6 -6
  11. package/dist/server/web-ui/_expo/static/js/web/index-29d10b030c09751bbc26003abd27f547.js.br +0 -0
  12. package/dist/server/web-ui/_expo/static/js/web/index-29d10b030c09751bbc26003abd27f547.js.gz +0 -0
  13. package/dist/server/web-ui/_expo/static/js/web/{index-8747c529e5cb02149fe51570f7cb697b.js.map.br → index-29d10b030c09751bbc26003abd27f547.js.map.br} +0 -0
  14. package/dist/server/web-ui/_expo/static/js/web/{index-8747c529e5cb02149fe51570f7cb697b.js.map.gz → index-29d10b030c09751bbc26003abd27f547.js.map.gz} +0 -0
  15. package/dist/server/web-ui/index.html +1 -1
  16. package/dist/server/web-ui/index.html.br +0 -0
  17. package/dist/server/web-ui/index.html.gz +0 -0
  18. package/package.json +6 -6
  19. package/dist/server/web-ui/_expo/static/js/web/index-8747c529e5cb02149fe51570f7cb697b.js.br +0 -0
  20. package/dist/server/web-ui/_expo/static/js/web/index-8747c529e5cb02149fe51570f7cb697b.js.gz +0 -0
@@ -233,6 +233,10 @@ function resolveIngestionConfig(input) {
233
233
  composio: {
234
234
  apiKey: env.PASEO_COMPOSIO_API_KEY?.trim() || undefined,
235
235
  },
236
+ selfHosted: {
237
+ url: env.PASEO_SELF_HOSTED_URL?.trim() || undefined,
238
+ token: env.PASEO_SELF_HOSTED_TOKEN?.trim() || undefined,
239
+ },
236
240
  };
237
241
  }
238
242
  function parseTrustedProxiesEnv(value) {
@@ -1,5 +1,6 @@
1
1
  import { createComposioGateway } from "./composio/gateway.js";
2
2
  import { createPipedreamGateway } from "./pipedream/gateway.js";
3
+ import { createSelfHostedGateway } from "./self-hosted/gateway.js";
3
4
  /**
4
5
  * The adapter factory for every source kind.
5
6
  *
@@ -16,6 +17,7 @@ import { createPipedreamGateway } from "./pipedream/gateway.js";
16
17
  export const SOURCE_ADAPTERS = {
17
18
  pipedream: (deps) => createPipedreamGateway(deps.config.pipedream),
18
19
  composio: (deps) => createComposioGateway(deps.config.composio),
20
+ "self-hosted": (deps) => createSelfHostedGateway(deps.config.selfHosted),
19
21
  };
20
22
  /**
21
23
  * The gateway for one kind. Total by construction: SOURCE_ADAPTERS covers every
@@ -13,6 +13,10 @@
13
13
  * COMPOSIO_OFFLINE | 502 | Composio unreachable
14
14
  * COMPOSIO_AUTH | 502 | Composio rejected our platform credential
15
15
  * COMPOSIO_ERROR | 502 | Composio answered non-ok for any other reason
16
+ * SELF_HOSTED_OFF | 422 | no bridge URL/token, or the URL is malformed
17
+ * SELF_HOSTED_OFFLINE | 502 | the user's bridge is unreachable or timed out
18
+ * SELF_HOSTED_AUTH | 502 | the bridge rejected this daemon's bearer
19
+ * SELF_HOSTED_ERROR | 502 | the bridge answered non-ok, non-JSON or incomplete
16
20
  * SOURCE_GONE | 502 | a resource we cached was deleted upstream
17
21
  * SOURCE_ERROR | 502 | any other non-ok aggregator response
18
22
  * FILTER_MISSING | 404 | no such filter on THIS daemon
@@ -37,7 +41,7 @@
37
41
  * tell the caller to re-authenticate, which cannot fix anything, and would
38
42
  * blame the wrong party for an operator's misconfiguration.
39
43
  */
40
- export declare const INGESTION_ERROR_CODES: readonly ["PIPEDREAM_OFF", "COMPOSIO_OFF", "COMPOSIO_NEEDS_SETUP", "PIPEDREAM_OFFLINE", "PIPEDREAM_AUTH", "COMPOSIO_OFFLINE", "COMPOSIO_AUTH", "COMPOSIO_ERROR", "SOURCE_GONE", "SOURCE_ERROR", "FILTER_MISSING", "SOURCE_MISSING"];
44
+ export declare const INGESTION_ERROR_CODES: readonly ["PIPEDREAM_OFF", "COMPOSIO_OFF", "COMPOSIO_NEEDS_SETUP", "PIPEDREAM_OFFLINE", "PIPEDREAM_AUTH", "COMPOSIO_OFFLINE", "COMPOSIO_AUTH", "COMPOSIO_ERROR", "SELF_HOSTED_OFF", "SELF_HOSTED_OFFLINE", "SELF_HOSTED_AUTH", "SELF_HOSTED_ERROR", "SOURCE_GONE", "SOURCE_ERROR", "FILTER_MISSING", "SOURCE_MISSING"];
41
45
  export type IngestionErrorCode = (typeof INGESTION_ERROR_CODES)[number];
42
46
  /** A domain error from the ingestion seam, with a message ready for a client. */
43
47
  export declare class IngestionError extends Error {
@@ -13,6 +13,10 @@
13
13
  * COMPOSIO_OFFLINE | 502 | Composio unreachable
14
14
  * COMPOSIO_AUTH | 502 | Composio rejected our platform credential
15
15
  * COMPOSIO_ERROR | 502 | Composio answered non-ok for any other reason
16
+ * SELF_HOSTED_OFF | 422 | no bridge URL/token, or the URL is malformed
17
+ * SELF_HOSTED_OFFLINE | 502 | the user's bridge is unreachable or timed out
18
+ * SELF_HOSTED_AUTH | 502 | the bridge rejected this daemon's bearer
19
+ * SELF_HOSTED_ERROR | 502 | the bridge answered non-ok, non-JSON or incomplete
16
20
  * SOURCE_GONE | 502 | a resource we cached was deleted upstream
17
21
  * SOURCE_ERROR | 502 | any other non-ok aggregator response
18
22
  * FILTER_MISSING | 404 | no such filter on THIS daemon
@@ -46,6 +50,10 @@ export const INGESTION_ERROR_CODES = [
46
50
  "COMPOSIO_OFFLINE",
47
51
  "COMPOSIO_AUTH",
48
52
  "COMPOSIO_ERROR",
53
+ "SELF_HOSTED_OFF",
54
+ "SELF_HOSTED_OFFLINE",
55
+ "SELF_HOSTED_AUTH",
56
+ "SELF_HOSTED_ERROR",
49
57
  "SOURCE_GONE",
50
58
  "SOURCE_ERROR",
51
59
  "FILTER_MISSING",
@@ -22,6 +22,16 @@ const ERROR_CODE_STATUS = {
22
22
  COMPOSIO_OFFLINE: "degraded",
23
23
  COMPOSIO_AUTH: "revoked",
24
24
  COMPOSIO_ERROR: "degraded",
25
+ // Same split as the aggregators, for the same reason. OFF/OFFLINE/ERROR are
26
+ // states the next sweep may clear on its own; AUTH is not. For a vendor,
27
+ // AUTH means the user must re-authorize there; for a self-hosted bridge it
28
+ // means the bearer in this daemon's env no longer matches the one the bridge
29
+ // expects. Different cause, identical consequence: a human has to act, so
30
+ // reporting it as `degraded` would leave a dead source looking merely slow.
31
+ SELF_HOSTED_OFF: "degraded",
32
+ SELF_HOSTED_OFFLINE: "degraded",
33
+ SELF_HOSTED_AUTH: "revoked",
34
+ SELF_HOSTED_ERROR: "degraded",
25
35
  SOURCE_GONE: "revoked",
26
36
  SOURCE_ERROR: "degraded",
27
37
  // The 404 pair cannot arise from a health check: `checkSource` is only ever
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The self-hosted source gateway.
3
+ *
4
+ * Composio and Pipedream are aggregators someone else runs: paseo holds a
5
+ * platform credential and talks to a fixed base URL baked into the adapter.
6
+ * This adapter inverts that. The user runs the aggregator, tells paseo where it
7
+ * is, and paseo holds nothing but a bearer token for that one endpoint.
8
+ *
9
+ * That inversion is the whole point. Nothing in this file names an app, a
10
+ * vendor or a protocol beyond HTTP+JSON: the bridge on the other end decides
11
+ * what it fronts. WhatsApp-over-wacli is the first one, but a Matrix bridge, an
12
+ * IMAP box or a signal-cli would implement the same six routes and paseo would
13
+ * not learn a new word.
14
+ *
15
+ * WHAT THE BRIDGE OWES US (v1):
16
+ *
17
+ * GET {base}/v1/sources?query=&cursor= -> SourcesResponse
18
+ * POST {base}/v1/connect -> ConnectResponse
19
+ * GET {base}/v1/accounts?... -> AccountsResponse
20
+ * GET {base}/v1/items?... -> ItemsResponse
21
+ * POST {base}/v1/mcp-target -> McpTargetResponse
22
+ *
23
+ * `listTriggerComponents` / `deployTrigger` are deliberately NOT implemented.
24
+ * They are optional on `SourceGateway`, and `service.ts` already answers
25
+ * `${kind} cannot deploy triggers` for a gateway that lacks them. A bridge that
26
+ * fronts a local daemon has nothing to deploy: it is already watching, or it is
27
+ * not running at all. Pretending otherwise would report a source as "watching"
28
+ * when nothing is.
29
+ *
30
+ * LEDGER IDENTITY IS OURS, NOT THE BRIDGE'S.
31
+ *
32
+ * The bridge returns `externalId` and a raw `payload`; `key` and `contentHash`
33
+ * are computed HERE with `fingerprint()`, byte-identically to the Composio
34
+ * adapter. A bridge that minted its own keys would be a second identity
35
+ * function for the same items, and an identity that disagrees with the ledger
36
+ * re-dispatches a backlog the user already cleared.
37
+ *
38
+ * Note what `key` deliberately does NOT include: `accountId`. Two accounts on
39
+ * the same underlying inbox (the read-only and read-write wacli pair, both
40
+ * linked to one WhatsApp number) see the SAME messages. Keying without the
41
+ * account makes that one ledger row handled once, which is the correct
42
+ * behaviour, and it also means re-pairing an account does not invalidate a
43
+ * backlog the user already worked through.
44
+ *
45
+ * CREDENTIALS.
46
+ *
47
+ * The bearer token never leaves this module, and `mcpTarget` results are per
48
+ * turn: they carry live headers minted by the bridge and are never persisted,
49
+ * cached or returned to a client. A `SourceItem` has no credential field and
50
+ * must never grow one - it is projected into `BackfillItem` and crosses the
51
+ * wire to the app.
52
+ */
53
+ import type { SelfHostedConfig, SourceGateway } from "../types.js";
54
+ export declare function createSelfHostedGateway(config?: SelfHostedConfig | undefined): SourceGateway;
55
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1,295 @@
1
+ /**
2
+ * The self-hosted source gateway.
3
+ *
4
+ * Composio and Pipedream are aggregators someone else runs: paseo holds a
5
+ * platform credential and talks to a fixed base URL baked into the adapter.
6
+ * This adapter inverts that. The user runs the aggregator, tells paseo where it
7
+ * is, and paseo holds nothing but a bearer token for that one endpoint.
8
+ *
9
+ * That inversion is the whole point. Nothing in this file names an app, a
10
+ * vendor or a protocol beyond HTTP+JSON: the bridge on the other end decides
11
+ * what it fronts. WhatsApp-over-wacli is the first one, but a Matrix bridge, an
12
+ * IMAP box or a signal-cli would implement the same six routes and paseo would
13
+ * not learn a new word.
14
+ *
15
+ * WHAT THE BRIDGE OWES US (v1):
16
+ *
17
+ * GET {base}/v1/sources?query=&cursor= -> SourcesResponse
18
+ * POST {base}/v1/connect -> ConnectResponse
19
+ * GET {base}/v1/accounts?... -> AccountsResponse
20
+ * GET {base}/v1/items?... -> ItemsResponse
21
+ * POST {base}/v1/mcp-target -> McpTargetResponse
22
+ *
23
+ * `listTriggerComponents` / `deployTrigger` are deliberately NOT implemented.
24
+ * They are optional on `SourceGateway`, and `service.ts` already answers
25
+ * `${kind} cannot deploy triggers` for a gateway that lacks them. A bridge that
26
+ * fronts a local daemon has nothing to deploy: it is already watching, or it is
27
+ * not running at all. Pretending otherwise would report a source as "watching"
28
+ * when nothing is.
29
+ *
30
+ * LEDGER IDENTITY IS OURS, NOT THE BRIDGE'S.
31
+ *
32
+ * The bridge returns `externalId` and a raw `payload`; `key` and `contentHash`
33
+ * are computed HERE with `fingerprint()`, byte-identically to the Composio
34
+ * adapter. A bridge that minted its own keys would be a second identity
35
+ * function for the same items, and an identity that disagrees with the ledger
36
+ * re-dispatches a backlog the user already cleared.
37
+ *
38
+ * Note what `key` deliberately does NOT include: `accountId`. Two accounts on
39
+ * the same underlying inbox (the read-only and read-write wacli pair, both
40
+ * linked to one WhatsApp number) see the SAME messages. Keying without the
41
+ * account makes that one ledger row handled once, which is the correct
42
+ * behaviour, and it also means re-pairing an account does not invalidate a
43
+ * backlog the user already worked through.
44
+ *
45
+ * CREDENTIALS.
46
+ *
47
+ * The bearer token never leaves this module, and `mcpTarget` results are per
48
+ * turn: they carry live headers minted by the bridge and are never persisted,
49
+ * cached or returned to a client. A `SourceItem` has no credential field and
50
+ * must never grow one - it is projected into `BackfillItem` and crosses the
51
+ * wire to the app.
52
+ */
53
+ import { IngestionError } from "../errors.js";
54
+ import { fingerprint } from "../fingerprint.js";
55
+ /** Hard ceiling on a bridge response body. A local bridge is not a reason to trust it. */
56
+ const MAX_BODY_BYTES = 8 * 1024 * 1024;
57
+ /** Per-request timeout. A wedged local daemon must not hang a paseo turn forever. */
58
+ const REQUEST_TIMEOUT_MS = 30000;
59
+ function notConfigured() {
60
+ throw new IngestionError("SELF_HOSTED_OFF", "This daemon has no self-hosted source bridge configured. Set PASEO_SELF_HOSTED_URL and PASEO_SELF_HOSTED_TOKEN.", 422);
61
+ }
62
+ /**
63
+ * Normalize the configured base so route joining is total.
64
+ *
65
+ * A trailing slash in the env var plus a leading slash in the route yields
66
+ * `//v1/sources`, which some servers route and some 404. Strip it once here
67
+ * rather than defending at five call sites.
68
+ */
69
+ function normalizeBase(url) {
70
+ return url.replace(/\/+$/, "");
71
+ }
72
+ /**
73
+ * Reject a base URL that is not http(s).
74
+ *
75
+ * The URL comes from the user's own environment, so this is not a trust
76
+ * boundary against an attacker. It is a boundary against a typo: a `file:` or
77
+ * `data:` base would make `fetch` do something surprising rather than fail
78
+ * with a readable message.
79
+ */
80
+ function assertHttpUrl(raw) {
81
+ let parsed;
82
+ try {
83
+ parsed = new URL(raw);
84
+ }
85
+ catch {
86
+ throw new IngestionError("SELF_HOSTED_OFF", `PASEO_SELF_HOSTED_URL is not a valid URL: ${raw}`, 422);
87
+ }
88
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
89
+ throw new IngestionError("SELF_HOSTED_OFF", `PASEO_SELF_HOSTED_URL must be http or https, got ${parsed.protocol}`, 422);
90
+ }
91
+ return parsed;
92
+ }
93
+ /** One catalogue page. Matches the Composio adapter's page size so the app's infinite scroll behaves identically. */
94
+ const CATALOGUE_LIMIT = 20;
95
+ function asString(value, fallback = "") {
96
+ return typeof value === "string" ? value : fallback;
97
+ }
98
+ /**
99
+ * Narrow a bridge's account status to the union the rest of the daemon reads.
100
+ *
101
+ * Anything unrecognized becomes "inactive" rather than throwing. A bridge that
102
+ * grows a new status must not take the whole sources list down with it, and
103
+ * "inactive" is the honest reading of "I do not know that this works".
104
+ */
105
+ function asAccountStatus(value) {
106
+ return value === "active" || value === "pending" || value === "inactive" ? value : "inactive";
107
+ }
108
+ export function createSelfHostedGateway(config) {
109
+ // Tolerates an absent config on purpose. `hasConfiguredGateway` asks EVERY
110
+ // kind whether it is configured, so a gateway that throws when its block is
111
+ // missing does not fail its own kind: it takes the whole sources screen down
112
+ // with it. An absent block means exactly "no bridge here", which is what
113
+ // `configured: false` already says.
114
+ const rawUrl = config?.url?.trim();
115
+ const token = config?.token?.trim();
116
+ const configured = Boolean(rawUrl) && Boolean(token);
117
+ const base = rawUrl ? normalizeBase(rawUrl) : "";
118
+ /**
119
+ * One HTTP call to the bridge, with the error taxonomy applied once.
120
+ *
121
+ * The three failure shapes are kept apart on purpose, mirroring the Composio
122
+ * adapter: unreachable (the bridge is down or the URL is wrong), rejected
123
+ * (the token is wrong), and everything else. Collapsing them would send the
124
+ * next reader to restart a daemon when the real problem was a stale token.
125
+ */
126
+ async function call(method, route, init) {
127
+ if (!configured) {
128
+ notConfigured();
129
+ }
130
+ assertHttpUrl(base);
131
+ const url = new URL(`${base}${route}`);
132
+ for (const [key, value] of Object.entries(init?.query ?? {})) {
133
+ if (value === null || value === undefined) {
134
+ continue;
135
+ }
136
+ url.searchParams.set(key, String(value));
137
+ }
138
+ const controller = new AbortController();
139
+ const timer = setTimeout(() => {
140
+ controller.abort();
141
+ }, REQUEST_TIMEOUT_MS);
142
+ let response;
143
+ try {
144
+ response = await fetch(url, {
145
+ method,
146
+ headers: {
147
+ authorization: `Bearer ${token}`,
148
+ accept: "application/json",
149
+ ...(init?.body === undefined ? {} : { "content-type": "application/json" }),
150
+ },
151
+ ...(init?.body === undefined ? {} : { body: JSON.stringify(init.body) }),
152
+ signal: controller.signal,
153
+ });
154
+ }
155
+ catch (cause) {
156
+ const reason = cause instanceof Error && cause.name === "AbortError" ? "timed out" : "is unreachable";
157
+ throw new IngestionError("SELF_HOSTED_OFFLINE", `The self-hosted source bridge at ${base} ${reason}.`, 502);
158
+ }
159
+ finally {
160
+ clearTimeout(timer);
161
+ }
162
+ if (response.status === 401 || response.status === 403) {
163
+ throw new IngestionError("SELF_HOSTED_AUTH", `The self-hosted source bridge at ${base} rejected this daemon's token.`, 502);
164
+ }
165
+ const text = await response.text();
166
+ if (text.length > MAX_BODY_BYTES) {
167
+ throw new IngestionError("SELF_HOSTED_ERROR", `The self-hosted source bridge returned a body over ${MAX_BODY_BYTES} bytes for ${route}.`, 502);
168
+ }
169
+ if (!response.ok) {
170
+ throw new IngestionError("SELF_HOSTED_ERROR", `The self-hosted source bridge answered ${response.status} for ${route}.`, 502);
171
+ }
172
+ try {
173
+ return JSON.parse(text);
174
+ }
175
+ catch {
176
+ throw new IngestionError("SELF_HOSTED_ERROR", `The self-hosted source bridge returned a non-JSON body for ${route}.`, 502);
177
+ }
178
+ }
179
+ return {
180
+ get configured() {
181
+ return configured;
182
+ },
183
+ async listSources(query, cursor) {
184
+ const raw = await call("GET", "/v1/sources", { query: { query, cursor: cursor ?? null, limit: CATALOGUE_LIMIT } });
185
+ const items = (Array.isArray(raw.items) ? raw.items : [])
186
+ .filter((entry) => Boolean(entry) && typeof entry === "object")
187
+ .map((entry) => ({
188
+ slug: asString(entry.slug),
189
+ name: asString(entry.name),
190
+ imgSrc: asString(entry.imgSrc),
191
+ description: asString(entry.description),
192
+ }))
193
+ // A catalogue entry without a slug cannot be connected to, so it is not
194
+ // an entry. Dropping it beats rendering a row that errors on click.
195
+ .filter((entry) => entry.slug.length > 0);
196
+ return {
197
+ items,
198
+ nextCursor: typeof raw.nextCursor === "string" && raw.nextCursor.length > 0 ? raw.nextCursor : null,
199
+ };
200
+ },
201
+ async connect(externalUserId, appSlug) {
202
+ const raw = await call("POST", "/v1/connect", {
203
+ body: { externalUserId, appSlug },
204
+ });
205
+ const redirectUrl = asString(raw.redirectUrl);
206
+ if (redirectUrl.length === 0) {
207
+ throw new IngestionError("SELF_HOSTED_ERROR", `The self-hosted source bridge returned no redirectUrl for ${appSlug}.`, 502);
208
+ }
209
+ return {
210
+ redirectUrl,
211
+ expiresAt: typeof raw.expiresAt === "string" && raw.expiresAt.length > 0 ? raw.expiresAt : null,
212
+ };
213
+ },
214
+ async listAccounts(externalUserId, appSlug) {
215
+ const raw = await call("GET", "/v1/accounts", {
216
+ query: { externalUserId, appSlug },
217
+ });
218
+ return (Array.isArray(raw.accounts) ? raw.accounts : [])
219
+ .filter((account) => Boolean(account) && typeof account === "object")
220
+ .map((account) => ({
221
+ id: asString(account.id),
222
+ status: asAccountStatus(account.status),
223
+ // `label` answers "WHICH account", the question a connected row could
224
+ // not answer before it existed. A bridge that omits it gets null, not "".
225
+ label: typeof account.label === "string" && account.label.length > 0 ? account.label : null,
226
+ }))
227
+ .filter((account) => account.id.length > 0);
228
+ },
229
+ async listItems(input) {
230
+ const raw = await call("GET", "/v1/items", {
231
+ query: {
232
+ externalUserId: input.externalUserId,
233
+ appSlug: input.appSlug,
234
+ accountId: input.accountId,
235
+ sinceMs: input.sinceMs,
236
+ cursor: input.cursor,
237
+ limit: input.limit,
238
+ },
239
+ });
240
+ const items = [];
241
+ for (const entry of Array.isArray(raw.items) ? raw.items : []) {
242
+ if (!entry || typeof entry !== "object") {
243
+ continue;
244
+ }
245
+ const externalId = asString(entry.externalId);
246
+ // No id means no stable ledger identity. Inventing one would re-dispatch
247
+ // the item on every read, so the item is dropped instead.
248
+ if (externalId.length === 0) {
249
+ continue;
250
+ }
251
+ // An adapter that cannot date an item drops it rather than inventing a
252
+ // timestamp: the backfill window is evaluated against this value.
253
+ if (typeof entry.timestampMs !== "number" || !Number.isFinite(entry.timestampMs)) {
254
+ continue;
255
+ }
256
+ const payload = entry.payload && typeof entry.payload === "object" && !Array.isArray(entry.payload)
257
+ ? entry.payload
258
+ : {};
259
+ items.push({
260
+ key: fingerprint("self-hosted", input.appSlug, externalId),
261
+ contentHash: fingerprint(JSON.stringify(payload)),
262
+ timestampMs: entry.timestampMs,
263
+ title: asString(entry.title, externalId),
264
+ subtitle: asString(entry.subtitle),
265
+ payload,
266
+ });
267
+ }
268
+ return {
269
+ items,
270
+ cursor: typeof raw.cursor === "string" && raw.cursor.length > 0 ? raw.cursor : null,
271
+ };
272
+ },
273
+ async mcpTarget(externalUserId, appSlug) {
274
+ const raw = await call("POST", "/v1/mcp-target", {
275
+ body: { externalUserId, appSlug },
276
+ });
277
+ const url = asString(raw.url);
278
+ if (url.length === 0) {
279
+ throw new IngestionError("SELF_HOSTED_ERROR", `The self-hosted source bridge returned no MCP url for ${appSlug}.`, 502);
280
+ }
281
+ const headers = {};
282
+ if (raw.headers && typeof raw.headers === "object" && !Array.isArray(raw.headers)) {
283
+ for (const [key, value] of Object.entries(raw.headers)) {
284
+ if (typeof value === "string") {
285
+ headers[key] = value;
286
+ }
287
+ }
288
+ }
289
+ // Never cached. The headers are a live credential minted by the bridge for
290
+ // this turn; a cache here would widen their reach past the turn that needs them.
291
+ return { url, headers };
292
+ },
293
+ };
294
+ }
295
+ //# sourceMappingURL=gateway.js.map
@@ -14,7 +14,7 @@
14
14
  * `Record<SourceKind, ...>` registry in `adapters.ts` turns that silence into
15
15
  * a compile error.
16
16
  */
17
- export declare const SOURCE_CAPABLE_KINDS: readonly ["pipedream", "composio"];
17
+ export declare const SOURCE_CAPABLE_KINDS: readonly ["pipedream", "composio", "self-hosted"];
18
18
  export type SourceKind = (typeof SOURCE_CAPABLE_KINDS)[number];
19
19
  /** Where to reach an aggregator's remote MCP server for a single agent turn. */
20
20
  export interface McpTarget {
@@ -101,9 +101,22 @@ export interface PipedreamConfig {
101
101
  export interface ComposioConfig {
102
102
  apiKey?: string | undefined;
103
103
  }
104
+ /**
105
+ * Where this daemon's OWN source bridge lives, and the token that opens it.
106
+ *
107
+ * Unlike the aggregator configs above, both fields are user-supplied and
108
+ * user-hosted: paseo holds no platform credential for a self-hosted bridge, it
109
+ * holds a bearer for one endpoint the user runs. `configured` is false when
110
+ * either is missing, which is how the sources screen knows not to offer it.
111
+ */
112
+ export interface SelfHostedConfig {
113
+ url?: string | undefined;
114
+ token?: string | undefined;
115
+ }
104
116
  export interface IngestionConfig {
105
117
  pipedream: PipedreamConfig;
106
118
  composio: ComposioConfig;
119
+ selfHosted?: SelfHostedConfig | undefined;
107
120
  }
108
121
  /**
109
122
  * A single-field object on purpose: later stories add a logger here without
@@ -14,5 +14,5 @@
14
14
  * `Record<SourceKind, ...>` registry in `adapters.ts` turns that silence into
15
15
  * a compile error.
16
16
  */
17
- export const SOURCE_CAPABLE_KINDS = ["pipedream", "composio"];
17
+ export const SOURCE_CAPABLE_KINDS = ["pipedream", "composio", "self-hosted"];
18
18
  //# sourceMappingURL=types.js.map