@wolpertingerlabs/drawlatch 1.0.0-alpha.2 → 1.0.0-alpha.29

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 (119) hide show
  1. package/CONNECTIONS.md +3 -0
  2. package/README.md +395 -465
  3. package/bin/drawlatch.js +1018 -49
  4. package/dist/auth/auth.d.ts +10 -0
  5. package/dist/auth/auth.js +146 -0
  6. package/dist/auth/env-writer.d.ts +6 -0
  7. package/dist/auth/env-writer.js +52 -0
  8. package/dist/auth/password.d.ts +8 -0
  9. package/dist/auth/password.js +28 -0
  10. package/dist/auth/sessions.d.ts +13 -0
  11. package/dist/auth/sessions.js +85 -0
  12. package/dist/cli/generate-keys.d.ts +4 -4
  13. package/dist/cli/generate-keys.js +30 -25
  14. package/dist/connections/{anthropic.json → ai/anthropic.json} +19 -1
  15. package/dist/connections/{devin.json → ai/devin.json} +7 -1
  16. package/dist/connections/{google-ai.json → ai/google-ai.json} +7 -1
  17. package/dist/connections/{openai.json → ai/openai.json} +7 -1
  18. package/dist/connections/{openrouter.json → ai/openrouter.json} +7 -1
  19. package/dist/connections/developer-tools/github.json +138 -0
  20. package/dist/connections/{hex.json → developer-tools/hex.json} +7 -1
  21. package/dist/connections/developer-tools/linear.json +75 -0
  22. package/dist/connections/{lichess.json → gaming/lichess.json} +7 -1
  23. package/dist/connections/messaging/agentmail.json +21 -0
  24. package/dist/connections/messaging/discord-bot.json +114 -0
  25. package/dist/connections/{discord-oauth.json → messaging/discord-oauth.json} +7 -1
  26. package/dist/connections/messaging/slack.json +75 -0
  27. package/dist/connections/messaging/telegram.json +66 -0
  28. package/dist/connections/{google.json → productivity/google.json} +7 -1
  29. package/dist/connections/productivity/notion.json +75 -0
  30. package/dist/connections/productivity/stripe.json +74 -0
  31. package/dist/connections/productivity/trello.json +113 -0
  32. package/dist/connections/{bluesky.json → social-media/bluesky.json} +41 -0
  33. package/dist/connections/social-media/mastodon.json +65 -0
  34. package/dist/connections/social-media/reddit.json +80 -0
  35. package/dist/connections/{twitch.json → social-media/twitch.json} +40 -0
  36. package/dist/connections/social-media/x.json +67 -0
  37. package/dist/mcp/server.js +544 -31
  38. package/dist/remote/admin-mutations.d.ts +37 -0
  39. package/dist/remote/admin-mutations.js +251 -0
  40. package/dist/remote/admin-types.d.ts +149 -0
  41. package/dist/remote/admin-types.js +11 -0
  42. package/dist/remote/admin.d.ts +37 -0
  43. package/dist/remote/admin.js +316 -0
  44. package/dist/remote/caller-bootstrap.d.ts +71 -0
  45. package/dist/remote/caller-bootstrap.js +141 -0
  46. package/dist/remote/ingestors/base-ingestor.d.ts +19 -3
  47. package/dist/remote/ingestors/base-ingestor.js +27 -6
  48. package/dist/remote/ingestors/discord/discord-gateway.d.ts +2 -2
  49. package/dist/remote/ingestors/discord/discord-gateway.js +29 -6
  50. package/dist/remote/ingestors/e2e/setup.d.ts +69 -0
  51. package/dist/remote/ingestors/e2e/setup.js +147 -0
  52. package/dist/remote/ingestors/manager.d.ts +110 -10
  53. package/dist/remote/ingestors/manager.js +449 -42
  54. package/dist/remote/ingestors/poll/poll-ingestor.d.ts +4 -2
  55. package/dist/remote/ingestors/poll/poll-ingestor.js +26 -5
  56. package/dist/remote/ingestors/registry.d.ts +2 -2
  57. package/dist/remote/ingestors/registry.js +2 -2
  58. package/dist/remote/ingestors/slack/socket-mode.d.ts +2 -2
  59. package/dist/remote/ingestors/slack/socket-mode.js +28 -6
  60. package/dist/remote/ingestors/types.d.ts +25 -0
  61. package/dist/remote/ingestors/webhook/base-webhook-ingestor.d.ts +46 -7
  62. package/dist/remote/ingestors/webhook/base-webhook-ingestor.js +115 -10
  63. package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +30 -0
  64. package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +73 -2
  65. package/dist/remote/ingestors/webhook/lifecycle-types.d.ts +63 -0
  66. package/dist/remote/ingestors/webhook/lifecycle-types.js +12 -0
  67. package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.js +2 -2
  68. package/dist/remote/ingestors/webhook/trello-webhook-ingestor.d.ts +17 -5
  69. package/dist/remote/ingestors/webhook/trello-webhook-ingestor.js +32 -26
  70. package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.d.ts +46 -0
  71. package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.js +261 -0
  72. package/dist/remote/server.d.ts +25 -33
  73. package/dist/remote/server.js +681 -195
  74. package/dist/remote/tool-dispatch.d.ts +84 -0
  75. package/dist/remote/tool-dispatch.js +910 -0
  76. package/dist/remote/triggers/rule-engine.d.ts +40 -0
  77. package/dist/remote/triggers/rule-engine.js +228 -0
  78. package/dist/remote/triggers/types.d.ts +69 -0
  79. package/dist/remote/triggers/types.js +10 -0
  80. package/dist/remote/tunnel-state.d.ts +14 -0
  81. package/dist/remote/tunnel-state.js +20 -0
  82. package/dist/remote/tunnel.d.ts +53 -0
  83. package/dist/remote/tunnel.js +149 -0
  84. package/dist/shared/config.d.ts +97 -25
  85. package/dist/shared/config.js +48 -35
  86. package/dist/shared/connections.d.ts +21 -5
  87. package/dist/shared/connections.js +56 -14
  88. package/dist/shared/crypto/index.d.ts +1 -0
  89. package/dist/shared/crypto/index.js +1 -0
  90. package/dist/shared/crypto/key-manager.d.ts +67 -0
  91. package/dist/shared/crypto/key-manager.js +152 -0
  92. package/dist/shared/env-utils.d.ts +42 -0
  93. package/dist/shared/env-utils.js +150 -0
  94. package/dist/shared/listener-config.d.ts +157 -0
  95. package/dist/shared/listener-config.js +10 -0
  96. package/dist/shared/migrations.d.ts +40 -0
  97. package/dist/shared/migrations.js +122 -0
  98. package/dist/shared/protocol/handshake.js +14 -1
  99. package/dist/shared/protocol/index.d.ts +2 -0
  100. package/dist/shared/protocol/index.js +2 -0
  101. package/dist/shared/protocol/sync-client.d.ts +52 -0
  102. package/dist/shared/protocol/sync-client.js +99 -0
  103. package/dist/shared/protocol/sync.d.ts +71 -0
  104. package/dist/shared/protocol/sync.js +176 -0
  105. package/frontend/dist/assets/index-D4k5QKBP.css +1 -0
  106. package/frontend/dist/assets/index-IzwnSOmu.js +267 -0
  107. package/frontend/dist/index.html +15 -0
  108. package/package.json +66 -13
  109. package/dist/connections/discord-bot.json +0 -24
  110. package/dist/connections/github.json +0 -25
  111. package/dist/connections/linear.json +0 -29
  112. package/dist/connections/mastodon.json +0 -25
  113. package/dist/connections/notion.json +0 -33
  114. package/dist/connections/reddit.json +0 -28
  115. package/dist/connections/slack.json +0 -23
  116. package/dist/connections/stripe.json +0 -25
  117. package/dist/connections/telegram.json +0 -26
  118. package/dist/connections/trello.json +0 -25
  119. package/dist/connections/x.json +0 -27
