@wolpertingerlabs/drawlatch 1.0.0-alpha.3 → 1.0.0-alpha.35

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 (123) hide show
  1. package/CONNECTIONS.md +3 -0
  2. package/README.md +393 -465
  3. package/bin/drawlatch.js +1151 -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 +43 -0
  39. package/dist/remote/admin-mutations.js +321 -0
  40. package/dist/remote/admin-types.d.ts +153 -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 +317 -0
  44. package/dist/remote/caller-bootstrap.d.ts +121 -0
  45. package/dist/remote/caller-bootstrap.js +270 -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 +596 -187
  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 +13 -0
  83. package/dist/remote/tunnel.js +33 -0
  84. package/dist/shared/config.d.ts +104 -25
  85. package/dist/shared/config.js +49 -36
  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 +81 -0
  91. package/dist/shared/crypto/key-manager.js +174 -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/caller-bundle-crypto.d.ts +37 -0
  99. package/dist/shared/protocol/caller-bundle-crypto.js +62 -0
  100. package/dist/shared/protocol/caller-bundle.d.ts +75 -0
  101. package/dist/shared/protocol/caller-bundle.js +24 -0
  102. package/dist/shared/protocol/handshake.js +14 -1
  103. package/dist/shared/protocol/index.d.ts +2 -0
  104. package/dist/shared/protocol/index.js +2 -0
  105. package/dist/shared/protocol/sync-client.d.ts +52 -0
  106. package/dist/shared/protocol/sync-client.js +99 -0
  107. package/dist/shared/protocol/sync.d.ts +71 -0
  108. package/dist/shared/protocol/sync.js +176 -0
  109. package/frontend/dist/assets/index-BdCSPSZK.js +282 -0
  110. package/frontend/dist/assets/index-BmK26bY2.css +1 -0
  111. package/frontend/dist/index.html +15 -0
  112. package/package.json +65 -13
  113. package/dist/connections/discord-bot.json +0 -24
  114. package/dist/connections/github.json +0 -25
  115. package/dist/connections/linear.json +0 -29
  116. package/dist/connections/mastodon.json +0 -25
  117. package/dist/connections/notion.json +0 -33
  118. package/dist/connections/reddit.json +0 -28
  119. package/dist/connections/slack.json +0 -23
  120. package/dist/connections/stripe.json +0 -25
  121. package/dist/connections/telegram.json +0 -26
  122. package/dist/connections/trello.json +0 -25
  123. package/dist/connections/x.json +0 -27
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Mutating admin API (item A), mounted behind `requireAuth` at `/api/admin/*`
3
+ * alongside the read-only endpoints in `admin.ts`.
4
+ *
5
+ * Every mutation reuses the canonical tool-dispatch (item D) so there is exactly
6
+ * one implementation of connection/secret/listener logic. Caller create/delete
7
+ * uses the programmatic bootstrap (item E).
8
+ *
9
+ * Security invariants (same as the read-only side):
10
+ * - Secret values are write-only: set via PUT, read back only as booleans.
11
+ * - No secret value, AES channel key, or process.env value is ever serialized.
12
+ * - After any config mutation, the daemon's live-reload path runs so ingestors
13
+ * and routes pick up the change (replacing callboard's restart banner).
14
+ */
15
+ import express from 'express';
16
+ import { type RemoteServerConfig, type ResolvedRoute } from '../shared/config.js';
17
+ import type { IngestorManager } from './ingestors/index.js';
18
+ export interface AdminMutationDeps {
19
+ ingestorManager: () => IngestorManager;
20
+ loadConfig: () => RemoteServerConfig;
21
+ /** Resolve the live routes for a caller (with secrets) for tool dispatch. */
22
+ resolveRoutesForCaller: (alias: string) => ResolvedRoute[];
23
+ /** Re-resolve routes for all active sessions of a caller (live reload). */
24
+ refreshCaller: (alias: string) => void;
25
+ /** Register/refresh the authorized peer for a newly-created caller. */
26
+ reloadPeer: (alias: string) => void;
27
+ /** Drop the authorized peer + active sessions for a deleted caller. */
28
+ removePeer: (alias: string) => void;
29
+ /** Audit-log a sensitive admin mutation (credential issuance). Optional so
30
+ * tests can omit it; the daemon wires it to the request audit log. */
31
+ audit?: (action: string, details?: Record<string, unknown>) => void;
32
+ /** Endpoint URL to pin in an issued bundle when the request omits one
33
+ * (the public tunnel URL, or the daemon's own host:port). */
34
+ defaultEndpointUrl?: () => string;
35
+ }
36
+ /**
37
+ * Mount the mutating routes onto an existing admin router.
38
+ *
39
+ * The router is already behind `requireAuth` (wired in server.ts), so these
40
+ * handlers can assume the request is authenticated.
41
+ */
42
+ export declare function mountAdminMutations(router: express.Router, deps: AdminMutationDeps): void;
43
+ //# sourceMappingURL=admin-mutations.d.ts.map
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Mutating admin API (item A), mounted behind `requireAuth` at `/api/admin/*`
3
+ * alongside the read-only endpoints in `admin.ts`.
4
+ *
5
+ * Every mutation reuses the canonical tool-dispatch (item D) so there is exactly
6
+ * one implementation of connection/secret/listener logic. Caller create/delete
7
+ * uses the programmatic bootstrap (item E).
8
+ *
9
+ * Security invariants (same as the read-only side):
10
+ * - Secret values are write-only: set via PUT, read back only as booleans.
11
+ * - No secret value, AES channel key, or process.env value is ever serialized.
12
+ * - After any config mutation, the daemon's live-reload path runs so ingestors
13
+ * and routes pick up the change (replacing callboard's restart banner).
14
+ */
15
+ import rateLimit from 'express-rate-limit';
16
+ import { saveRemoteConfig } from '../shared/config.js';
17
+ import { dispatchTool } from './tool-dispatch.js';
18
+ import { createCallerWithKeys, deleteCaller, issueCallerBundle, CALLER_ALIAS_REGEX, } from './caller-bootstrap.js';
19
+ /** Translate a thrown error into an HTTP status + message. */
20
+ function errorStatus(message) {
21
+ if (/already exists/i.test(message))
22
+ return 409;
23
+ if (/not found|unknown caller/i.test(message))
24
+ return 404;
25
+ if (/cannot delete|invalid|required/i.test(message))
26
+ return 400;
27
+ return 500;
28
+ }
29
+ /**
30
+ * Mount the mutating routes onto an existing admin router.
31
+ *
32
+ * The router is already behind `requireAuth` (wired in server.ts), so these
33
+ * handlers can assume the request is authenticated.
34
+ */
35
+ export function mountAdminMutations(router, deps) {
36
+ /** Build a ToolContext for a caller, wired to live reload on mutation. */
37
+ function ctx(alias) {
38
+ return {
39
+ callerAlias: alias,
40
+ ingestorManager: deps.ingestorManager(),
41
+ refreshRoutes: () => deps.refreshCaller(alias),
42
+ };
43
+ }
44
+ /** Guard: 404 unless the caller exists in config. */
45
+ function callerExists(alias) {
46
+ return alias in deps.loadConfig().callers;
47
+ }
48
+ /** Run a tool and send its result, mapping thrown errors to HTTP codes. */
49
+ async function runTool(res, alias, tool, input) {
50
+ if (!callerExists(alias)) {
51
+ res.status(404).json({ error: `Unknown caller: ${alias}` });
52
+ return;
53
+ }
54
+ try {
55
+ const result = await dispatchTool(tool, input, deps.resolveRoutesForCaller(alias), ctx(alias));
56
+ res.json(result);
57
+ }
58
+ catch (err) {
59
+ const message = err instanceof Error ? err.message : String(err);
60
+ res.status(errorStatus(message)).json({ error: message });
61
+ }
62
+ }
63
+ // ── Callers (create / delete) ──────────────────────────────────────────
64
+ // POST /api/admin/callers { alias, name?, connections? }
65
+ router.post('/callers', (req, res) => {
66
+ const { alias, name, connections } = (req.body ?? {});
67
+ if (!alias || typeof alias !== 'string') {
68
+ res.status(400).json({ error: 'alias is required and must be a string' });
69
+ return;
70
+ }
71
+ if (!CALLER_ALIAS_REGEX.test(alias)) {
72
+ res.status(400).json({
73
+ error: 'alias must start with a letter or number and contain only letters, numbers, hyphens, and underscores',
74
+ });
75
+ return;
76
+ }
77
+ try {
78
+ const result = createCallerWithKeys(alias, {
79
+ ...(name !== undefined && { name }),
80
+ ...(Array.isArray(connections) && { connections }),
81
+ });
82
+ deps.reloadPeer(alias);
83
+ res.status(201).json({
84
+ alias: result.alias,
85
+ name: result.name,
86
+ fingerprint: result.fingerprint,
87
+ publicKeys: result.publicKeys,
88
+ keysDir: result.keysDir,
89
+ connections: result.connections,
90
+ });
91
+ }
92
+ catch (err) {
93
+ const message = err instanceof Error ? err.message : String(err);
94
+ res.status(errorStatus(message)).json({ error: message });
95
+ }
96
+ });
97
+ // ── Caller credential issuance ─────────────────────────────────────────
98
+ //
99
+ // Mint (or rotate) a caller credential bundle and return it ONCE. drawlatch
100
+ // keeps only the public key, so the private material is unrecoverable after
101
+ // this response — re-issue to rotate. Rate-limited and audit-logged; the
102
+ // bundle never carries any connection-secret value (only connection names).
103
+ const issueLimiter = rateLimit({
104
+ windowMs: 60 * 1000,
105
+ max: 10,
106
+ standardHeaders: true,
107
+ legacyHeaders: false,
108
+ message: { error: 'Too many issuance requests. Try again in a minute.' },
109
+ });
110
+ // POST /api/admin/callers/:alias/issue { connections?, endpointUrl?, passphrase?, name? }
111
+ router.post('/callers/:alias/issue', issueLimiter, (req, res) => {
112
+ const alias = req.params.alias;
113
+ if (!CALLER_ALIAS_REGEX.test(alias)) {
114
+ res.status(400).json({ error: 'Invalid caller alias' });
115
+ return;
116
+ }
117
+ const { connections, endpointUrl, passphrase, name } = (req.body ?? {});
118
+ if (connections !== undefined &&
119
+ (!Array.isArray(connections) || !connections.every((c) => typeof c === 'string'))) {
120
+ res.status(400).json({ error: 'connections must be an array of strings' });
121
+ return;
122
+ }
123
+ if (endpointUrl !== undefined && typeof endpointUrl !== 'string') {
124
+ res.status(400).json({ error: 'endpointUrl must be a string' });
125
+ return;
126
+ }
127
+ if (passphrase !== undefined && typeof passphrase !== 'string') {
128
+ res.status(400).json({ error: 'passphrase must be a string' });
129
+ return;
130
+ }
131
+ if (name !== undefined && typeof name !== 'string') {
132
+ res.status(400).json({ error: 'name must be a string' });
133
+ return;
134
+ }
135
+ const resolvedEndpoint = typeof endpointUrl === 'string' && endpointUrl.trim() !== ''
136
+ ? endpointUrl.trim()
137
+ : (deps.defaultEndpointUrl?.() ?? '');
138
+ const reissue = alias in deps.loadConfig().callers;
139
+ try {
140
+ const bundle = issueCallerBundle({
141
+ alias,
142
+ endpointUrl: resolvedEndpoint,
143
+ ...(typeof name === 'string' && { name }),
144
+ ...(Array.isArray(connections) && { connections }),
145
+ ...(typeof passphrase === 'string' && passphrase !== '' && { passphrase }),
146
+ });
147
+ // Install the fresh public key and invalidate any prior credential's
148
+ // active sessions (removePeer drops sessions + peer; reloadPeer re-adds
149
+ // the new key) so a rotation takes effect immediately.
150
+ deps.removePeer(alias);
151
+ deps.reloadPeer(alias);
152
+ deps.audit?.('caller_issue', {
153
+ caller: alias,
154
+ reissue,
155
+ passphraseWrapped: bundle.encryption !== null,
156
+ connections: bundle.connections,
157
+ fingerprint: bundle.fingerprint,
158
+ });
159
+ res.json(bundle);
160
+ }
161
+ catch (err) {
162
+ const message = err instanceof Error ? err.message : String(err);
163
+ res.status(errorStatus(message)).json({ error: message });
164
+ }
165
+ });
166
+ // DELETE /api/admin/callers/:alias
167
+ router.delete('/callers/:alias', (req, res) => {
168
+ try {
169
+ deleteCaller(req.params.alias);
170
+ deps.removePeer(req.params.alias);
171
+ res.json({ deleted: req.params.alias });
172
+ }
173
+ catch (err) {
174
+ const message = err instanceof Error ? err.message : String(err);
175
+ res.status(errorStatus(message)).json({ error: message });
176
+ }
177
+ });
178
+ // ── Tunnel (cloudflared) config-only toggle ────────────────────────────
179
+ //
180
+ // Persists `config.tunnel` to remote.config.json. The daemon reads this flag
181
+ // at boot to decide whether to spawn cloudflared, so a restart is required
182
+ // for the change to take effect. The dashboard surfaces this expectation via
183
+ // a banner when `tunnelEnabled` (intent) disagrees with `tunnelUrl` (runtime).
184
+ //
185
+ // PUT /api/admin/tunnel { enabled: boolean }
186
+ router.put('/tunnel', (req, res) => {
187
+ const { enabled } = (req.body ?? {});
188
+ if (typeof enabled !== 'boolean') {
189
+ res.status(400).json({ error: 'enabled must be a boolean' });
190
+ return;
191
+ }
192
+ const config = deps.loadConfig();
193
+ config.tunnel = enabled;
194
+ saveRemoteConfig(config);
195
+ res.json({ tunnel: enabled });
196
+ });
197
+ // ── Connections (enable / secrets / test) ──────────────────────────────
198
+ // POST /api/admin/callers/:alias/connections/:connection { enabled }
199
+ router.post('/callers/:alias/connections/:connection', async (req, res) => {
200
+ const { enabled } = (req.body ?? {});
201
+ if (typeof enabled !== 'boolean') {
202
+ res.status(400).json({ error: 'enabled must be a boolean' });
203
+ return;
204
+ }
205
+ await runTool(res, req.params.alias, 'set_connection_enabled', {
206
+ connection: req.params.connection,
207
+ enabled,
208
+ });
209
+ });
210
+ // PUT /api/admin/callers/:alias/connections/:connection/secrets { secrets }
211
+ router.put('/callers/:alias/connections/:connection/secrets', async (req, res) => {
212
+ const { secrets } = (req.body ?? {});
213
+ if (!secrets || typeof secrets !== 'object') {
214
+ res.status(400).json({ error: 'secrets must be an object of name → value' });
215
+ return;
216
+ }
217
+ await runTool(res, req.params.alias, 'set_secrets', { secrets });
218
+ });
219
+ // POST /api/admin/callers/:alias/connections/:connection/test
220
+ router.post('/callers/:alias/connections/:connection/test', async (req, res) => {
221
+ await runTool(res, req.params.alias, 'test_connection', {
222
+ connection: req.params.connection,
223
+ });
224
+ });
225
+ // POST /api/admin/callers/:alias/connections/:connection/test-ingestor
226
+ router.post('/callers/:alias/connections/:connection/test-ingestor', async (req, res) => {
227
+ await runTool(res, req.params.alias, 'test_ingestor', {
228
+ connection: req.params.connection,
229
+ });
230
+ });
231
+ // ── Listener / ingestor control ────────────────────────────────────────
232
+ // POST /api/admin/callers/:alias/connections/:connection/listener/control
233
+ // { action: 'start'|'stop'|'restart', instance_id? }
234
+ router.post('/callers/:alias/connections/:connection/listener/control', async (req, res) => {
235
+ const { action, instance_id } = (req.body ?? {});
236
+ if (action !== 'start' && action !== 'stop' && action !== 'restart') {
237
+ res.status(400).json({ error: "action must be 'start', 'stop', or 'restart'" });
238
+ return;
239
+ }
240
+ await runTool(res, req.params.alias, 'control_listener', {
241
+ connection: req.params.connection,
242
+ action,
243
+ ...(instance_id !== undefined && { instance_id }),
244
+ });
245
+ });
246
+ // GET /api/admin/callers/:alias/listener-configs
247
+ router.get('/callers/:alias/listener-configs', async (req, res) => {
248
+ await runTool(res, req.params.alias, 'list_listener_configs', {});
249
+ });
250
+ // GET /api/admin/callers/:alias/connections/:connection/listener/params?instance_id=
251
+ router.get('/callers/:alias/connections/:connection/listener/params', async (req, res) => {
252
+ const instanceId = req.query.instance_id;
253
+ await runTool(res, req.params.alias, 'get_listener_params', {
254
+ connection: req.params.connection,
255
+ ...(typeof instanceId === 'string' && { instance_id: instanceId }),
256
+ });
257
+ });
258
+ // PUT /api/admin/callers/:alias/connections/:connection/listener/params
259
+ // { params, instance_id?, create_instance? }
260
+ router.put('/callers/:alias/connections/:connection/listener/params', async (req, res) => {
261
+ const { params, instance_id, create_instance } = (req.body ?? {});
262
+ if (!params || typeof params !== 'object') {
263
+ res.status(400).json({ error: 'params must be an object' });
264
+ return;
265
+ }
266
+ await runTool(res, req.params.alias, 'set_listener_params', {
267
+ connection: req.params.connection,
268
+ params,
269
+ ...(instance_id !== undefined && { instance_id }),
270
+ ...(create_instance !== undefined && { create_instance }),
271
+ });
272
+ });
273
+ // GET /api/admin/callers/:alias/connections/:connection/listener/instances
274
+ router.get('/callers/:alias/connections/:connection/listener/instances', async (req, res) => {
275
+ await runTool(res, req.params.alias, 'list_listener_instances', {
276
+ connection: req.params.connection,
277
+ });
278
+ });
279
+ // POST /api/admin/callers/:alias/connections/:connection/listener/instances
280
+ // { instance_id, params? } — creates a new multi-instance listener
281
+ router.post('/callers/:alias/connections/:connection/listener/instances', async (req, res) => {
282
+ const { instance_id, params } = (req.body ?? {});
283
+ if (!instance_id || typeof instance_id !== 'string') {
284
+ res.status(400).json({ error: 'instance_id is required and must be a string' });
285
+ return;
286
+ }
287
+ if (!CALLER_ALIAS_REGEX.test(instance_id)) {
288
+ res.status(400).json({
289
+ error: 'instance_id must start with a letter or number and contain only letters, numbers, hyphens, and underscores',
290
+ });
291
+ return;
292
+ }
293
+ await runTool(res, req.params.alias, 'set_listener_params', {
294
+ connection: req.params.connection,
295
+ instance_id,
296
+ params: params ?? {},
297
+ create_instance: true,
298
+ });
299
+ });
300
+ // DELETE /api/admin/callers/:alias/connections/:connection/listener/instances/:instanceId
301
+ router.delete('/callers/:alias/connections/:connection/listener/instances/:instanceId', async (req, res) => {
302
+ await runTool(res, req.params.alias, 'delete_listener_instance', {
303
+ connection: req.params.connection,
304
+ instance_id: req.params.instanceId,
305
+ });
306
+ });
307
+ // POST /api/admin/callers/:alias/connections/:connection/listener/resolve-options
308
+ // { paramKey }
309
+ router.post('/callers/:alias/connections/:connection/listener/resolve-options', async (req, res) => {
310
+ const { paramKey } = (req.body ?? {});
311
+ if (!paramKey || typeof paramKey !== 'string') {
312
+ res.status(400).json({ error: 'paramKey is required and must be a string' });
313
+ return;
314
+ }
315
+ await runTool(res, req.params.alias, 'resolve_listener_options', {
316
+ connection: req.params.connection,
317
+ paramKey,
318
+ });
319
+ });
320
+ }
321
+ //# sourceMappingURL=admin-mutations.js.map
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Response DTOs for the daemon's `/admin/*` API (served at `/api/admin/*`).
3
+ *
4
+ * This file is the single source of truth for these shapes. `src/remote/admin.ts`
5
+ * imports them to annotate its handler responses, and the frontend imports them
6
+ * (type-only) instead of maintaining a duplicate `shared` mirror.
7
+ *
8
+ * Type-only — compiles to nothing.
9
+ */
10
+ export type { CallerBundleV1, BundleEncryption, CallerSource, } from '../shared/protocol/caller-bundle.js';
11
+ export interface AdminMeta {
12
+ version: string;
13
+ port: number;
14
+ pid: number;
15
+ startedAt: number;
16
+ uptimeSec: number;
17
+ configPath: string;
18
+ callerKeysDir: string;
19
+ serverKeysDir: string;
20
+ envFilePath: string;
21
+ /** Public URL of the self-managed cloudflared tunnel, when active (item C). */
22
+ tunnelUrl: string | null;
23
+ /** Whether the cloudflared tunnel is enabled in config (intent flag).
24
+ * Distinct from `tunnelUrl`, which reflects the active runtime state —
25
+ * the two disagree until the daemon restarts after a toggle. */
26
+ tunnelEnabled: boolean;
27
+ }
28
+ export interface AdminIngestorCounts {
29
+ connected: number;
30
+ error: number;
31
+ starting: number;
32
+ stopped: number;
33
+ }
34
+ export interface AdminHealth {
35
+ status: 'ok';
36
+ activeSessions: number;
37
+ ingestorCounts: AdminIngestorCounts;
38
+ uptimeSec: number;
39
+ }
40
+ export interface AdminCaller {
41
+ alias: string;
42
+ name: string | null;
43
+ connections: string[];
44
+ envKeys: string[];
45
+ fingerprint: string | null;
46
+ keysDirExists: boolean;
47
+ /** How this caller's keypair was provisioned (drives the source badge).
48
+ * null for callers created before issuance existed. */
49
+ source: 'local-auto' | 'bundle-issued' | null;
50
+ }
51
+ export type ConnectionCategory = 'ai' | 'developer-tools' | 'gaming' | 'messaging' | 'productivity' | 'social-media';
52
+ export interface AdminConnectionTemplate {
53
+ alias: string;
54
+ name: string;
55
+ description?: string;
56
+ docsUrl?: string;
57
+ openApiUrl?: string;
58
+ stability: 'stable' | 'beta' | 'dev';
59
+ category: ConnectionCategory;
60
+ requiredSecrets: string[];
61
+ optionalSecrets: string[];
62
+ hasIngestor: boolean;
63
+ ingestorType?: 'websocket' | 'webhook' | 'poll';
64
+ hasTestConnection: boolean;
65
+ hasTestIngestor: boolean;
66
+ hasListenerConfig: boolean;
67
+ supportsMultiInstance: boolean;
68
+ allowedEndpoints: string[];
69
+ }
70
+ /** Per-caller status for EVERY connection template — whether the caller has it
71
+ * enabled, and which secrets are configured (booleans only, never values).
72
+ * This is the one-call payload the dashboard Connections page renders from. */
73
+ export interface AdminConnectionStatus extends AdminConnectionTemplate {
74
+ enabled: boolean;
75
+ requiredSecretsSet: Record<string, boolean>;
76
+ optionalSecretsSet: Record<string, boolean>;
77
+ }
78
+ export interface AdminSecretRef {
79
+ name: string;
80
+ present: boolean;
81
+ }
82
+ export interface AdminListenerInstance {
83
+ instanceId: string;
84
+ enabled: boolean;
85
+ params: Record<string, unknown>;
86
+ }
87
+ export interface AdminCallerConnection {
88
+ connectionAlias: string;
89
+ enabled: boolean;
90
+ isCustom: boolean;
91
+ requiredSecrets: AdminSecretRef[];
92
+ optionalSecrets: AdminSecretRef[];
93
+ hasIngestor: boolean;
94
+ instances: AdminListenerInstance[];
95
+ }
96
+ export type IngestorState = 'starting' | 'connected' | 'reconnecting' | 'stopped' | 'error';
97
+ export interface AdminIngestor {
98
+ callerAlias: string;
99
+ connection: string;
100
+ instanceId?: string;
101
+ type: 'websocket' | 'webhook' | 'poll';
102
+ state: IngestorState;
103
+ bufferedEvents: number;
104
+ totalEventsReceived: number;
105
+ lastEventAt: string | null;
106
+ error?: string;
107
+ webhookRegistration?: {
108
+ registered: boolean;
109
+ webhookId?: string;
110
+ error?: string;
111
+ };
112
+ }
113
+ export interface AdminSession {
114
+ sessionIdShort: string;
115
+ callerAlias: string;
116
+ createdAt: number;
117
+ lastActivity: number;
118
+ requestCount: number;
119
+ windowRequests: number;
120
+ windowStart: number;
121
+ }
122
+ /** A buffered ingestor event, as surfaced to the dashboard Logs viewer.
123
+ * This is the ingested payload from an external service (e.g. a Discord
124
+ * message) — never a drawlatch secret. */
125
+ export interface AdminEvent {
126
+ id: number;
127
+ idempotencyKey: string;
128
+ receivedAt: string;
129
+ receivedAtMs: number;
130
+ callerAlias: string;
131
+ source: string;
132
+ instanceId?: string;
133
+ eventType: string;
134
+ data: unknown;
135
+ }
136
+ export interface AdminSecret {
137
+ callerAlias: string;
138
+ connection: string;
139
+ name: string;
140
+ required: boolean;
141
+ present: boolean;
142
+ }
143
+ /**
144
+ * Legacy envelope from the standalone drawlatch-ui backend, which proxied to a
145
+ * separate daemon process and needed to surface an "offline" state when that
146
+ * process was unreachable. Retained as a type for the frontend's transition;
147
+ * same-origin calls in the merged daemon cannot refuse themselves.
148
+ */
149
+ export interface DaemonOfflineEnvelope {
150
+ daemon: 'down';
151
+ error: string;
152
+ }
153
+ //# sourceMappingURL=admin-types.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Response DTOs for the daemon's `/admin/*` API (served at `/api/admin/*`).
3
+ *
4
+ * This file is the single source of truth for these shapes. `src/remote/admin.ts`
5
+ * imports them to annotate its handler responses, and the frontend imports them
6
+ * (type-only) instead of maintaining a duplicate `shared` mirror.
7
+ *
8
+ * Type-only — compiles to nothing.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=admin-types.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Read-only admin HTTP API for the merged dashboard, served at `/api/admin/*`.
3
+ *
4
+ * Trust boundary: the scrypt password gate (the admin router is mounted behind
5
+ * `requireAuth` in server.ts, wired in Step 2). This replaces the old
6
+ * loopback-only posture, so the dashboard can be exposed on a host bind
7
+ * (DRAWLATCH_HOST=0.0.0.0) while still being password-protected.
8
+ *
9
+ * This file is the type authority for the admin DTOs — see `admin-types.ts`.
10
+ *
11
+ * SECURITY non-negotiables (mirrored in /admin.test.ts):
12
+ * - Never serializes Session.channel (AES keys) or ResolvedRoute.secrets.
13
+ * - Never returns process.env values; only key names. Secret presence is
14
+ * reported via `isSecretSetForCaller()` (booleans only).
15
+ * - Caller `env` mappings (e.g. "GITHUB_TOKEN": "${ACME_GITHUB_TOKEN}") are
16
+ * reduced to key-name lists — the value strings are NOT returned.
17
+ * - No mutations: every endpoint is GET.
18
+ * - No CORS.
19
+ */
20
+ import express from 'express';
21
+ import { type RemoteServerConfig } from '../shared/config.js';
22
+ import { type AdminMutationDeps } from './admin-mutations.js';
23
+ import type { IngestorManager } from './ingestors/index.js';
24
+ import type { SessionSnapshot } from './server.js';
25
+ export interface AdminRouterDeps extends AdminMutationDeps {
26
+ /** Sanitized session snapshot — see Session in server.ts. */
27
+ getSessionsSnapshot: () => SessionSnapshot[];
28
+ /** Late-bound so tests can swap the manager without rebuilding the router. */
29
+ ingestorManager: () => IngestorManager;
30
+ /** Late-bound so changes to disk config are picked up between requests. */
31
+ loadConfig: () => RemoteServerConfig;
32
+ version: string;
33
+ port: number;
34
+ startedAt: number;
35
+ }
36
+ export declare function createAdminRouter(deps: AdminRouterDeps): express.Router;
37
+ //# sourceMappingURL=admin.d.ts.map