@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.
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 +595 -186
  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 +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 +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,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