@@ -11,6 +11,10 @@
11
11
  * Keys directory: ~/.drawlatch/keys/
12
12
  */
13
13
  import type { IngestorConfig } from '../remote/ingestors/types.js';
14
+ import type { TriggerRule } from '../remote/triggers/types.js';
15
+ import type { TestConnectionConfig, TestIngestorConfig, ListenerConfigSchema } from './listener-config.js';
16
+ export type { TestConnectionConfig, TestIngestorConfig, ListenerConfigSchema, } from './listener-config.js';
17
+ export type { ListenerConfigField, ListenerConfigOption } from './listener-config.js';
14
18
  /** Resolve the base config directory at call time (not import time).
15
19
  * Defaults to ~/.drawlatch in the user's home directory.
16
20
  * Override with MCP_CONFIG_DIR env var for custom deployments.
@@ -23,22 +27,16 @@ export declare function getConfigPath(): string;
23
27
  export declare function getProxyConfigPath(): string;
24
28
  export declare function getRemoteConfigPath(): string;
25
29
  export declare function getKeysDir(): string;
26
- export declare function getLocalKeysDir(): string;
27
- export declare function getRemoteKeysDir(): string;
28
- export declare function getPeerKeysDir(): string;
30
+ export declare function getCallerKeysDir(): string;
31
+ export declare function getServerKeysDir(): string;
29
32
  export declare function getEnvFilePath(): string;
33
+ /** Category grouping for built-in connection templates.
34
+ * Matches the subdirectory name under src/connections/. */
35
+ export type ConnectionCategory = 'ai' | 'developer-tools' | 'gaming' | 'messaging' | 'productivity' | 'social-media';
30
36
  /** MCP proxy (local) configuration */
