@wolpertingerlabs/drawlatch 1.0.0-alpha.3 → 1.0.0-alpha.31
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.
- package/CONNECTIONS.md +3 -0
- package/README.md +393 -465
- package/bin/drawlatch.js +1151 -49
- package/dist/auth/auth.d.ts +10 -0
- package/dist/auth/auth.js +146 -0
- package/dist/auth/env-writer.d.ts +6 -0
- package/dist/auth/env-writer.js +52 -0
- package/dist/auth/password.d.ts +8 -0
- package/dist/auth/password.js +28 -0
- package/dist/auth/sessions.d.ts +13 -0
- package/dist/auth/sessions.js +85 -0
- package/dist/cli/generate-keys.d.ts +4 -4
- package/dist/cli/generate-keys.js +30 -25
- package/dist/connections/{anthropic.json → ai/anthropic.json} +19 -1
- package/dist/connections/{devin.json → ai/devin.json} +7 -1
- package/dist/connections/{google-ai.json → ai/google-ai.json} +7 -1
- package/dist/connections/{openai.json → ai/openai.json} +7 -1
- package/dist/connections/{openrouter.json → ai/openrouter.json} +7 -1
- package/dist/connections/developer-tools/github.json +138 -0
- package/dist/connections/{hex.json → developer-tools/hex.json} +7 -1
- package/dist/connections/developer-tools/linear.json +75 -0
- package/dist/connections/{lichess.json → gaming/lichess.json} +7 -1
- package/dist/connections/messaging/agentmail.json +21 -0
- package/dist/connections/messaging/discord-bot.json +114 -0
- package/dist/connections/{discord-oauth.json → messaging/discord-oauth.json} +7 -1
- package/dist/connections/messaging/slack.json +75 -0
- package/dist/connections/messaging/telegram.json +66 -0
- package/dist/connections/{google.json → productivity/google.json} +7 -1
- package/dist/connections/productivity/notion.json +75 -0
- package/dist/connections/productivity/stripe.json +74 -0
- package/dist/connections/productivity/trello.json +113 -0
- package/dist/connections/{bluesky.json → social-media/bluesky.json} +41 -0
- package/dist/connections/social-media/mastodon.json +65 -0
- package/dist/connections/social-media/reddit.json +80 -0
- package/dist/connections/{twitch.json → social-media/twitch.json} +40 -0
- package/dist/connections/social-media/x.json +67 -0
- package/dist/mcp/server.js +544 -31
- package/dist/remote/admin-mutations.d.ts +43 -0
- package/dist/remote/admin-mutations.js +321 -0
- package/dist/remote/admin-types.d.ts +153 -0
- package/dist/remote/admin-types.js +11 -0
- package/dist/remote/admin.d.ts +37 -0
- package/dist/remote/admin.js +317 -0
- package/dist/remote/caller-bootstrap.d.ts +121 -0
- package/dist/remote/caller-bootstrap.js +270 -0
- package/dist/remote/ingestors/base-ingestor.d.ts +19 -3
- package/dist/remote/ingestors/base-ingestor.js +27 -6
- package/dist/remote/ingestors/discord/discord-gateway.d.ts +2 -2
- package/dist/remote/ingestors/discord/discord-gateway.js +29 -6
- package/dist/remote/ingestors/e2e/setup.d.ts +69 -0
- package/dist/remote/ingestors/e2e/setup.js +147 -0
- package/dist/remote/ingestors/manager.d.ts +110 -10
- package/dist/remote/ingestors/manager.js +449 -42
- package/dist/remote/ingestors/poll/poll-ingestor.d.ts +4 -2
- package/dist/remote/ingestors/poll/poll-ingestor.js +26 -5
- package/dist/remote/ingestors/registry.d.ts +2 -2
- package/dist/remote/ingestors/registry.js +2 -2
- package/dist/remote/ingestors/slack/socket-mode.d.ts +2 -2
- package/dist/remote/ingestors/slack/socket-mode.js +28 -6
- package/dist/remote/ingestors/types.d.ts +25 -0
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.d.ts +46 -7
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.js +115 -10
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +30 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +73 -2
- package/dist/remote/ingestors/webhook/lifecycle-types.d.ts +63 -0
- package/dist/remote/ingestors/webhook/lifecycle-types.js +12 -0
- package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.js +2 -2
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.d.ts +17 -5
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.js +32 -26
- package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.d.ts +46 -0
- package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.js +261 -0
- package/dist/remote/server.d.ts +25 -33
- package/dist/remote/server.js +595 -186
- package/dist/remote/tool-dispatch.d.ts +84 -0
- package/dist/remote/tool-dispatch.js +910 -0
- package/dist/remote/triggers/rule-engine.d.ts +40 -0
- package/dist/remote/triggers/rule-engine.js +228 -0
- package/dist/remote/triggers/types.d.ts +69 -0
- package/dist/remote/triggers/types.js +10 -0
- package/dist/remote/tunnel-state.d.ts +14 -0
- package/dist/remote/tunnel-state.js +20 -0
- package/dist/remote/tunnel.d.ts +13 -0
- package/dist/remote/tunnel.js +33 -0
- package/dist/shared/config.d.ts +104 -25
- package/dist/shared/config.js +48 -35
- package/dist/shared/connections.d.ts +21 -5
- package/dist/shared/connections.js +56 -14
- package/dist/shared/crypto/index.d.ts +1 -0
- package/dist/shared/crypto/index.js +1 -0
- package/dist/shared/crypto/key-manager.d.ts +81 -0
- package/dist/shared/crypto/key-manager.js +174 -0
- package/dist/shared/env-utils.d.ts +42 -0
- package/dist/shared/env-utils.js +150 -0
- package/dist/shared/listener-config.d.ts +157 -0
- package/dist/shared/listener-config.js +10 -0
- package/dist/shared/migrations.d.ts +40 -0
- package/dist/shared/migrations.js +122 -0
- package/dist/shared/protocol/caller-bundle-crypto.d.ts +37 -0
- package/dist/shared/protocol/caller-bundle-crypto.js +62 -0
- package/dist/shared/protocol/caller-bundle.d.ts +75 -0
- package/dist/shared/protocol/caller-bundle.js +24 -0
- package/dist/shared/protocol/handshake.js +14 -1
- package/dist/shared/protocol/index.d.ts +2 -0
- package/dist/shared/protocol/index.js +2 -0
- package/dist/shared/protocol/sync-client.d.ts +52 -0
- package/dist/shared/protocol/sync-client.js +99 -0
- package/dist/shared/protocol/sync.d.ts +71 -0
- package/dist/shared/protocol/sync.js +176 -0
- package/frontend/dist/assets/index-BdCSPSZK.js +282 -0
- package/frontend/dist/assets/index-BmK26bY2.css +1 -0
- package/frontend/dist/index.html +15 -0
- package/package.json +65 -13
- package/dist/connections/discord-bot.json +0 -24
- package/dist/connections/github.json +0 -25
- package/dist/connections/linear.json +0 -29
- package/dist/connections/mastodon.json +0 -25
- package/dist/connections/notion.json +0 -33
- package/dist/connections/reddit.json +0 -28
- package/dist/connections/slack.json +0 -23
- package/dist/connections/stripe.json +0 -25
- package/dist/connections/telegram.json +0 -26
- package/dist/connections/trello.json +0 -25
- package/dist/connections/x.json +0 -27
|
@@ -0,0 +1,317 @@
|
|
|
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 fs from 'node:fs';
|
|
22
|
+
import { getCallerKeysDir, getServerKeysDir, getEnvFilePath, getRemoteConfigPath, } from '../shared/config.js';
|
|
23
|
+
import { listConnectionTemplates, loadConnection } from '../shared/connections.js';
|
|
24
|
+
import { isSecretSetForCaller } from '../shared/env-utils.js';
|
|
25
|
+
import { callerFingerprint } from '../shared/crypto/key-manager.js';
|
|
26
|
+
import { getTunnelUrl } from './tunnel-state.js';
|
|
27
|
+
import { mountAdminMutations } from './admin-mutations.js';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
export function createAdminRouter(deps) {
|
|
30
|
+
const router = express.Router();
|
|
31
|
+
// ── /admin/meta ────────────────────────────────────────────────────────
|
|
32
|
+
router.get('/meta', (_req, res) => {
|
|
33
|
+
const body = {
|
|
34
|
+
version: deps.version,
|
|
35
|
+
port: deps.port,
|
|
36
|
+
pid: process.pid,
|
|
37
|
+
startedAt: deps.startedAt,
|
|
38
|
+
uptimeSec: Math.floor((Date.now() - deps.startedAt) / 1000),
|
|
39
|
+
configPath: getRemoteConfigPath(),
|
|
40
|
+
callerKeysDir: getCallerKeysDir(),
|
|
41
|
+
serverKeysDir: getServerKeysDir(),
|
|
42
|
+
envFilePath: getEnvFilePath(),
|
|
43
|
+
tunnelUrl: getTunnelUrl(),
|
|
44
|
+
tunnelEnabled: deps.loadConfig().tunnel === true,
|
|
45
|
+
};
|
|
46
|
+
res.json(body);
|
|
47
|
+
});
|
|
48
|
+
// ── /admin/health ──────────────────────────────────────────────────────
|
|
49
|
+
router.get('/health', (_req, res) => {
|
|
50
|
+
const statuses = deps.ingestorManager().getAllStatuses();
|
|
51
|
+
const counts = { connected: 0, error: 0, starting: 0, stopped: 0 };
|
|
52
|
+
for (const s of statuses) {
|
|
53
|
+
if (s.state === 'connected')
|
|
54
|
+
counts.connected++;
|
|
55
|
+
else if (s.state === 'error')
|
|
56
|
+
counts.error++;
|
|
57
|
+
else if (s.state === 'starting' || s.state === 'reconnecting')
|
|
58
|
+
counts.starting++;
|
|
59
|
+
else
|
|
60
|
+
counts.stopped++;
|
|
61
|
+
}
|
|
62
|
+
const body = {
|
|
63
|
+
status: 'ok',
|
|
64
|
+
activeSessions: deps.getSessionsSnapshot().length,
|
|
65
|
+
ingestorCounts: counts,
|
|
66
|
+
uptimeSec: Math.floor((Date.now() - deps.startedAt) / 1000),
|
|
67
|
+
};
|
|
68
|
+
res.json(body);
|
|
69
|
+
});
|
|
70
|
+
// ── /admin/callers ─────────────────────────────────────────────────────
|
|
71
|
+
// Returns caller config metadata only — env values are reduced to key names.
|
|
72
|
+
router.get('/callers', (_req, res) => {
|
|
73
|
+
const config = deps.loadConfig();
|
|
74
|
+
const callersKeysDir = getCallerKeysDir();
|
|
75
|
+
const out = Object.entries(config.callers).map(([alias, caller]) => {
|
|
76
|
+
const keysDirExists = fs.existsSync(path.join(callersKeysDir, alias));
|
|
77
|
+
let fingerprint = null;
|
|
78
|
+
try {
|
|
79
|
+
fingerprint = keysDirExists ? callerFingerprint(alias) : null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
fingerprint = null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
alias,
|
|
86
|
+
name: caller.name ?? null,
|
|
87
|
+
connections: caller.connections,
|
|
88
|
+
// Key names ONLY — never the mapping value strings.
|
|
89
|
+
envKeys: Object.keys(caller.env ?? {}),
|
|
90
|
+
fingerprint,
|
|
91
|
+
keysDirExists,
|
|
92
|
+
source: caller.source ?? null,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
res.json(out);
|
|
96
|
+
});
|
|
97
|
+
// ── /admin/connections ─────────────────────────────────────────────────
|
|
98
|
+
// listConnectionTemplates already returns names-only — safe as-is.
|
|
99
|
+
router.get('/connections', (_req, res) => {
|
|
100
|
+
const out = listConnectionTemplates();
|
|
101
|
+
res.json(out);
|
|
102
|
+
});
|
|
103
|
+
// ── /admin/callers/:alias/connection-status ─────────────────────────────
|
|
104
|
+
// Every connection template + this caller's enabled flag + secret presence
|
|
105
|
+
// (booleans only). The Connections page renders from this single payload.
|
|
106
|
+
router.get('/callers/:alias/connection-status', (req, res) => {
|
|
107
|
+
const config = deps.loadConfig();
|
|
108
|
+
if (!(req.params.alias in config.callers)) {
|
|
109
|
+
res.status(404).json({ error: `Unknown caller: ${req.params.alias}` });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const caller = config.callers[req.params.alias];
|
|
113
|
+
const enabled = new Set(caller.connections);
|
|
114
|
+
const out = listConnectionTemplates().map((t) => {
|
|
115
|
+
const requiredSecretsSet = {};
|
|
116
|
+
for (const s of t.requiredSecrets) {
|
|
117
|
+
requiredSecretsSet[s] = isSecretSetForCaller(s, req.params.alias, caller.env);
|
|
118
|
+
}
|
|
119
|
+
const optionalSecretsSet = {};
|
|
120
|
+
for (const s of t.optionalSecrets) {
|
|
121
|
+
optionalSecretsSet[s] = isSecretSetForCaller(s, req.params.alias, caller.env);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...t,
|
|
125
|
+
enabled: enabled.has(t.alias),
|
|
126
|
+
requiredSecretsSet,
|
|
127
|
+
optionalSecretsSet,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
res.json(out);
|
|
131
|
+
});
|
|
132
|
+
// ── /admin/callers/:alias/connections ──────────────────────────────────
|
|
133
|
+
router.get('/callers/:alias/connections', (req, res) => {
|
|
134
|
+
const config = deps.loadConfig();
|
|
135
|
+
if (!(req.params.alias in config.callers)) {
|
|
136
|
+
res.status(404).json({ error: `Unknown caller: ${req.params.alias}` });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const caller = config.callers[req.params.alias];
|
|
140
|
+
const templates = listConnectionTemplates();
|
|
141
|
+
const tplMap = new Map(templates.map((t) => [t.alias, t]));
|
|
142
|
+
const customMap = new Map((config.connectors ?? []).map((c) => [c.alias, c]));
|
|
143
|
+
const out = caller.connections.map((connectionAlias) => {
|
|
144
|
+
const tpl = tplMap.get(connectionAlias);
|
|
145
|
+
const customRoute = customMap.get(connectionAlias);
|
|
146
|
+
const isCustom = !tpl;
|
|
147
|
+
const requiredNames = tpl?.requiredSecrets ?? [];
|
|
148
|
+
const optionalNames = tpl?.optionalSecrets ?? [];
|
|
149
|
+
const requiredSecrets = requiredNames.map((name) => ({
|
|
150
|
+
name,
|
|
151
|
+
present: isSecretSetForCaller(name, req.params.alias, caller.env),
|
|
152
|
+
}));
|
|
153
|
+
const optionalSecrets = optionalNames.map((name) => ({
|
|
154
|
+
name,
|
|
155
|
+
present: isSecretSetForCaller(name, req.params.alias, caller.env),
|
|
156
|
+
}));
|
|
157
|
+
// Resolve listenerConfig so we can identify ListenerConfigField.type === 'secret'
|
|
158
|
+
// fields and strip them from the params projection. Without this, a future
|
|
159
|
+
// template that declares a 'secret' field would silently leak the value via
|
|
160
|
+
// the ...ov.params spread below.
|
|
161
|
+
let listenerConfig;
|
|
162
|
+
if (customRoute) {
|
|
163
|
+
listenerConfig = customRoute.listenerConfig;
|
|
164
|
+
}
|
|
165
|
+
else if (tpl) {
|
|
166
|
+
try {
|
|
167
|
+
listenerConfig = loadConnection(connectionAlias).listenerConfig;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
listenerConfig = undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const secretFieldKeys = new Set((listenerConfig?.fields ?? []).filter((f) => f.type === 'secret').map((f) => f.key));
|
|
174
|
+
// Multi-instance projection — drop anything that could carry secrets.
|
|
175
|
+
const instancesMap = caller.listenerInstances?.[connectionAlias] ?? {};
|
|
176
|
+
const instances = Object.entries(instancesMap).map(([instanceId, ov]) => {
|
|
177
|
+
const safeParams = ov.params !== undefined
|
|
178
|
+
? Object.fromEntries(Object.entries(ov.params).filter(([k]) => !secretFieldKeys.has(k)))
|
|
179
|
+
: undefined;
|
|
180
|
+
return {
|
|
181
|
+
instanceId,
|
|
182
|
+
enabled: ov.disabled !== true,
|
|
183
|
+
params: {
|
|
184
|
+
...(ov.intents !== undefined && { intents: ov.intents }),
|
|
185
|
+
...(ov.eventFilter !== undefined && { eventFilter: ov.eventFilter }),
|
|
186
|
+
...(ov.guildIds !== undefined && { guildIds: ov.guildIds }),
|
|
187
|
+
...(ov.channelIds !== undefined && { channelIds: ov.channelIds }),
|
|
188
|
+
...(ov.userIds !== undefined && { userIds: ov.userIds }),
|
|
189
|
+
...(ov.bufferSize !== undefined && { bufferSize: ov.bufferSize }),
|
|
190
|
+
...(ov.intervalMs !== undefined && { intervalMs: ov.intervalMs }),
|
|
191
|
+
...(ov.disabled !== undefined && { disabled: ov.disabled }),
|
|
192
|
+
// Listener params (board IDs, subreddit names, etc.) are user-set
|
|
193
|
+
// configuration, not secrets. Pass through verbatim — except for any
|
|
194
|
+
// field whose schema marks it as type: 'secret', which is filtered above.
|
|
195
|
+
...(safeParams !== undefined && safeParams),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
// `enabled`: a connection is enabled by default; explicit disabled
|
|
200
|
+
// override (single-instance) toggles it off.
|
|
201
|
+
const overrideDisabled = caller.ingestorOverrides?.[connectionAlias]?.disabled === true;
|
|
202
|
+
return {
|
|
203
|
+
connectionAlias,
|
|
204
|
+
enabled: !overrideDisabled,
|
|
205
|
+
isCustom,
|
|
206
|
+
requiredSecrets,
|
|
207
|
+
optionalSecrets,
|
|
208
|
+
hasIngestor: tpl?.hasIngestor ?? customMap.get(connectionAlias)?.ingestor !== undefined,
|
|
209
|
+
instances,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
res.json(out);
|
|
213
|
+
});
|
|
214
|
+
// ── /admin/ingestors ───────────────────────────────────────────────────
|
|
215
|
+
router.get('/ingestors', (_req, res) => {
|
|
216
|
+
const statuses = deps.ingestorManager().getAllStatuses();
|
|
217
|
+
// getAllStatuses already augments with callerAlias; connection and
|
|
218
|
+
// instanceId come from base IngestorStatus.
|
|
219
|
+
const out = statuses.map((s) => ({
|
|
220
|
+
callerAlias: s.callerAlias,
|
|
221
|
+
connection: s.connection,
|
|
222
|
+
...(s.instanceId !== undefined && { instanceId: s.instanceId }),
|
|
223
|
+
type: s.type,
|
|
224
|
+
state: s.state,
|
|
225
|
+
bufferedEvents: s.bufferedEvents,
|
|
226
|
+
totalEventsReceived: s.totalEventsReceived,
|
|
227
|
+
lastEventAt: s.lastEventAt,
|
|
228
|
+
...(s.error !== undefined && { error: s.error }),
|
|
229
|
+
...(s.webhookRegistration !== undefined && {
|
|
230
|
+
webhookRegistration: s.webhookRegistration,
|
|
231
|
+
}),
|
|
232
|
+
}));
|
|
233
|
+
res.json(out);
|
|
234
|
+
});
|
|
235
|
+
// ── /admin/callers/:alias/ingestors ──────────────────────────────────────
|
|
236
|
+
// Per-caller ingestor statuses (the Logs viewer's status cards).
|
|
237
|
+
router.get('/callers/:alias/ingestors', (req, res) => {
|
|
238
|
+
if (!(req.params.alias in deps.loadConfig().callers)) {
|
|
239
|
+
res.status(404).json({ error: `Unknown caller: ${req.params.alias}` });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const statuses = deps.ingestorManager().getStatuses(req.params.alias);
|
|
243
|
+
const out = statuses.map((s) => ({
|
|
244
|
+
callerAlias: req.params.alias,
|
|
245
|
+
connection: s.connection,
|
|
246
|
+
...(s.instanceId !== undefined && { instanceId: s.instanceId }),
|
|
247
|
+
type: s.type,
|
|
248
|
+
state: s.state,
|
|
249
|
+
bufferedEvents: s.bufferedEvents,
|
|
250
|
+
totalEventsReceived: s.totalEventsReceived,
|
|
251
|
+
lastEventAt: s.lastEventAt,
|
|
252
|
+
...(s.error !== undefined && { error: s.error }),
|
|
253
|
+
...(s.webhookRegistration !== undefined && { webhookRegistration: s.webhookRegistration }),
|
|
254
|
+
}));
|
|
255
|
+
res.json(out);
|
|
256
|
+
});
|
|
257
|
+
// ── /admin/callers/:alias/events ──────────────────────────────────────────
|
|
258
|
+
// Buffered ingestor events for the Logs viewer. These are external-service
|
|
259
|
+
// payloads (Discord messages, webhooks, …), never drawlatch secrets.
|
|
260
|
+
router.get('/callers/:alias/events', (req, res) => {
|
|
261
|
+
if (!(req.params.alias in deps.loadConfig().callers)) {
|
|
262
|
+
res.status(404).json({ error: `Unknown caller: ${req.params.alias}` });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const afterRaw = req.query.after_id;
|
|
266
|
+
const afterId = typeof afterRaw === 'string' ? parseInt(afterRaw, 10) : -1;
|
|
267
|
+
const events = deps
|
|
268
|
+
.ingestorManager()
|
|
269
|
+
.getAllEvents(req.params.alias, Number.isNaN(afterId) ? -1 : afterId);
|
|
270
|
+
res.json(events);
|
|
271
|
+
});
|
|
272
|
+
// ── /admin/sessions ────────────────────────────────────────────────────
|
|
273
|
+
// SessionSnapshot already strips channel + resolvedRoutes — pass it through.
|
|
274
|
+
router.get('/sessions', (_req, res) => {
|
|
275
|
+
res.json(deps.getSessionsSnapshot());
|
|
276
|
+
});
|
|
277
|
+
// ── /admin/secrets ─────────────────────────────────────────────────────
|
|
278
|
+
// Flat join of (caller × connection × secret), with `present` computed via
|
|
279
|
+
// isSecretSetForCaller() — never the actual env value.
|
|
280
|
+
router.get('/secrets', (_req, res) => {
|
|
281
|
+
const config = deps.loadConfig();
|
|
282
|
+
const tplMap = new Map(listConnectionTemplates().map((t) => [t.alias, t]));
|
|
283
|
+
const out = [];
|
|
284
|
+
for (const [callerAlias, caller] of Object.entries(config.callers)) {
|
|
285
|
+
for (const connectionAlias of caller.connections) {
|
|
286
|
+
const tpl = tplMap.get(connectionAlias);
|
|
287
|
+
if (!tpl)
|
|
288
|
+
continue; // custom connector — no template-defined secrets to enumerate
|
|
289
|
+
for (const name of tpl.requiredSecrets) {
|
|
290
|
+
out.push({
|
|
291
|
+
callerAlias,
|
|
292
|
+
connection: connectionAlias,
|
|
293
|
+
name,
|
|
294
|
+
required: true,
|
|
295
|
+
present: isSecretSetForCaller(name, callerAlias, caller.env),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
for (const name of tpl.optionalSecrets) {
|
|
299
|
+
out.push({
|
|
300
|
+
callerAlias,
|
|
301
|
+
connection: connectionAlias,
|
|
302
|
+
name,
|
|
303
|
+
required: false,
|
|
304
|
+
present: isSecretSetForCaller(name, callerAlias, caller.env),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
res.json(out);
|
|
310
|
+
});
|
|
311
|
+
// ── Mutating endpoints (item A) ──────────────────────────────────────────
|
|
312
|
+
// Connections enable/secrets/test, caller create/delete, and listener/ingestor
|
|
313
|
+
// control. Mounted on the same router (already behind requireAuth in server.ts).
|
|
314
|
+
mountAdminMutations(router, deps);
|
|
315
|
+
return router;
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=admin.js.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caller provisioning — drawlatch is the sole issuer of caller key material.
|
|
3
|
+
*
|
|
4
|
+
* One primitive, two delivery modes (see plans/caller-credential-issuance.md):
|
|
5
|
+
* - `issueCallerBundle()` — mint a keypair IN MEMORY, persist only the public
|
|
6
|
+
* half, and return the credential bundle (download via the admin API, or the
|
|
7
|
+
* `drawlatch issue-caller` CLI). The caller PRIVATE key never touches
|
|
8
|
+
* drawlatch disk; it lives only inside the returned bundle.
|
|
9
|
+
* - `issueLocalCaller()` — same primitive, but write the UNPACKED key files
|
|
10
|
+
* straight into a co-located callboard's keys dir over the shared filesystem.
|
|
11
|
+
* The same-host write IS the trust proof (replaces the old enroll-token dance).
|
|
12
|
+
*
|
|
13
|
+
* `createCallerWithKeys()` (priv+pub on drawlatch disk) is retained for the
|
|
14
|
+
* password-gated `POST /api/admin/callers` create endpoint and tests.
|
|
15
|
+
*/
|
|
16
|
+
import { type RemoteServerConfig } from '../shared/config.js';
|
|
17
|
+
import { type SerializedPublicKeys } from '../shared/crypto/keys.js';
|
|
18
|
+
import type { CallerBundleV1 } from '../shared/protocol/caller-bundle.js';
|
|
19
|
+
/** Valid caller alias: starts with a letter/number, then letters/numbers/-/_. */
|
|
20
|
+
export declare const CALLER_ALIAS_REGEX: RegExp;
|
|
21
|
+
export interface CreateCallerOptions {
|
|
22
|
+
/** Human-readable display name (defaults to the alias). */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** Connection aliases to grant. When omitted, clones the `default` caller's
|
|
25
|
+
* connections so a new caller starts with the same access (common expectation). */
|
|
26
|
+
connections?: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface CreateCallerResult {
|
|
29
|
+
alias: string;
|
|
30
|
+
name: string;
|
|
31
|
+
fingerprint: string;
|
|
32
|
+
/** The caller's freshly-generated public keys (private keys stay on disk). */
|
|
33
|
+
publicKeys: SerializedPublicKeys;
|
|
34
|
+
/** Directory holding the caller's keypair (keys/callers/<alias>/). */
|
|
35
|
+
keysDir: string;
|
|
36
|
+
connections: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a caller WITH a fresh keypair on drawlatch disk (priv+pub), registered
|
|
40
|
+
* in remote.config.json. Used by the dashboard "New caller" action.
|
|
41
|
+
*
|
|
42
|
+
* Throws on invalid alias, or if the caller (config entry or key dir) exists.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createCallerWithKeys(alias: string, opts?: CreateCallerOptions): CreateCallerResult;
|
|
45
|
+
export interface IssueCallerOptions {
|
|
46
|
+
alias: string;
|
|
47
|
+
/** Human-readable display name (preserved on re-issue; defaults to alias). */
|
|
48
|
+
name?: string;
|
|
49
|
+
/** Connections to authorize. Preserved on re-issue; clones `default` for new. */
|
|
50
|
+
connections?: string[];
|
|
51
|
+
/** drawlatch endpoint this caller identity is scoped to (pinned in the bundle). */
|
|
52
|
+
endpointUrl: string;
|
|
53
|
+
/** When set, `caller.*.priv` is scrypt+AES-256-GCM wrapped in the bundle. */
|
|
54
|
+
passphrase?: string;
|
|
55
|
+
/** ISO timestamp override (tests). Defaults to now. */
|
|
56
|
+
createdAt?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Issue (or re-issue / rotate) a caller credential bundle.
|
|
60
|
+
*
|
|
61
|
+
* 1. Generate the Ed25519 + X25519 keypair IN MEMORY.
|
|
62
|
+
* 2. Register/update the caller in remote.config.json (connections, name,
|
|
63
|
+
* source='bundle-issued').
|
|
64
|
+
* 3. Persist ONLY the public keys to keys/callers/<alias>/ (the private key
|
|
65
|
+
* never touches drawlatch disk).
|
|
66
|
+
* 4. Assemble the v1 bundle (caller priv+pub, server pub-only, endpoint,
|
|
67
|
+
* fingerprints, connections), optionally passphrase-wrapping the two
|
|
68
|
+
* private PEMs.
|
|
69
|
+
* 5. Zero the derived key material from memory and return the bundle.
|
|
70
|
+
*
|
|
71
|
+
* Re-issuing an existing caller mints a FRESH keypair and overwrites the stored
|
|
72
|
+
* public key — invalidating the prior credential (the old private key can no
|
|
73
|
+
* longer authenticate). The bundle is returned once; drawlatch keeps no copy of
|
|
74
|
+
* the private material, so it is unrecoverable afterward.
|
|
75
|
+
*/
|
|
76
|
+
export declare function issueCallerBundle(opts: IssueCallerOptions): CallerBundleV1;
|
|
77
|
+
export interface IssueLocalCallerOptions {
|
|
78
|
+
/** Caller alias (default 'callboard-local'). */
|
|
79
|
+
alias?: string;
|
|
80
|
+
/** Human-readable display name. */
|
|
81
|
+
name?: string;
|
|
82
|
+
/** Connections to authorize (defaults to cloning `default`). */
|
|
83
|
+
connections?: string[];
|
|
84
|
+
/** callboard's keys dir (the directory that holds `callers/` and `server/`). */
|
|
85
|
+
keysDir: string;
|
|
86
|
+
}
|
|
87
|
+
export interface IssueLocalCallerResult {
|
|
88
|
+
alias: string;
|
|
89
|
+
fingerprint: string;
|
|
90
|
+
/** The caller key directory written inside callboard's keys dir. */
|
|
91
|
+
callerKeysDir: string;
|
|
92
|
+
connections: string[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Issue a caller and write the UNPACKED key files straight into a co-located
|
|
96
|
+
* callboard's keys dir over the shared filesystem (same-host write is the trust
|
|
97
|
+
* proof). drawlatch keeps only the public key; callboard gets the private key on
|
|
98
|
+
* its own disk — bit-for-bit the steady state of the old pairing flow, with no
|
|
99
|
+
* token dance.
|
|
100
|
+
*
|
|
101
|
+
* Writes, under `<keysDir>`:
|
|
102
|
+
* - callers/<alias>/{signing,exchange}.{key,pub}.pem (0600 / 0644)
|
|
103
|
+
* - server/{signing,exchange}.pub.pem (0644)
|
|
104
|
+
*/
|
|
105
|
+
export declare function issueLocalCaller(opts: IssueLocalCallerOptions): IssueLocalCallerResult;
|
|
106
|
+
/**
|
|
107
|
+
* First-boot hook: when drawlatch is supervised by a co-located callboard (which
|
|
108
|
+
* sets DRAWLATCH_LOCAL_CALLER_KEYS_DIR to its own keys dir), auto-share a default
|
|
109
|
+
* caller into that dir — but only once (skipped if the caller already exists).
|
|
110
|
+
*
|
|
111
|
+
* Replaces the retired enroll-token / `/sync/auto-enroll` path.
|
|
112
|
+
*/
|
|
113
|
+
export declare function maybeIssueLocalCaller(): IssueLocalCallerResult | null;
|
|
114
|
+
/**
|
|
115
|
+
* Delete a caller: removes its config entry, prefixed env vars, and key dir.
|
|
116
|
+
* The `default` caller cannot be deleted.
|
|
117
|
+
*/
|
|
118
|
+
export declare function deleteCaller(alias: string): void;
|
|
119
|
+
/** Re-exported for callers that want to introspect config without re-importing. */
|
|
120
|
+
export type { RemoteServerConfig };
|
|
121
|
+
//# sourceMappingURL=caller-bootstrap.d.ts.map
|