@wolpertingerlabs/drawlatch 1.0.0-alpha.1

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 (95) hide show
  1. package/CONNECTIONS.md +197 -0
  2. package/INGESTORS.md +790 -0
  3. package/LICENSE +21 -0
  4. package/README.md +685 -0
  5. package/dist/cli/generate-keys.d.ts +15 -0
  6. package/dist/cli/generate-keys.js +107 -0
  7. package/dist/connections/anthropic.json +16 -0
  8. package/dist/connections/bluesky.json +26 -0
  9. package/dist/connections/devin.json +15 -0
  10. package/dist/connections/discord-bot.json +24 -0
  11. package/dist/connections/discord-oauth.json +16 -0
  12. package/dist/connections/github.json +25 -0
  13. package/dist/connections/google-ai.json +15 -0
  14. package/dist/connections/google.json +28 -0
  15. package/dist/connections/hex.json +14 -0
  16. package/dist/connections/lichess.json +15 -0
  17. package/dist/connections/linear.json +29 -0
  18. package/dist/connections/mastodon.json +25 -0
  19. package/dist/connections/notion.json +33 -0
  20. package/dist/connections/openai.json +16 -0
  21. package/dist/connections/openrouter.json +16 -0
  22. package/dist/connections/reddit.json +28 -0
  23. package/dist/connections/slack.json +23 -0
  24. package/dist/connections/stripe.json +25 -0
  25. package/dist/connections/telegram.json +26 -0
  26. package/dist/connections/trello.json +25 -0
  27. package/dist/connections/twitch.json +28 -0
  28. package/dist/connections/x.json +27 -0
  29. package/dist/mcp/server.d.ts +13 -0
  30. package/dist/mcp/server.js +258 -0
  31. package/dist/remote/ingestors/base-ingestor.d.ts +65 -0
  32. package/dist/remote/ingestors/base-ingestor.js +132 -0
  33. package/dist/remote/ingestors/discord/discord-gateway.d.ts +58 -0
  34. package/dist/remote/ingestors/discord/discord-gateway.js +341 -0
  35. package/dist/remote/ingestors/discord/index.d.ts +3 -0
  36. package/dist/remote/ingestors/discord/index.js +3 -0
  37. package/dist/remote/ingestors/discord/types.d.ts +56 -0
  38. package/dist/remote/ingestors/discord/types.js +68 -0
  39. package/dist/remote/ingestors/index.d.ts +16 -0
  40. package/dist/remote/ingestors/index.js +20 -0
  41. package/dist/remote/ingestors/manager.d.ts +65 -0
  42. package/dist/remote/ingestors/manager.js +201 -0
  43. package/dist/remote/ingestors/poll/index.d.ts +2 -0
  44. package/dist/remote/ingestors/poll/index.js +2 -0
  45. package/dist/remote/ingestors/poll/poll-ingestor.d.ts +78 -0
  46. package/dist/remote/ingestors/poll/poll-ingestor.js +283 -0
  47. package/dist/remote/ingestors/registry.d.ts +32 -0
  48. package/dist/remote/ingestors/registry.js +46 -0
  49. package/dist/remote/ingestors/ring-buffer.d.ts +33 -0
  50. package/dist/remote/ingestors/ring-buffer.js +62 -0
  51. package/dist/remote/ingestors/slack/index.d.ts +3 -0
  52. package/dist/remote/ingestors/slack/index.js +3 -0
  53. package/dist/remote/ingestors/slack/socket-mode.d.ts +48 -0
  54. package/dist/remote/ingestors/slack/socket-mode.js +267 -0
  55. package/dist/remote/ingestors/slack/types.d.ts +70 -0
  56. package/dist/remote/ingestors/slack/types.js +72 -0
  57. package/dist/remote/ingestors/types.d.ts +138 -0
  58. package/dist/remote/ingestors/types.js +13 -0
  59. package/dist/remote/ingestors/webhook/base-webhook-ingestor.d.ts +112 -0
  60. package/dist/remote/ingestors/webhook/base-webhook-ingestor.js +119 -0
  61. package/dist/remote/ingestors/webhook/github-types.d.ts +45 -0
  62. package/dist/remote/ingestors/webhook/github-types.js +65 -0
  63. package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +43 -0
  64. package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +86 -0
  65. package/dist/remote/ingestors/webhook/index.d.ts +8 -0
  66. package/dist/remote/ingestors/webhook/index.js +12 -0
  67. package/dist/remote/ingestors/webhook/stripe-types.d.ts +57 -0
  68. package/dist/remote/ingestors/webhook/stripe-types.js +108 -0
  69. package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.d.ts +47 -0
  70. package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.js +90 -0
  71. package/dist/remote/ingestors/webhook/trello-types.d.ts +90 -0
  72. package/dist/remote/ingestors/webhook/trello-types.js +81 -0
  73. package/dist/remote/ingestors/webhook/trello-webhook-ingestor.d.ts +60 -0
  74. package/dist/remote/ingestors/webhook/trello-webhook-ingestor.js +126 -0
  75. package/dist/remote/server.d.ts +103 -0
  76. package/dist/remote/server.js +536 -0
  77. package/dist/shared/config.d.ts +213 -0
  78. package/dist/shared/config.js +269 -0
  79. package/dist/shared/connections.d.ts +72 -0
  80. package/dist/shared/connections.js +103 -0
  81. package/dist/shared/crypto/channel.d.ts +95 -0
  82. package/dist/shared/crypto/channel.js +175 -0
  83. package/dist/shared/crypto/index.d.ts +3 -0
  84. package/dist/shared/crypto/index.js +3 -0
  85. package/dist/shared/crypto/keys.d.ts +92 -0
  86. package/dist/shared/crypto/keys.js +143 -0
  87. package/dist/shared/logger.d.ts +30 -0
  88. package/dist/shared/logger.js +74 -0
  89. package/dist/shared/protocol/handshake.d.ts +116 -0
  90. package/dist/shared/protocol/handshake.js +214 -0
  91. package/dist/shared/protocol/index.d.ts +3 -0
  92. package/dist/shared/protocol/index.js +2 -0
  93. package/dist/shared/protocol/messages.d.ts +46 -0
  94. package/dist/shared/protocol/messages.js +8 -0
  95. package/package.json +105 -0
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Configuration schema and loading for MCP proxy and remote server.
3
+ *
4
+ * Config files:
5
+ * - proxy.config.json — MCP proxy (local) settings
6
+ * - remote.config.json — Remote server settings
7
+ *
8
+ * Each loader falls back to a legacy combined config.json (if present)
9
+ * for backward compatibility, then to built-in defaults.
10
+ *
11
+ * Keys directory: ~/.drawlatch/keys/
12
+ */
13
+ import type { IngestorConfig } from '../remote/ingestors/types.js';
14
+ /** Resolve the base config directory at call time (not import time).
15
+ * Defaults to ~/.drawlatch in the user's home directory.
16
+ * Override with MCP_CONFIG_DIR env var for custom deployments.
17
+ *
18
+ * These are functions (not constants) so that process.env.MCP_CONFIG_DIR can
19
+ * be set at runtime before the first call — important for hosts like
20
+ * callboard that configure the path after ESM imports are resolved. */
21
+ export declare function getConfigDir(): string;
22
+ export declare function getConfigPath(): string;
23
+ export declare function getProxyConfigPath(): string;
24
+ export declare function getRemoteConfigPath(): string;
25
+ export declare function getKeysDir(): string;
26
+ export declare function getLocalKeysDir(): string;
27
+ export declare function getRemoteKeysDir(): string;
28
+ export declare function getPeerKeysDir(): string;
29
+ /** MCP proxy (local) configuration */
30
+ export interface ProxyConfig {
31
+ /** Remote server URL */
32
+ remoteUrl: string;
33
+ /** Key alias — resolved to keys/local/<alias>/.
34
+ * Overridden by the MCP_KEY_ALIAS env var at runtime.
35
+ * When set, takes precedence over localKeysDir. */
36
+ localKeyAlias?: string;
37
+ /** Path to our own key bundle (full-path override; ignored when alias is set) */
38
+ localKeysDir: string;
39
+ /** Path to the remote server's public keys */
40
+ remotePublicKeysDir: string;
41
+ /** Connection timeout (ms) */
42
+ connectTimeout: number;
43
+ /** Request timeout (ms) */
44
+ requestTimeout: number;
45
+ }
46
+ /** A single route / connector definition — scopes secrets and headers to a set of endpoints */
47
+ export interface Route {
48
+ /** Alias for referencing this connector from caller connection lists.
49
+ * Required for custom connectors that callers need to reference by name. */
50
+ alias?: string;
51
+ /** Human-readable name for this route (e.g., "GitHub API", "Stripe Payments").
52
+ * Optional but recommended for discoverability by the local agent. */
53
+ name?: string;
54
+ /** Short description of what this route provides or what it's used for.
55
+ * Optional — helps the agent understand the route's purpose. */
56
+ description?: string;
57
+ /** URL linking to API documentation for the service behind this route.
58
+ * Optional — helps the agent find usage instructions. */
59
+ docsUrl?: string;
60
+ /** URL to an OpenAPI / Swagger spec (JSON or YAML) for this route's API.
61
+ * Optional — provides more structured, agent-friendly documentation. */
62
+ openApiUrl?: string;
63
+ /** Headers to inject automatically into outgoing requests for this route.
64
+ * These MUST NOT conflict with client-provided headers (request is rejected on conflict).
65
+ * Values may contain ${VAR} placeholders resolved against this route's secrets. */
66
+ headers?: Record<string, string>;
67
+ /** Secrets available for ${VAR} placeholder resolution in this route only.
68
+ * Values can be literals or "${ENV_VAR}" references resolved at startup. */
69
+ secrets?: Record<string, string>;
70
+ /** Allowlisted URL patterns (glob). A request must match at least one pattern
71
+ * in this route's list to use this route. Empty = matches nothing. */
72
+ allowedEndpoints: string[];
73
+ /** Whether to resolve ${VAR} placeholders in request bodies.
74
+ * Defaults to false — prevents agents from exfiltrating secrets by
75
+ * writing placeholder strings into API resources and reading them back. */
76
+ resolveSecretsInBody?: boolean;
77
+ /** Optional ingestor configuration for real-time event ingestion.
78
+ * When present, the remote server can start a long-lived ingestor
79
+ * (WebSocket, webhook listener, or poller) for this connection. */
80
+ ingestor?: IngestorConfig;
81
+ }
82
+ /** A route after secret/header resolution — used at runtime */
83
+ export interface ResolvedRoute {
84
+ /** Human-readable name for this route (carried from config) */
85
+ name?: string;
86
+ /** Short description of this route's purpose (carried from config) */
87
+ description?: string;
88
+ /** Link to API documentation for the service behind this route (carried from config) */
89
+ docsUrl?: string;
90
+ /** URL to an OpenAPI / Swagger spec for this route's API (carried from config) */
91
+ openApiUrl?: string;
92
+ headers: Record<string, string>;
93
+ secrets: Record<string, string>;
94
+ allowedEndpoints: string[];
95
+ /** Whether to resolve ${VAR} placeholders in request bodies (default: false) */
96
+ resolveSecretsInBody: boolean;
97
+ }
98
+ /** Per-connection ingestor overrides (all fields optional — omitted fields inherit from template). */
99
+ export interface IngestorOverrides {
100
+ /** Override the Discord Gateway intents bitmask. */
101
+ intents?: number;
102
+ /** Override event type filter (e.g., ["MESSAGE_CREATE"]). Empty array = capture all. */
103
+ eventFilter?: string[];
104
+ /** Only buffer events from these guild IDs. Omitted = all guilds. */
105
+ guildIds?: string[];
106
+ /** Only buffer events from these channel IDs. Omitted = all channels. */
107
+ channelIds?: string[];
108
+ /** Only buffer events from these user IDs. Omitted = all users. */
109
+ userIds?: string[];
110
+ /** Override ring buffer capacity. */
111
+ bufferSize?: number;
112
+ /** Disable the ingestor for this connection entirely. */
113
+ disabled?: boolean;
114
+ /** Override the poll interval in milliseconds (poll ingestors only). */
115
+ intervalMs?: number;
116
+ }
117
+ /** Per-caller access configuration */
118
+ export interface CallerConfig {
119
+ /** Human-readable name for this caller (used in audit logs) */
120
+ name?: string;
121
+ /** Path to this caller's public key files (signing.pub.pem + exchange.pub.pem) */
122
+ peerKeyDir: string;
123
+ /** List of connection aliases — references built-in templates (e.g., "github")
124
+ * or custom connector aliases defined in the top-level connectors array. */
125
+ connections: string[];
126
+ /** Per-caller environment variable overrides.
127
+ * Keys = env var names that connectors reference (e.g., "GITHUB_TOKEN").
128
+ * Values = "${REAL_ENV_VAR}" (redirect to a different env var) or a literal string (direct injection).
129
+ * These are resolved first, then checked BEFORE process.env during secret resolution. */
130
+ env?: Record<string, string>;
131
+ /** Per-connection ingestor overrides. Keys are connection aliases (e.g., "discord-bot").
132
+ * Allows callers to customize intents, event filters, guild/channel/user ID filters,
133
+ * buffer size, or disable an ingestor without modifying the connection template. */
134
+ ingestorOverrides?: Record<string, IngestorOverrides>;
135
+ }
136
+ /** Remote server configuration */
137
+ export interface RemoteServerConfig {
138
+ /** Host to bind to */
139
+ host: string;
140
+ /** Port to listen on */
141
+ port: number;
142
+ /** Path to our own key bundle */
143
+ localKeysDir: string;
144
+ /** Custom connector definitions — a reusable pool referenced by alias from callers.
145
+ * Each connector scopes secrets and headers to endpoint patterns. */
146
+ connectors?: Route[];
147
+ /** Per-caller access control. Keys are caller aliases (used in audit logs).
148
+ * Each caller specifies their peer key directory and which connections they can use. */
149
+ callers: Record<string, CallerConfig>;
150
+ /** Rate limit: max requests per minute per session */
151
+ rateLimitPerMinute: number;
152
+ }
153
+ /**
154
+ * Load the MCP proxy (local) config.
155
+ *
156
+ * Resolution order:
157
+ * 1. proxy.config.json (flat ProxyConfig)
158
+ * 2. config.json → .proxy section (legacy combined format)
159
+ * 3. Built-in defaults
160
+ *
161
+ * Key alias resolution (applied after loading):
162
+ * 1. MCP_KEY_ALIAS env var (highest — set per agent at spawn time)
163
+ * 2. localKeyAlias in config file
164
+ * 3. localKeysDir in config file (explicit full path)
165
+ * 4. Default: keys/local/default
166
+ */
167
+ export declare function loadProxyConfig(): ProxyConfig;
168
+ /**
169
+ * Load the remote server config.
170
+ *
171
+ * Resolution order:
172
+ * 1. remote.config.json (flat RemoteServerConfig)
173
+ * 2. config.json → .remote section (legacy combined format)
174
+ * 3. Built-in defaults
175
+ *
176
+ * Legacy configs with `routes`/`authorizedPeersDir`/`connections` are auto-migrated
177
+ * to the caller-centric format with a deprecation warning.
178
+ */
179
+ export declare function loadRemoteConfig(): RemoteServerConfig;
180
+ export declare function saveProxyConfig(config: ProxyConfig): void;
181
+ export declare function saveRemoteConfig(config: RemoteServerConfig): void;
182
+ /**
183
+ * Resolve the effective routes for a specific caller.
184
+ *
185
+ * For each connection name in the caller's `connections` list:
186
+ * 1. Check custom connectors (by alias) first
187
+ * 2. Fall back to built-in connection templates (e.g., "github", "stripe")
188
+ *
189
+ * Returns an array of Route objects ready for `resolveRoutes()`.
190
+ */
191
+ export declare function resolveCallerRoutes(config: RemoteServerConfig, callerAlias: string): Route[];
192
+ /**
193
+ * Replace ${VAR} placeholders in a string with values from a secrets map.
194
+ * Unknown placeholders are left unchanged (with a warning).
195
+ */
196
+ export declare function resolvePlaceholders(str: string, secretsMap: Record<string, string>): string;
197
+ /**
198
+ * Load secrets from the config's secrets map, resolving from environment
199
+ * variables. Value can be a literal string or "${VAR_NAME}" to read from env.
200
+ *
201
+ * When `envOverrides` is provided (pre-resolved caller env map), those values
202
+ * are checked BEFORE process.env, allowing per-caller secret redirection.
203
+ */
204
+ export declare function resolveSecrets(secretsMap: Record<string, string>, envOverrides?: Record<string, string>): Record<string, string>;
205
+ /**
206
+ * Resolve all routes: resolve secrets from env vars, then resolve header
207
+ * placeholders against each route's own resolved secrets.
208
+ *
209
+ * When `envOverrides` is provided, those pre-resolved values are checked
210
+ * before process.env during secret resolution (used for per-caller env).
211
+ */
212
+ export declare function resolveRoutes(routes: Route[], envOverrides?: Record<string, string>): ResolvedRoute[];
213
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Configuration schema and loading for MCP proxy and remote server.
3
+ *
4
+ * Config files:
5
+ * - proxy.config.json — MCP proxy (local) settings
6
+ * - remote.config.json — Remote server settings
7
+ *
8
+ * Each loader falls back to a legacy combined config.json (if present)
9
+ * for backward compatibility, then to built-in defaults.
10
+ *
11
+ * Keys directory: ~/.drawlatch/keys/
12
+ */
13
+ import fs from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { loadConnection } from './connections.js';
17
+ /** Resolve the base config directory at call time (not import time).
18
+ * Defaults to ~/.drawlatch in the user's home directory.
19
+ * Override with MCP_CONFIG_DIR env var for custom deployments.
20
+ *
21
+ * These are functions (not constants) so that process.env.MCP_CONFIG_DIR can
22
+ * be set at runtime before the first call — important for hosts like
23
+ * callboard that configure the path after ESM imports are resolved. */
24
+ export function getConfigDir() {
25
+ return process.env.MCP_CONFIG_DIR ?? path.join(os.homedir(), '.drawlatch');
26
+ }
27
+ export function getConfigPath() {
28
+ return path.join(getConfigDir(), 'config.json');
29
+ }
30
+ export function getProxyConfigPath() {
31
+ return path.join(getConfigDir(), 'proxy.config.json');
32
+ }
33
+ export function getRemoteConfigPath() {
34
+ return path.join(getConfigDir(), 'remote.config.json');
35
+ }
36
+ export function getKeysDir() {
37
+ return path.join(getConfigDir(), 'keys');
38
+ }
39
+ export function getLocalKeysDir() {
40
+ return path.join(getKeysDir(), 'local');
41
+ }
42
+ export function getRemoteKeysDir() {
43
+ return path.join(getKeysDir(), 'remote');
44
+ }
45
+ export function getPeerKeysDir() {
46
+ return path.join(getKeysDir(), 'peers');
47
+ }
48
+ // ── Defaults ─────────────────────────────────────────────────────────────────
49
+ function proxyDefaults() {
50
+ return {
51
+ remoteUrl: 'http://localhost:9999',
52
+ localKeysDir: path.join(getLocalKeysDir(), 'default'),
53
+ remotePublicKeysDir: path.join(getPeerKeysDir(), 'remote-server'),
54
+ connectTimeout: 10_000,
55
+ requestTimeout: 30_000,
56
+ };
57
+ }
58
+ function remoteDefaults() {
59
+ return {
60
+ host: '127.0.0.1',
61
+ port: 9999,
62
+ localKeysDir: getRemoteKeysDir(),
63
+ callers: {},
64
+ rateLimitPerMinute: 60,
65
+ };
66
+ }
67
+ // ── Split config loading (preferred) ─────────────────────────────────────────
68
+ /**
69
+ * Load the MCP proxy (local) config.
70
+ *
71
+ * Resolution order:
72
+ * 1. proxy.config.json (flat ProxyConfig)
73
+ * 2. config.json → .proxy section (legacy combined format)
74
+ * 3. Built-in defaults
75
+ *
76
+ * Key alias resolution (applied after loading):
77
+ * 1. MCP_KEY_ALIAS env var (highest — set per agent at spawn time)
78
+ * 2. localKeyAlias in config file
79
+ * 3. localKeysDir in config file (explicit full path)
80
+ * 4. Default: keys/local/default
81
+ */
82
+ export function loadProxyConfig() {
83
+ const def = proxyDefaults();
84
+ let config;
85
+ // Try dedicated proxy config file first
86
+ if (fs.existsSync(getProxyConfigPath())) {
87
+ const raw = JSON.parse(fs.readFileSync(getProxyConfigPath(), 'utf-8'));
88
+ config = { ...def, ...raw };
89
+ }
90
+ else if (fs.existsSync(getConfigPath())) {
91
+ // Fall back to combined config.json
92
+ const raw = JSON.parse(fs.readFileSync(getConfigPath(), 'utf-8'));
93
+ config = raw.proxy ? { ...def, ...raw.proxy } : def;
94
+ }
95
+ else {
96
+ config = def;
97
+ }
98
+ // Alias resolution: env var > config alias > localKeysDir > default
99
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentionally coerces empty string to undefined
100
+ const envAlias = process.env.MCP_KEY_ALIAS?.trim() || undefined;
101
+ const alias = envAlias ?? config.localKeyAlias;
102
+ if (alias) {
103
+ config.localKeysDir = path.join(getLocalKeysDir(), alias);
104
+ }
105
+ return config;
106
+ }
107
+ /**
108
+ * Load the remote server config.
109
+ *
110
+ * Resolution order:
111
+ * 1. remote.config.json (flat RemoteServerConfig)
112
+ * 2. config.json → .remote section (legacy combined format)
113
+ * 3. Built-in defaults
114
+ *
115
+ * Legacy configs with `routes`/`authorizedPeersDir`/`connections` are auto-migrated
116
+ * to the caller-centric format with a deprecation warning.
117
+ */
118
+ export function loadRemoteConfig() {
119
+ const def = remoteDefaults();
120
+ let config;
121
+ // Try dedicated remote config file first
122
+ if (fs.existsSync(getRemoteConfigPath())) {
123
+ const raw = JSON.parse(fs.readFileSync(getRemoteConfigPath(), 'utf-8'));
124
+ config = { ...def, ...raw };
125
+ }
126
+ else if (fs.existsSync(getConfigPath())) {
127
+ // Fall back to combined config.json
128
+ const raw = JSON.parse(fs.readFileSync(getConfigPath(), 'utf-8'));
129
+ config = raw.remote ? { ...def, ...raw.remote } : def;
130
+ }
131
+ else {
132
+ config = def;
133
+ }
134
+ // Legacy migration: old format had routes/authorizedPeersDir/connections at top level
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- reading unknown legacy config shape
136
+ const rawConfig = config;
137
+ if (rawConfig.routes && !('default' in config.callers) && !rawConfig.connectors) {
138
+ console.error('[config] Warning: legacy config format detected (routes/authorizedPeersDir/connections). ' +
139
+ 'Migrating to caller-centric format. Please update your remote.config.json.');
140
+ const legacyRoutes = rawConfig.routes;
141
+ const legacyConnections = rawConfig.connections ?? [];
142
+ const legacyPeersDir = rawConfig.authorizedPeersDir ?? path.join(getPeerKeysDir(), 'authorized-clients');
143
+ // Auto-assign aliases to unnamed routes for the default caller
144
+ const connectors = legacyRoutes.map((r, i) => ({
145
+ ...r,
146
+ alias: r.alias ?? r.name?.toLowerCase().replace(/\s+/g, '-') ?? `route-${i}`,
147
+ }));
148
+ const allConnectionNames = [...legacyConnections, ...connectors.map((c) => c.alias)];
149
+ config = {
150
+ ...def,
151
+ host: config.host,
152
+ port: config.port,
153
+ localKeysDir: config.localKeysDir,
154
+ connectors,
155
+ callers: {
156
+ default: {
157
+ peerKeyDir: legacyPeersDir,
158
+ connections: allConnectionNames,
159
+ },
160
+ },
161
+ rateLimitPerMinute: config.rateLimitPerMinute,
162
+ };
163
+ }
164
+ return config;
165
+ }
166
+ // ── Split config saving ─────────────────────────────────────────────────────
167
+ export function saveProxyConfig(config) {
168
+ fs.mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
169
+ fs.writeFileSync(getProxyConfigPath(), JSON.stringify(config, null, 2), { mode: 0o600 });
170
+ }
171
+ export function saveRemoteConfig(config) {
172
+ fs.mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
173
+ fs.writeFileSync(getRemoteConfigPath(), JSON.stringify(config, null, 2), { mode: 0o600 });
174
+ }
175
+ // ── Per-caller route resolution ──────────────────────────────────────────
176
+ /**
177
+ * Resolve the effective routes for a specific caller.
178
+ *
179
+ * For each connection name in the caller's `connections` list:
180
+ * 1. Check custom connectors (by alias) first
181
+ * 2. Fall back to built-in connection templates (e.g., "github", "stripe")
182
+ *
183
+ * Returns an array of Route objects ready for `resolveRoutes()`.
184
+ */
185
+ export function resolveCallerRoutes(config, callerAlias) {
186
+ if (!(callerAlias in config.callers))
187
+ return [];
188
+ const caller = config.callers[callerAlias];
189
+ // Build lookup map for custom connectors by alias
190
+ const connectorsByAlias = new Map();
191
+ for (const c of config.connectors ?? []) {
192
+ if (c.alias)
193
+ connectorsByAlias.set(c.alias, c);
194
+ }
195
+ return caller.connections.map((name) => {
196
+ // Custom connectors take precedence over built-in templates
197
+ const custom = connectorsByAlias.get(name);
198
+ if (custom)
199
+ return custom;
200
+ return loadConnection(name);
201
+ });
202
+ }
203
+ // ── Secret / placeholder resolution ──────────────────────────────────────────
204
+ /**
205
+ * Replace ${VAR} placeholders in a string with values from a secrets map.
206
+ * Unknown placeholders are left unchanged (with a warning).
207
+ */
208
+ export function resolvePlaceholders(str, secretsMap) {
209
+ return str.replace(/\$\{(\w+)\}/g, (match, name) => {
210
+ if (name in secretsMap)
211
+ return secretsMap[name];
212
+ console.error(`[config] Warning: placeholder ${match} not found in secrets`);
213
+ return match;
214
+ });
215
+ }
216
+ /**
217
+ * Load secrets from the config's secrets map, resolving from environment
218
+ * variables. Value can be a literal string or "${VAR_NAME}" to read from env.
219
+ *
220
+ * When `envOverrides` is provided (pre-resolved caller env map), those values
221
+ * are checked BEFORE process.env, allowing per-caller secret redirection.
222
+ */
223
+ export function resolveSecrets(secretsMap, envOverrides) {
224
+ const resolved = {};
225
+ for (const [key, value] of Object.entries(secretsMap)) {
226
+ const envMatch = /^\$\{(.+)\}$/.exec(value);
227
+ if (envMatch) {
228
+ const varName = envMatch[1];
229
+ const envVal = envOverrides?.[varName] ?? process.env[varName];
230
+ if (envVal !== undefined) {
231
+ resolved[key] = envVal;
232
+ }
233
+ else {
234
+ console.error(`[secrets] Warning: env var ${varName} not found for key ${key}`);
235
+ }
236
+ }
237
+ else {
238
+ resolved[key] = value;
239
+ }
240
+ }
241
+ return resolved;
242
+ }
243
+ /**
244
+ * Resolve all routes: resolve secrets from env vars, then resolve header
245
+ * placeholders against each route's own resolved secrets.
246
+ *
247
+ * When `envOverrides` is provided, those pre-resolved values are checked
248
+ * before process.env during secret resolution (used for per-caller env).
249
+ */
250
+ export function resolveRoutes(routes, envOverrides) {
251
+ return routes.map((route) => {
252
+ const resolvedSecrets = resolveSecrets(route.secrets ?? {}, envOverrides);
253
+ const resolvedHeaders = {};
254
+ for (const [key, value] of Object.entries(route.headers ?? {})) {
255
+ resolvedHeaders[key] = resolvePlaceholders(value, resolvedSecrets);
256
+ }
257
+ return {
258
+ ...(route.name !== undefined && { name: route.name }),
259
+ ...(route.description !== undefined && { description: route.description }),
260
+ ...(route.docsUrl !== undefined && { docsUrl: route.docsUrl }),
261
+ ...(route.openApiUrl !== undefined && { openApiUrl: route.openApiUrl }),
262
+ headers: resolvedHeaders,
263
+ secrets: resolvedSecrets,
264
+ allowedEndpoints: route.allowedEndpoints,
265
+ resolveSecretsInBody: route.resolveSecretsInBody ?? false,
266
+ };
267
+ });
268
+ }
269
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Connection template loading.
3
+ *
4
+ * Connections are pre-built Route templates (JSON files) that ship with
5
+ * the package in the connections/ directory. They provide ready-made
6
+ * configurations for popular APIs (GitHub, Stripe, Trello, etc.).
7
+ *
8
+ * At runtime, templates are loaded from disk relative to this module's
9
+ * location, so they work from both src/ (dev via tsx) and dist/ (production).
10
+ */
11
+ import type { Route } from './config.js';
12
+ /** Metadata about a built-in connection template — used by UIs to render
13
+ * connection cards, form fields, and badges without parsing raw JSON. */
14
+ export interface ConnectionTemplateInfo {
15
+ /** Template alias / filename (e.g., "github", "slack"). */
16
+ alias: string;
17
+ /** Human-readable name (e.g., "GitHub API"). */
18
+ name: string;
19
+ /** Short description of the connection's purpose. */
20
+ description?: string;
21
+ /** Link to API documentation. */
22
+ docsUrl?: string;
23
+ /** URL to an OpenAPI / Swagger spec. */
24
+ openApiUrl?: string;
25
+ /** Secret names referenced in route headers — these are auto-injected
26
+ * into every request, so they must always be configured. */
27
+ requiredSecrets: string[];
28
+ /** Secret names defined in the template but NOT referenced in headers.
29
+ * Used by ingestors, URL placeholders, body templates, etc. */
30
+ optionalSecrets: string[];
31
+ /** Whether this connection has an ingestor for real-time events. */
32
+ hasIngestor: boolean;
33
+ /** Ingestor type, when present. */
34
+ ingestorType?: 'websocket' | 'webhook' | 'poll';
35
+ /** Allowlisted URL patterns (glob). */
36
+ allowedEndpoints: string[];
37
+ }
38
+ /**
39
+ * Load a single connection template by name.
40
+ *
41
+ * @param name — Connection name (e.g., "github", "stripe", "trello").
42
+ * Must match the filename without the .json extension.
43
+ * @returns The parsed Route object from the template.
44
+ * @throws If the template file does not exist or contains invalid JSON.
45
+ */
46
+ export declare function loadConnection(name: string): Route;
47
+ /**
48
+ * List all available connection template names.
49
+ *
50
+ * Scans the connections directory for .json files and returns their
51
+ * basenames (without extension), sorted alphabetically.
52
+ */
53
+ export declare function listAvailableConnections(): string[];
54
+ /**
55
+ * List all available connection templates with structured metadata.
56
+ *
57
+ * For each built-in template, returns its name, description, docs links,
58
+ * secrets (categorized as required vs. optional), ingestor info, and
59
+ * allowed endpoints.
60
+ *
61
+ * Secret categorization:
62
+ * - **required** — referenced in route `headers` values (auto-injected
63
+ * into every outgoing request, so they must always be configured).
64
+ * - **optional** — defined in the template's `secrets` map but not
65
+ * referenced in headers (used by ingestors, URL placeholders, etc.).
66
+ *
67
+ * Used by:
68
+ * - callboard's ConnectionManager (local mode, direct import)
69
+ * - admin_list_connection_templates tool handler (remote mode, Stage 3)
70
+ */
71
+ export declare function listConnectionTemplates(): ConnectionTemplateInfo[];
72
+ //# sourceMappingURL=connections.d.ts.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Connection template loading.
3
+ *
4
+ * Connections are pre-built Route templates (JSON files) that ship with
5
+ * the package in the connections/ directory. They provide ready-made
6
+ * configurations for popular APIs (GitHub, Stripe, Trello, etc.).
7
+ *
8
+ * At runtime, templates are loaded from disk relative to this module's
9
+ * location, so they work from both src/ (dev via tsx) and dist/ (production).
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ /** Directory containing connection template JSON files. */
15
+ const CONNECTIONS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'connections');
16
+ /**
17
+ * Load a single connection template by name.
18
+ *
19
+ * @param name — Connection name (e.g., "github", "stripe", "trello").
20
+ * Must match the filename without the .json extension.
21
+ * @returns The parsed Route object from the template.
22
+ * @throws If the template file does not exist or contains invalid JSON.
23
+ */
24
+ export function loadConnection(name) {
25
+ const filePath = path.join(CONNECTIONS_DIR, `${name}.json`);
26
+ if (!fs.existsSync(filePath)) {
27
+ const available = listAvailableConnections();
28
+ throw new Error(`Unknown connection "${name}". Available connections: ${available.join(', ') || '(none)'}`);
29
+ }
30
+ const raw = fs.readFileSync(filePath, 'utf-8');
31
+ return JSON.parse(raw);
32
+ }
33
+ /**
34
+ * List all available connection template names.
35
+ *
36
+ * Scans the connections directory for .json files and returns their
37
+ * basenames (without extension), sorted alphabetically.
38
+ */
39
+ export function listAvailableConnections() {
40
+ if (!fs.existsSync(CONNECTIONS_DIR)) {
41
+ return [];
42
+ }
43
+ return fs
44
+ .readdirSync(CONNECTIONS_DIR, 'utf-8')
45
+ .filter((f) => f.endsWith('.json'))
46
+ .map((f) => f.replace(/\.json$/, ''))
47
+ .sort();
48
+ }
49
+ // ── Template introspection ────────────────────────────────────────────────
50
+ /** Extract ${VAR} placeholder names from a string. */
51
+ function extractPlaceholderNames(str) {
52
+ const names = new Set();
53
+ for (const match of str.matchAll(/\$\{(\w+)\}/g)) {
54
+ names.add(match[1]);
55
+ }
56
+ return names;
57
+ }
58
+ /**
59
+ * List all available connection templates with structured metadata.
60
+ *
61
+ * For each built-in template, returns its name, description, docs links,
62
+ * secrets (categorized as required vs. optional), ingestor info, and
63
+ * allowed endpoints.
64
+ *
65
+ * Secret categorization:
66
+ * - **required** — referenced in route `headers` values (auto-injected
67
+ * into every outgoing request, so they must always be configured).
68
+ * - **optional** — defined in the template's `secrets` map but not
69
+ * referenced in headers (used by ingestors, URL placeholders, etc.).
70
+ *
71
+ * Used by:
72
+ * - callboard's ConnectionManager (local mode, direct import)
73
+ * - admin_list_connection_templates tool handler (remote mode, Stage 3)
74
+ */
75
+ export function listConnectionTemplates() {
76
+ return listAvailableConnections().map((alias) => {
77
+ const route = loadConnection(alias);
78
+ // Collect secret names referenced in header values
79
+ const headerSecretNames = new Set();
80
+ for (const value of Object.values(route.headers ?? {})) {
81
+ for (const name of extractPlaceholderNames(value)) {
82
+ headerSecretNames.add(name);
83
+ }
84
+ }
85
+ // Partition secrets into required (in headers) vs optional (elsewhere)
86
+ const allSecretNames = Object.keys(route.secrets ?? {});
87
+ const requiredSecrets = allSecretNames.filter((s) => headerSecretNames.has(s));
88
+ const optionalSecrets = allSecretNames.filter((s) => !headerSecretNames.has(s));
89
+ return {
90
+ alias,
91
+ name: route.name ?? alias,
92
+ ...(route.description !== undefined && { description: route.description }),
93
+ ...(route.docsUrl !== undefined && { docsUrl: route.docsUrl }),
94
+ ...(route.openApiUrl !== undefined && { openApiUrl: route.openApiUrl }),
95
+ requiredSecrets,
96
+ optionalSecrets,
97
+ hasIngestor: route.ingestor !== undefined,
98
+ ...(route.ingestor !== undefined && { ingestorType: route.ingestor.type }),
99
+ allowedEndpoints: route.allowedEndpoints,
100
+ };
101
+ });
102
+ }
103
+ //# sourceMappingURL=connections.js.map