31
37
  export interface ProxyConfig {
32
38
  /** Remote server URL */
33
39
  remoteUrl: string;
34
- /** Key alias — resolved to keys/local/<alias>/.
35
- * Overridden by the MCP_KEY_ALIAS env var at runtime.
36
- * When set, takes precedence over localKeysDir. */
37
- localKeyAlias?: string;
38
- /** Path to our own key bundle (full-path override; ignored when alias is set) */
39
- localKeysDir: string;
40
- /** Path to the remote server's public keys */
41
- remotePublicKeysDir: string;
42
40
  /** Connection timeout (ms) */
43
41
  connectTimeout: number;
44
42
  /** Request timeout (ms) */
@@ -61,6 +59,14 @@ export interface Route {
61
59
  /** URL to an OpenAPI / Swagger spec (JSON or YAML) for this route's API.
62
60
  * Optional — provides more structured, agent-friendly documentation. */
63
61
  openApiUrl?: string;
62
+ /** Stability level of this connection: "stable", "beta", or "dev".
63
+ * Defaults to "dev" if omitted. Helps agents and UIs communicate
64
+ * whether a connection is production-ready, in testing, or experimental. */
65
+ stability?: 'stable' | 'beta' | 'dev';
66
+ /** Category grouping for this connection template (e.g., "ai", "messaging").
67
+ * Matches the subdirectory under src/connections/. Only present on built-in
68
+ * connection templates; custom connectors may omit it. */
69
+ category?: ConnectionCategory;
64
70
  /** Headers to inject automatically into outgoing requests for this route.
65
71
  * These MUST NOT conflict with client-provided headers (request is rejected on conflict).
66
72
  * Values may contain ${VAR} placeholders resolved against this route's secrets. */
@@ -79,9 +85,22 @@ export interface Route {
79
85
  * When present, the remote server can start a long-lived ingestor
80
86
  * (WebSocket, webhook listener, or poller) for this connection. */
81
87
  ingestor?: IngestorConfig;
88
+ /** Pre-configured test request for verifying connection credentials.
89
+ * Must be a non-destructive, read-only endpoint with zero side effects. */
90
+ testConnection?: TestConnectionConfig;
91
+ /** Pre-configured test for verifying ingestor / event listener configuration.
92
+ * Set to null to explicitly indicate this listener cannot be tested.
93
+ * Omitted if the connection has no ingestor. */
94
+ testIngestor?: TestIngestorConfig | null;
95
+ /** Schema describing configurable fields for this connection's event listener.
96
+ * Used by UIs and management tools to render configuration forms.
97
+ * Only present on connections that have an ingestor. */
98
+ listenerConfig?: ListenerConfigSchema;
82
99
  }
83
100
  /** A route after secret/header resolution — used at runtime */
84
101
  export interface ResolvedRoute {
102
+ /** Connection alias (e.g., "github", "discord-bot"). Populated during caller route resolution. */
103
+ alias?: string;
85
104
  /** Human-readable name for this route (carried from config) */
86
105
  name?: string;
87
106
  /** Short description of this route's purpose (carried from config) */
@@ -90,11 +109,23 @@ export interface ResolvedRoute {
90
109
  docsUrl?: string;
91
110
  /** URL to an OpenAPI / Swagger spec for this route's API (carried from config) */
92
111
  openApiUrl?: string;
112
+ /** Stability level (carried from config) */
113
+ stability?: 'stable' | 'beta' | 'dev';
114
+ /** Category grouping (carried from config) */
115
+ category?: ConnectionCategory;
93
116
  headers: Record<string, string>;
94
117
  secrets: Record<string, string>;
95
118
  allowedEndpoints: string[];
96
119
  /** Whether to resolve ${VAR} placeholders in request bodies (default: false) */
97
120
  resolveSecretsInBody: boolean;
121
+ /** Pre-configured test request for verifying connection credentials (carried from config) */
122
+ testConnection?: TestConnectionConfig;
123
+ /** Pre-configured test for verifying ingestor / event listener (carried from config) */
124
+ testIngestor?: TestIngestorConfig | null;
125
+ /** Listener configuration schema for UI rendering (carried from config) */
126
+ listenerConfig?: ListenerConfigSchema;
127
+ /** Raw ingestor configuration (carried from config, needed by tool handlers) */
128
+ ingestorConfig?: IngestorConfig;
98
129
  }
99
130
  /** Per-connection ingestor overrides (all fields optional — omitted fields inherit from template). */
100
131
  export interface IngestorOverrides {
@@ -114,13 +145,16 @@ export interface IngestorOverrides {
114
145
  disabled?: boolean;
115
146
  /** Override the poll interval in milliseconds (poll ingestors only). */
116
147
  intervalMs?: number;
148
+ /** Generic parameter bag for listener configuration.
149
+ * Keys correspond to ListenerConfigField.key values from the connection's
150
+ * listenerConfig schema. Values are mapped to typed ingestor config fields
151
+ * during mergeIngestorConfig(). */
152
+ params?: Record<string, unknown>;
117
153
  }
118
154
  /** Per-caller access configuration */
119
155
  export interface CallerConfig {
120
156
  /** Human-readable name for this caller (used in audit logs) */
121
157
  name?: string;
122
- /** Path to this caller's public key files (signing.pub.pem + exchange.pub.pem) */
123
- peerKeyDir: string;
124
158
  /** List of connection aliases — references built-in templates (e.g., "github")
125
159
  * or custom connector aliases defined in the top-level connectors array. */
126
160
  connections: string[];
@@ -133,6 +167,26 @@ export interface CallerConfig {
133
167
  * Allows callers to customize intents, event filters, guild/channel/user ID filters,
134
168
  * buffer size, or disable an ingestor without modifying the connection template. */
135
169
  ingestorOverrides?: Record<string, IngestorOverrides>;
170
+ /** Multi-instance listener definitions keyed by connection alias.
171
+ * Value is a map of instanceId → IngestorOverrides.
172
+ * When present for a connection, spawns one ingestor per instanceId instead of
173
+ * a single default instance. Takes precedence over ingestorOverrides for that connection.
174
+ * Instance IDs must match /^[a-zA-Z0-9_-]+$/.
175
+ *
176
+ * Example:
177
+ * ```json
178
+ * {
179
+ * "trello": {
180
+ * "project-board": { "params": { "boardId": "abc123" } },
181
+ * "sprint-board": { "params": { "boardId": "def456" } }
182
+ * }
183
+ * }
184
+ * ``` */
185
+ listenerInstances?: Record<string, Record<string, IngestorOverrides>>;
186
+ /** Trigger rules that map ingestor events to Claude Code remote trigger invocations.
187
+ * When an ingestor event matches a rule's criteria (source, event type, filter),
188
+ * the engine dispatches the event to the configured remote trigger. */
189
+ triggerRules?: TriggerRule[];
136
190
  }
137
191
  /** Remote server configuration */
138
192
  export interface RemoteServerConfig {
@@ -140,16 +194,21 @@ export interface RemoteServerConfig {
140
194
  host: string;
141
195
  /** Port to listen on */
142
196
  port: number;
143
- /** Path to our own key bundle */
144
- localKeysDir: string;
145
197
  /** Custom connector definitions — a reusable pool referenced by alias from callers.
146
198
  * Each connector scopes secrets and headers to endpoint patterns. */
147
199
  connectors?: Route[];
148
200
  /** Per-caller access control. Keys are caller aliases (used in audit logs).
149
- * Each caller specifies their peer key directory and which connections they can use. */
201
+ * Each caller specifies which connections they can use.
202
+ * Caller public keys are loaded from keys/callers/<alias>/. */
150
203
  callers: Record<string, CallerConfig>;
151
204
  /** Rate limit: max requests per minute per session */
152
205
  rateLimitPerMinute: number;
206
+ /** When true, drawlatch brings up and supervises its own cloudflared quick
207
+ * tunnel on startup, injecting the public URL into callback-dependent
208
+ * connection configs before secret resolution and ingestor start (item C).
209
+ * Equivalent to setting DRAWLATCH_TUNNEL=1. Not a control surface — just a
210
+ * config flag. Default: false. */
211
+ tunnel?: boolean;
153
212
  }
154
213
  /**
155
214
  * Load the MCP proxy (local) config.
@@ -159,13 +218,17 @@ export interface RemoteServerConfig {
159
218
  * 2. config.json → .proxy section (legacy combined format)
160
219
  * 3. Built-in defaults
161
220
  *
162
- * Key alias resolution (applied after loading):
163
- * 1. MCP_KEY_ALIAS env var (highest — set per agent at spawn time)
164
- * 2. localKeyAlias in config file
165
- * 3. localKeysDir in config file (explicit full path)
166
- * 4. Default: keys/local/default
221
+ * Key paths are derived automatically:
222
+ * - Caller keys: keys/callers/{MCP_KEY_ALIAS || "default"}/
223
+ * - Server keys: keys/server/
167
224
  */
168
225
  export declare function loadProxyConfig(): ProxyConfig;
226
+ /**
227
+ * Resolve the caller key alias for the MCP proxy.
228
+ *
229
+ * Resolution: MCP_KEY_ALIAS env var > "default"
230
+ */
231
+ export declare function resolveCallerKeyAlias(): string;
169
232
  /**
170
233
  * Load the remote server config.
171
234
  *
@@ -199,16 +262,25 @@ export declare function resolvePlaceholders(str: string, secretsMap: Record<stri
199
262
  * Load secrets from the config's secrets map, resolving from environment
200
263
  * variables. Value can be a literal string or "${VAR_NAME}" to read from env.
201
264
  *
202
- * When `envOverrides` is provided (pre-resolved caller env map), those values
203
- * are checked BEFORE process.env, allowing per-caller secret redirection.
265
+ * Resolution order for each ${VAR} reference:
266
+ * 1. `envOverrides[VAR]` caller's explicit env mapping (pre-resolved)
267
+ * 2. `process.env[PREFIX_VAR]` — prefixed env var (e.g., ALICE_GITHUB_TOKEN)
268
+ * 3. Warning if not found (bare process.env fallback intentionally removed
269
+ * to prevent cross-caller secret leakage)
270
+ *
271
+ * When `callerAlias` is omitted, step 2 is skipped (used for resolving
272
+ * the caller's own env mapping where prefixing doesn't apply).
204
273
  */
205
- export declare function resolveSecrets(secretsMap: Record<string, string>, envOverrides?: Record<string, string>): Record<string, string>;
274
+ export declare function resolveSecrets(secretsMap: Record<string, string>, envOverrides?: Record<string, string>, callerAlias?: string): Record<string, string>;
206
275
  /**
207
276
  * Resolve all routes: resolve secrets from env vars, then resolve header
208
277
  * placeholders against each route's own resolved secrets.
209
278
  *
210
279
  * When `envOverrides` is provided, those pre-resolved values are checked
211
280
  * before process.env during secret resolution (used for per-caller env).
281
+ *
282
+ * When `callerAlias` is provided, prefixed env vars (e.g., ALICE_GITHUB_TOKEN)
283
+ * are checked as a fallback before giving up on a ${VAR} reference.
212
284
  */
213
- export declare function resolveRoutes(routes: Route[], envOverrides?: Record<string, string>): ResolvedRoute[];
285
+ export declare function resolveRoutes(routes: Route[], envOverrides?: Record<string, string>, callerAlias?: string): ResolvedRoute[];
214
286
  //# sourceMappingURL=config.d.ts.map
@@ -36,14 +36,11 @@ export function getRemoteConfigPath() {
36
36
  export function getKeysDir() {
37
37
  return path.join(getConfigDir(), 'keys');
38
38
  }
39
- export function getLocalKeysDir() {
40
- return path.join(getKeysDir(), 'local');
39
+ export function getCallerKeysDir() {
40
+ return path.join(getKeysDir(), 'callers');
41
41
  }
42
- export function getRemoteKeysDir() {
43
- return path.join(getKeysDir(), 'remote');
44
- }
45
- export function getPeerKeysDir() {
46
- return path.join(getKeysDir(), 'peers');
42
+ export function getServerKeysDir() {
43
+ return path.join(getKeysDir(), 'server');
47
44
  }
48
45
  export function getEnvFilePath() {
49
46
  return path.join(getConfigDir(), '.env');
@@ -52,8 +49,6 @@ export function getEnvFilePath() {
52
49
  function proxyDefaults() {
53
50
  return {
54
51
  remoteUrl: 'http://localhost:9999',
55
- localKeysDir: path.join(getLocalKeysDir(), 'default'),
56
- remotePublicKeysDir: path.join(getPeerKeysDir(), 'remote-server'),
57
52
  connectTimeout: 10_000,
58
53
  requestTimeout: 30_000,
59
54
  };
@@ -62,7 +57,6 @@ function remoteDefaults() {
62
57
  return {
63
58
  host: '127.0.0.1',
64
59
  port: 9999,
65
- localKeysDir: getRemoteKeysDir(),
66
60
  callers: {},
67
61
  rateLimitPerMinute: 60,
68
62
  };
@@ -76,11 +70,9 @@ function remoteDefaults() {
76
70
  * 2. config.json → .proxy section (legacy combined format)
77
71
  * 3. Built-in defaults
78
72
  *
79
- * Key alias resolution (applied after loading):
80
- * 1. MCP_KEY_ALIAS env var (highest — set per agent at spawn time)
81
- * 2. localKeyAlias in config file
82
- * 3. localKeysDir in config file (explicit full path)
83
- * 4. Default: keys/local/default
73
+ * Key paths are derived automatically:
74
+ * - Caller keys: keys/callers/{MCP_KEY_ALIAS || "default"}/
75
+ * - Server keys: keys/server/
84
76
  */
85
77
  export function loadProxyConfig() {
86
78
  const def = proxyDefaults();
@@ -98,15 +90,17 @@ export function loadProxyConfig() {
98
90
  else {
99
91
  config = def;
100
92
  }
101
- // Alias resolution: env var > config alias > localKeysDir > default
102
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentionally coerces empty string to undefined
103
- const envAlias = process.env.MCP_KEY_ALIAS?.trim() || undefined;
104
- const alias = envAlias ?? config.localKeyAlias;
105
- if (alias) {
106
- config.localKeysDir = path.join(getLocalKeysDir(), alias);
107
- }
108
93
  return config;
109
94
  }
95
+ /**
96
+ * Resolve the caller key alias for the MCP proxy.
97
+ *
98
+ * Resolution: MCP_KEY_ALIAS env var > "default"
99
+ */
100
+ export function resolveCallerKeyAlias() {
101
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentionally coerces empty string
102
+ return process.env.MCP_KEY_ALIAS?.trim() || 'default';
103
+ }
110
104
  /**
111
105
  * Load the remote server config.
112
106
  *
@@ -142,7 +136,6 @@ export function loadRemoteConfig() {
142
136
  'Migrating to caller-centric format. Please update your remote.config.json.');
143
137
  const legacyRoutes = rawConfig.routes;
144
138
  const legacyConnections = rawConfig.connections ?? [];
145
- const legacyPeersDir = rawConfig.authorizedPeersDir ?? path.join(getPeerKeysDir(), 'authorized-clients');
146
139
  // Auto-assign aliases to unnamed routes for the default caller
147
140
  const connectors = legacyRoutes.map((r, i) => ({
148
141
  ...r,
@@ -153,11 +146,9 @@ export function loadRemoteConfig() {
153
146
  ...def,
154
147
  host: config.host,
155
148
  port: config.port,
156
- localKeysDir: config.localKeysDir,
157
149
  connectors,
158
150
  callers: {
159
151
  default: {
160
- peerKeyDir: legacyPeersDir,
161
152
  connections: allConnectionNames,
162
153
  },
163
154
  },
@@ -198,9 +189,9 @@ export function resolveCallerRoutes(config, callerAlias) {
198
189
  return caller.connections.map((name) => {
199
190
  // Custom connectors take precedence over built-in templates
200
191
  const custom = connectorsByAlias.get(name);
201
- if (custom)
202
- return custom;
203
- return loadConnection(name);
192
+ const route = custom ?? loadConnection(name);
193
+ // Ensure every route carries its alias so it survives resolution
194
+ return route.alias === name ? route : { ...route, alias: name };
204
195
  });
205
196
  }
206
197
  // ── Secret / placeholder resolution ──────────────────────────────────────────
@@ -220,21 +211,33 @@ export function resolvePlaceholders(str, secretsMap) {
220
211
  * Load secrets from the config's secrets map, resolving from environment
221
212
  * variables. Value can be a literal string or "${VAR_NAME}" to read from env.
222
213
  *
223
- * When `envOverrides` is provided (pre-resolved caller env map), those values
224
- * are checked BEFORE process.env, allowing per-caller secret redirection.
214
+ * Resolution order for each ${VAR} reference:
215
+ * 1. `envOverrides[VAR]` caller's explicit env mapping (pre-resolved)
216
+ * 2. `process.env[PREFIX_VAR]` — prefixed env var (e.g., ALICE_GITHUB_TOKEN)
217
+ * 3. Warning if not found (bare process.env fallback intentionally removed
218
+ * to prevent cross-caller secret leakage)
219
+ *
220
+ * When `callerAlias` is omitted, step 2 is skipped (used for resolving
221
+ * the caller's own env mapping where prefixing doesn't apply).
225
222
  */
226
- export function resolveSecrets(secretsMap, envOverrides) {
223
+ export function resolveSecrets(secretsMap, envOverrides, callerAlias) {
227
224
  const resolved = {};
228
225
  for (const [key, value] of Object.entries(secretsMap)) {
229
226
  const envMatch = /^\$\{(.+)\}$/.exec(value);
230
227
  if (envMatch) {
231
228
  const varName = envMatch[1];
232
- const envVal = envOverrides?.[varName] ?? process.env[varName];
229
+ // 1. Caller's explicit env mapping (pre-resolved values)
230
+ let envVal = envOverrides?.[varName];
231
+ // 2. Prefixed env var (e.g., ALICE_GITHUB_TOKEN for caller "alice" + var "GITHUB_TOKEN")
232
+ if (envVal === undefined && callerAlias) {
233
+ const prefix = callerAlias.toUpperCase().replace(/-/g, '_');
234
+ envVal = process.env[`${prefix}_${varName}`];
235
+ }
233
236
  if (envVal !== undefined) {
234
237
  resolved[key] = envVal;
235
238
  }
236
239
  else {
237
- console.error(`[secrets] Warning: env var ${varName} not found for key ${key}`);
240
+ console.error(`[secrets] Warning: env var ${varName} not found for key ${key}${callerAlias ? ` (caller: ${callerAlias})` : ''}`);
238
241
  }
239
242
  }
240
243
  else {
@@ -249,23 +252,33 @@ export function resolveSecrets(secretsMap, envOverrides) {
249
252
  *
250
253
  * When `envOverrides` is provided, those pre-resolved values are checked
251
254
  * before process.env during secret resolution (used for per-caller env).
255
+ *
256
+ * When `callerAlias` is provided, prefixed env vars (e.g., ALICE_GITHUB_TOKEN)
257
+ * are checked as a fallback before giving up on a ${VAR} reference.
252
258
  */
253
- export function resolveRoutes(routes, envOverrides) {
259
+ export function resolveRoutes(routes, envOverrides, callerAlias) {
254
260
  return routes.map((route) => {
255
- const resolvedSecrets = resolveSecrets(route.secrets ?? {}, envOverrides);
261
+ const resolvedSecrets = resolveSecrets(route.secrets ?? {}, envOverrides, callerAlias);
256
262
  const resolvedHeaders = {};
257
263
  for (const [key, value] of Object.entries(route.headers ?? {})) {
258
264
  resolvedHeaders[key] = resolvePlaceholders(value, resolvedSecrets);
259
265
  }
260
266
  return {
267
+ ...(route.alias !== undefined && { alias: route.alias }),
261
268
  ...(route.name !== undefined && { name: route.name }),
262
269
  ...(route.description !== undefined && { description: route.description }),
263
270
  ...(route.docsUrl !== undefined && { docsUrl: route.docsUrl }),
264
271
  ...(route.openApiUrl !== undefined && { openApiUrl: route.openApiUrl }),
272
+ ...(route.stability !== undefined && { stability: route.stability }),
273
+ ...(route.category !== undefined && { category: route.category }),
265
274
  headers: resolvedHeaders,
266
275
  secrets: resolvedSecrets,
267
276
  allowedEndpoints: route.allowedEndpoints,
268
277
  resolveSecretsInBody: route.resolveSecretsInBody ?? false,
278
+ ...(route.testConnection !== undefined && { testConnection: route.testConnection }),
279
+ ...(route.testIngestor !== undefined && { testIngestor: route.testIngestor }),
280
+ ...(route.listenerConfig !== undefined && { listenerConfig: route.listenerConfig }),
281
+ ...(route.ingestor !== undefined && { ingestorConfig: route.ingestor }),
269
282
  };
270
283
  });
271
284
  }
@@ -2,13 +2,13 @@
2
2
  * Connection template loading.
3
3
  *
4
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.).
5
+ * the package in the connections/ directory, organized into category
6
+ * subdirectories (ai/, messaging/, social-media/, etc.).
7
7
  *
8
8
  * At runtime, templates are loaded from disk relative to this module's
9
9
  * location, so they work from both src/ (dev via tsx) and dist/ (production).
10
10
  */
11
- import type { Route } from './config.js';
11
+ import type { Route, ConnectionCategory } from './config.js';
12
12
  /** Metadata about a built-in connection template — used by UIs to render
13
13
  * connection cards, form fields, and badges without parsing raw JSON. */
14
14
  export interface ConnectionTemplateInfo {
@@ -22,6 +22,10 @@ export interface ConnectionTemplateInfo {
22
22
  docsUrl?: string;
23
23
  /** URL to an OpenAPI / Swagger spec. */
24
24
  openApiUrl?: string;
25
+ /** Stability level: "stable", "beta", or "dev". */
26
+ stability: 'stable' | 'beta' | 'dev';
27
+ /** Category grouping (e.g., "ai", "messaging", "social-media"). */
28
+ category: ConnectionCategory;
25
29
  /** Secret names referenced in route headers — these are auto-injected
26
30
  * into every request, so they must always be configured. */
27
31
  requiredSecrets: string[];
@@ -32,9 +36,20 @@ export interface ConnectionTemplateInfo {
32
36
  hasIngestor: boolean;
33
37
  /** Ingestor type, when present. */
34
38
  ingestorType?: 'websocket' | 'webhook' | 'poll';
39
+ /** Whether this connection has a pre-configured test request. */
40
+ hasTestConnection: boolean;
41
+ /** Whether this connection's ingestor has a pre-configured test. */
42
+ hasTestIngestor: boolean;
43
+ /** Whether this connection has a listener configuration schema. */
44
+ hasListenerConfig: boolean;
45
+ /** Whether this connection's listener supports multiple concurrent instances
46
+ * (e.g., watching multiple Trello boards or Reddit subreddits simultaneously). */
47
+ supportsMultiInstance: boolean;
35
48
  /** Allowlisted URL patterns (glob). */
36
49
  allowedEndpoints: string[];
37
50
  }
51
+ /** Invalidate the cached index. Exported for testing only. */
52
+ export declare function _resetConnectionIndex(): void;
38
53
  /**
39
54
  * Load a single connection template by name.
40
55
  *
@@ -47,8 +62,9 @@ export declare function loadConnection(name: string): Route;
47
62
  /**
48
63
  * List all available connection template names.
49
64
  *
50
- * Scans the connections directory for .json files and returns their
51
- * basenames (without extension), sorted alphabetically.
65
+ * Scans the connections directory (including category subdirectories) for
66
+ * .json files and returns their basenames (without extension), sorted
67
+ * alphabetically.
52
68
  */
53
69
  export declare function listAvailableConnections(): string[];
54
70
  /**
@@ -2,8 +2,8 @@
2
2
  * Connection template loading.
3
3
  *
4
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.).
5
+ * the package in the connections/ directory, organized into category
6
+ * subdirectories (ai/, messaging/, social-media/, etc.).
7
7
  *
8
8
  * At runtime, templates are loaded from disk relative to this module's
9
9
  * location, so they work from both src/ (dev via tsx) and dist/ (production).
@@ -13,6 +13,46 @@ import path from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
14
  /** Directory containing connection template JSON files. */
15
15
  const CONNECTIONS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'connections');
16
+ // ── Lazy-cached alias→filepath index ──────────────────────────────────────
17
+ /** Cached alias → absolute filepath index. Built lazily on first access.
18
+ * Supports both flat files (connections/foo.json) and category
19
+ * subdirectories (connections/ai/anthropic.json). */
20
+ let connectionIndex = null;
21
+ /** Build the alias→filepath index by scanning CONNECTIONS_DIR.
22
+ * Files at the top level and files in one level of subdirectories are
23
+ * both indexed. The alias is always the filename without .json. */
24
+ function getConnectionIndex() {
25
+ if (connectionIndex)
26
+ return connectionIndex;
27
+ connectionIndex = new Map();
28
+ if (!fs.existsSync(CONNECTIONS_DIR))
29
+ return connectionIndex;
30
+ const entries = fs.readdirSync(CONNECTIONS_DIR, { withFileTypes: true });
31
+ for (const entry of entries) {
32
+ if (entry.isFile() && entry.name.endsWith('.json')) {
33
+ // Top-level JSON file (backward compat)
34
+ const alias = entry.name.replace(/\.json$/, '');
35
+ connectionIndex.set(alias, path.join(CONNECTIONS_DIR, entry.name));
36
+ }
37
+ else if (entry.isDirectory()) {
38
+ // Category subdirectory — scan one level deep
39
+ const subdir = path.join(CONNECTIONS_DIR, entry.name);
40
+ const subEntries = fs.readdirSync(subdir, 'utf-8');
41
+ for (const subFile of subEntries) {
42
+ if (subFile.endsWith('.json')) {
43
+ const alias = subFile.replace(/\.json$/, '');
44
+ connectionIndex.set(alias, path.join(subdir, subFile));
45
+ }
46
+ }
47
+ }
48
+ }
49
+ return connectionIndex;
50
+ }
51
+ /** Invalidate the cached index. Exported for testing only. */
52
+ export function _resetConnectionIndex() {
53
+ connectionIndex = null;
54
+ }
55
+ // ── Public API ────────────────────────────────────────────────────────────
16
56
  /**
17
57
  * Load a single connection template by name.
18
58
  *
@@ -22,8 +62,9 @@ const CONNECTIONS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)),
22
62
  * @throws If the template file does not exist or contains invalid JSON.
23
63
  */
24
64
  export function loadConnection(name) {
25
- const filePath = path.join(CONNECTIONS_DIR, `${name}.json`);
26
- if (!fs.existsSync(filePath)) {
65
+ const index = getConnectionIndex();
66
+ const filePath = index.get(name);
67
+ if (!filePath) {
27
68
  const available = listAvailableConnections();
28
69
  throw new Error(`Unknown connection "${name}". Available connections: ${available.join(', ') || '(none)'}`);
29
70
  }
@@ -33,18 +74,13 @@ export function loadConnection(name) {
33
74
  /**
34
75
  * List all available connection template names.
35
76
  *
36
- * Scans the connections directory for .json files and returns their
37
- * basenames (without extension), sorted alphabetically.
77
+ * Scans the connections directory (including category subdirectories) for
78
+ * .json files and returns their basenames (without extension), sorted
79
+ * alphabetically.
38
80
  */
39
81
  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();
82
+ const index = getConnectionIndex();
83
+ return [...index.keys()].sort();
48
84
  }
49
85
  // ── Template introspection ────────────────────────────────────────────────
50
86
  /** Extract ${VAR} placeholder names from a string. */
@@ -92,10 +128,16 @@ export function listConnectionTemplates() {
92
128
  ...(route.description !== undefined && { description: route.description }),
93
129
  ...(route.docsUrl !== undefined && { docsUrl: route.docsUrl }),
94
130
  ...(route.openApiUrl !== undefined && { openApiUrl: route.openApiUrl }),
131
+ stability: route.stability ?? 'dev',
132
+ category: route.category,
95
133
  requiredSecrets,
96
134
  optionalSecrets,
97
135
  hasIngestor: route.ingestor !== undefined,
98
136
  ...(route.ingestor !== undefined && { ingestorType: route.ingestor.type }),
137
+ hasTestConnection: route.testConnection !== undefined,
138
+ hasTestIngestor: route.testIngestor !== undefined && route.testIngestor !== null,
139
+ hasListenerConfig: route.listenerConfig !== undefined,
140
+ supportsMultiInstance: route.listenerConfig?.supportsMultiInstance ?? false,
99
141
  allowedEndpoints: route.allowedEndpoints,
100
142
  };
101
143
  });
@@ -1,3 +1,4 @@
1
1
  export { type KeyBundle, type SerializedKeyBundle, type PublicKeyBundle, type SerializedPublicKeys, generateKeyBundle, extractPublicKeys, serializeKeyBundle, deserializeKeyBundle, serializePublicKeys, deserializePublicKeys, saveKeyBundle, loadKeyBundle, loadPublicKeys, fingerprint, } from './keys.js';
2
2
  export { type DirectionalKey, type SessionKeys, deriveSessionKeys, EncryptedChannel, } from './channel.js';
3
+ export { type CreateCallerResult, createCaller, exportCallerPublicKeys, exportServerPublicKeys, importCallerPublicKeys, saveServerPublicKeys, listCallers, callerExists, serverExists, callerFingerprint, serverFingerprint, } from './key-manager.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
1
  export { generateKeyBundle, extractPublicKeys, serializeKeyBundle, deserializeKeyBundle, serializePublicKeys, deserializePublicKeys, saveKeyBundle, loadKeyBundle, loadPublicKeys, fingerprint, } from './keys.js';
2
2
  export { deriveSessionKeys, EncryptedChannel, } from './channel.js';
3
+ export { createCaller, exportCallerPublicKeys, exportServerPublicKeys, importCallerPublicKeys, saveServerPublicKeys, listCallers, callerExists, serverExists, callerFingerprint, serverFingerprint, } from './key-manager.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * High-level key management API.
3
+ *
4
+ * Provides CRUD operations over the drawlatch key directory structure
5
+ * so that callboard (and other consumers) can manage keys programmatically
6
+ * without touching the filesystem directly.
7
+ *
8
+ * Key layout:
9
+ * keys/callers/<alias>/ — Caller keypairs (one per alias)
10
+ * keys/server/ — Server keypair (single, flat)
11
+ */
12
+ import { type SerializedPublicKeys } from './keys.js';
13
+ export interface CreateCallerResult {
14
+ publicKeys: SerializedPublicKeys;
15
+ fingerprint: string;
16
+ }
17
+ interface KeyManagerOpts {
18
+ configDir?: string;
19
+ }
20
+ /**
21
+ * Create a new caller identity (Ed25519 + X25519 keypairs).
22
+ * Saves under `keys/callers/<alias>/`.
23
+ * Throws if the alias already exists.
24
+ */
25
+ export declare function createCaller(alias: string, opts?: KeyManagerOpts): CreateCallerResult;
26
+ /**
27
+ * Export public keys for a caller identity.
28
+ * Reads from `keys/callers/<alias>/`.
29
+ */
30
+ export declare function exportCallerPublicKeys(alias: string, opts?: KeyManagerOpts): SerializedPublicKeys;
31
+ /**
32
+ * Export public keys for the server.
33
+ * Reads from `keys/server/`.
34
+ */
35
+ export declare function exportServerPublicKeys(opts?: KeyManagerOpts): SerializedPublicKeys;
36
+ /**
37
+ * Import a caller's public keys. Saves under `keys/callers/<alias>/`.
38
+ * Used by the server to store received caller public keys (e.g., via sync).
39
+ */
40
+ export declare function importCallerPublicKeys(alias: string, keys: SerializedPublicKeys, opts?: KeyManagerOpts): void;
41
+ /**
42
+ * Save server public keys. Writes to `keys/server/`.
43
+ * Used by callboard to store the remote server's public keys (e.g., via sync).
44
+ */
45
+ export declare function saveServerPublicKeys(keys: SerializedPublicKeys, opts?: KeyManagerOpts): void;
46
+ /**
47
+ * List all caller aliases (scans `keys/callers/`).
48
+ */
49
+ export declare function listCallers(opts?: KeyManagerOpts): string[];
50
+ /**
51
+ * Check if a caller identity exists (has at least public keys).
52
+ */
53
+ export declare function callerExists(alias: string, opts?: KeyManagerOpts): boolean;
54
+ /**
55
+ * Check if server keys exist.
56
+ */
57
+ export declare function serverExists(opts?: KeyManagerOpts): boolean;
58
+ /**
59
+ * Get the fingerprint of a caller's keys.
60
+ */
61
+ export declare function callerFingerprint(alias: string, opts?: KeyManagerOpts): string;
62
+ /**
63
+ * Get the fingerprint of the server's keys.
64
+ */
65
+ export declare function serverFingerprint(opts?: KeyManagerOpts): string;
66
+ export {};
67
+ //# sourceMappingURL=key-manager.d.ts.map