@wolpertingerlabs/drawlatch 1.0.0-alpha.29 → 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/bin/drawlatch.js CHANGED
@@ -83,6 +83,12 @@ try {
83
83
  full: { type: "boolean", default: false },
84
84
  requests: { type: "boolean", default: false },
85
85
  ttl: { type: "string", default: "300" },
86
+ name: { type: "string" },
87
+ connections: { type: "string" },
88
+ endpoint: { type: "string" },
89
+ passphrase: { type: "boolean", default: false },
90
+ output: { type: "string", short: "o" },
91
+ into: { type: "string" },
86
92
  },
87
93
  strict: false,
88
94
  allowPositionals: true,
@@ -179,7 +185,16 @@ switch (subcommand) {
179
185
  await cmdDoctor();
180
186
  }
181
187
  break;
188
+ case "issue-caller":
189
+ if (values.help) {
190
+ printIssueCallerHelp();
191
+ } else {
192
+ await cmdIssueCaller();
193
+ }
194
+ break;
182
195
  case "sync":
196
+ // Parked: kept in-tree as a high-assurance escape hatch, undocumented in
197
+ // the main help (superseded by `issue-caller`).
183
198
  if (values.help) {
184
199
  printSyncHelp();
185
200
  } else {
@@ -307,11 +322,13 @@ async function cmdInit() {
307
322
  }
308
323
 
309
324
  console.log(`\nSetup complete! Next steps:\n`);
310
- console.log(` 1. Start the remote server:`);
325
+ console.log(` 1. Set the dashboard password:`);
326
+ console.log(` drawlatch set-password\n`);
327
+ console.log(` 2. Start the remote server:`);
311
328
  console.log(` drawlatch start\n`);
312
- console.log(` 2. Add callers via key sync:`);
313
- console.log(` drawlatch sync\n`);
314
- console.log(` 3. Verify your setup:`);
329
+ console.log(` 3. Issue a caller credential bundle (or use the dashboard Callers page):`);
330
+ console.log(` drawlatch issue-caller callboard-prod -o callboard-prod.drawlatch-caller.json\n`);
331
+ console.log(` 4. Verify your setup:`);
315
332
  console.log(` drawlatch doctor\n`);
316
333
  }
317
334
 
@@ -976,6 +993,133 @@ async function cmdSync() {
976
993
  process.exit(1);
977
994
  }
978
995
 
996
+ // ── issue-caller ──────────────────────────────────────────────────────────
997
+ //
998
+ // Mint a caller credential bundle using the same primitive as the dashboard
999
+ // "Issue credentials" action. Two delivery modes:
1000
+ // - default / -o file : emit the {alias}.drawlatch-caller.json bundle (the
1001
+ // caller PRIVATE key is in the file; drawlatch keeps only the public key).
1002
+ // - --into <keysDir> : write the UNPACKED key files straight into a
1003
+ // co-located callboard's keys dir (same-host convenience).
1004
+ async function cmdIssueCaller() {
1005
+ const alias = positionals[0];
1006
+ if (!alias) {
1007
+ console.error("Error: caller alias is required.\n Usage: drawlatch issue-caller <alias> [options]");
1008
+ process.exit(1);
1009
+ }
1010
+
1011
+ ensureConfigDir();
1012
+
1013
+ const { issueCallerBundle, issueLocalCaller, CALLER_ALIAS_REGEX } = await import(
1014
+ join(PKG_ROOT, "dist/remote/caller-bootstrap.js")
1015
+ );
1016
+
1017
+ if (!CALLER_ALIAS_REGEX.test(alias)) {
1018
+ console.error(
1019
+ "Error: invalid alias. Use letters, numbers, dashes or underscores (must start alphanumeric).",
1020
+ );
1021
+ process.exit(1);
1022
+ }
1023
+
1024
+ const connections = values.connections
1025
+ ? values.connections.split(",").map((s) => s.trim()).filter(Boolean)
1026
+ : undefined;
1027
+ const name = values.name;
1028
+
1029
+ try {
1030
+ // ── Mode 2: write unpacked key files directly into callboard's keys dir ──
1031
+ if (values.into) {
1032
+ const keysDir = resolve(values.into);
1033
+ const res = issueLocalCaller({
1034
+ alias,
1035
+ keysDir,
1036
+ ...(connections ? { connections } : {}),
1037
+ ...(name ? { name } : {}),
1038
+ });
1039
+ console.log(`\nIssued local caller "${res.alias}" (write-to-path).`);
1040
+ console.log(` Key files: ${res.callerKeysDir}/`);
1041
+ console.log(` Server keys: ${join(keysDir, "server")}/`);
1042
+ console.log(` Fingerprint: ${res.fingerprint}`);
1043
+ console.log(` Connections: ${res.connections.join(", ") || "(none)"}`);
1044
+ notifyDaemonReloadNeeded();
1045
+ return;
1046
+ }
1047
+
1048
+ // ── Mode 1: emit a bundle (file or stdout) ──────────────────────────────
1049
+ const config = loadRemoteConfig();
1050
+ const endpoint = values.endpoint || `http://${connectHost(config.host)}:${config.port}`;
1051
+
1052
+ let passphrase;
1053
+ if (values.passphrase) {
1054
+ let confirm;
1055
+ if (process.stdin.isTTY) {
1056
+ passphrase = await promptPasswordTTY("Passphrase to protect private keys: ");
1057
+ confirm = await promptPasswordTTY("Confirm passphrase: ");
1058
+ } else {
1059
+ // Piped input — read both lines from one readline interface.
1060
+ const { createInterface } = await import("node:readline");
1061
+ const rl = createInterface({ input: process.stdin });
1062
+ [passphrase, confirm] = await readTwoLines(rl);
1063
+ rl.close();
1064
+ }
1065
+ if (!passphrase) {
1066
+ console.error("Error: passphrase cannot be empty.");
1067
+ process.exit(1);
1068
+ }
1069
+ if (passphrase !== confirm) {
1070
+ console.error("Error: passphrases do not match.");
1071
+ process.exit(1);
1072
+ }
1073
+ }
1074
+
1075
+ const bundle = issueCallerBundle({
1076
+ alias,
1077
+ endpointUrl: endpoint,
1078
+ ...(connections ? { connections } : {}),
1079
+ ...(name ? { name } : {}),
1080
+ ...(passphrase ? { passphrase } : {}),
1081
+ });
1082
+
1083
+ const json = JSON.stringify(bundle, null, 2) + "\n";
1084
+
1085
+ if (values.output) {
1086
+ const outPath = resolve(values.output);
1087
+ writeFileSync(outPath, json, { mode: 0o600 });
1088
+ console.error(`\nWrote credential bundle to ${outPath}`);
1089
+ console.error(` Caller: ${bundle.callerAlias}`);
1090
+ console.error(` Fingerprint: ${bundle.fingerprint}`);
1091
+ console.error(` Endpoint: ${bundle.endpointUrl}`);
1092
+ console.error(
1093
+ bundle.encryption
1094
+ ? ` Private keys: passphrase-protected (share the passphrase out-of-band).`
1095
+ : ` Private keys: PLAINTEXT in the file — protect it (or use --passphrase).`,
1096
+ );
1097
+ console.error(` This file contains the caller PRIVATE key and won't be re-issued.`);
1098
+ notifyDaemonReloadNeeded();
1099
+ } else {
1100
+ // Bundle (with private keys) to stdout; notes to stderr.
1101
+ process.stdout.write(json);
1102
+ console.error(`\n# Caller "${bundle.callerAlias}" issued (fingerprint ${bundle.fingerprint}).`);
1103
+ if (!bundle.encryption) {
1104
+ console.error(`# WARNING: private keys are PLAINTEXT above. Use --passphrase or -o to a 0600 file.`);
1105
+ }
1106
+ notifyDaemonReloadNeeded();
1107
+ }
1108
+ } catch (err) {
1109
+ console.error(`Error: ${err.message}`);
1110
+ process.exit(1);
1111
+ }
1112
+ }
1113
+
1114
+ /** If the daemon is running, it won't authorize a freshly-issued caller until
1115
+ * it reloads its peer set — tell the user to restart it. (The dashboard issue
1116
+ * flow live-reloads peers; the offline CLI path cannot.) */
1117
+ function notifyDaemonReloadNeeded() {
1118
+ if (readPid()) {
1119
+ console.error(`\nNote: restart the daemon so it authorizes the new caller — drawlatch restart`);
1120
+ }
1121
+ }
1122
+
979
1123
  async function promptInput(prompt) {
980
1124
  const { createInterface } = await import("node:readline");
981
1125
  return new Promise((resolve) => {
@@ -1389,7 +1533,7 @@ Commands:
1389
1533
  watch Watch ingestor events in real time
1390
1534
  doctor Validate setup and diagnose issues
1391
1535
  generate-keys Generate Ed25519 + X25519 keypairs
1392
- sync Exchange keys with a callboard instance
1536
+ issue-caller Issue a caller credential bundle (for a callboard instance)
1393
1537
  set-password Set/change the dashboard password (alias: change-password)
1394
1538
 
1395
1539
  Options:
@@ -1542,8 +1686,8 @@ drawlatch init
1542
1686
  Set up the drawlatch remote server. Generates server keys, creates
1543
1687
  config files, and scaffolds a .env template.
1544
1688
 
1545
- Callers are added separately via 'drawlatch sync' after the server
1546
- is running.
1689
+ Callers are issued separately via 'drawlatch issue-caller' (or the dashboard
1690
+ Callers page) after the server is running.
1547
1691
 
1548
1692
  Usage: drawlatch init [options]
1549
1693
 
@@ -1595,6 +1739,36 @@ The server must be running (drawlatch start) before using this command.
1595
1739
  `);
1596
1740
  }
1597
1741
 
1742
+ function printIssueCallerHelp() {
1743
+ console.log(`
1744
+ drawlatch issue-caller
1745
+
1746
+ Issue a caller credential bundle that a callboard instance imports to gain a
1747
+ caller identity. drawlatch mints the keypair, keeps only the PUBLIC key, and
1748
+ hands the private key out in the bundle (shown once — re-issue to rotate).
1749
+
1750
+ Usage: drawlatch issue-caller <alias> [options]
1751
+
1752
+ Options:
1753
+ --name <name> Human-readable display name (defaults to the alias)
1754
+ --connections <a,b,c> Comma-separated connections to authorize
1755
+ (defaults to cloning the "default" caller)
1756
+ --endpoint <url> Endpoint URL to pin in the bundle
1757
+ (defaults to this server's host:port)
1758
+ --passphrase Prompt for a passphrase and scrypt+AES-256-GCM wrap
1759
+ the private keys in the bundle (for untrusted transport)
1760
+ -o, --output <file> Write the bundle to a file (0600) instead of stdout
1761
+ --into <keysDir> Same-host: write the UNPACKED key files directly into a
1762
+ co-located callboard's keys dir (no bundle file)
1763
+ -h, --help Show this help message
1764
+
1765
+ Examples:
1766
+ drawlatch issue-caller callboard-prod -o callboard-prod.drawlatch-caller.json
1767
+ drawlatch issue-caller callboard-prod --passphrase -o bundle.json
1768
+ drawlatch issue-caller callboard-local --into ~/.callboard/keys
1769
+ `);
1770
+ }
1771
+
1598
1772
  function printSetPasswordHelp(cmd = "set-password") {
1599
1773
  console.log(`
1600
1774
  drawlatch ${cmd}
@@ -26,6 +26,12 @@ export interface AdminMutationDeps {
26
26
  reloadPeer: (alias: string) => void;
27
27
  /** Drop the authorized peer + active sessions for a deleted caller. */
28
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;
29
35
  }
30
36
  /**
31
37
  * Mount the mutating routes onto an existing admin router.
@@ -12,9 +12,10 @@
12
12
  * - After any config mutation, the daemon's live-reload path runs so ingestors
13
13
  * and routes pick up the change (replacing callboard's restart banner).
14
14
  */
15
+ import rateLimit from 'express-rate-limit';
15
16
  import { saveRemoteConfig } from '../shared/config.js';
16
17
  import { dispatchTool } from './tool-dispatch.js';
17
- import { createCallerWithKeys, deleteCaller, CALLER_ALIAS_REGEX } from './caller-bootstrap.js';
18
+ import { createCallerWithKeys, deleteCaller, issueCallerBundle, CALLER_ALIAS_REGEX, } from './caller-bootstrap.js';
18
19
  /** Translate a thrown error into an HTTP status + message. */
19
20
  function errorStatus(message) {
20
21
  if (/already exists/i.test(message))
@@ -93,6 +94,75 @@ export function mountAdminMutations(router, deps) {
93
94
  res.status(errorStatus(message)).json({ error: message });
94
95
  }
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
+ });
96
166
  // DELETE /api/admin/callers/:alias
97
167
  router.delete('/callers/:alias', (req, res) => {
98
168
  try {
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * Type-only — compiles to nothing.
9
9
  */
10
+ export type { CallerBundleV1, BundleEncryption, CallerSource, } from '../shared/protocol/caller-bundle.js';
10
11
  export interface AdminMeta {
11
12
  version: string;
12
13
  port: number;
@@ -43,6 +44,9 @@ export interface AdminCaller {
43
44
  envKeys: string[];
44
45
  fingerprint: string | null;
45
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;
46
50
  }
47
51
  export type ConnectionCategory = 'ai' | 'developer-tools' | 'gaming' | 'messaging' | 'productivity' | 'social-media';
48
52
  export interface AdminConnectionTemplate {
@@ -89,6 +89,7 @@ export function createAdminRouter(deps) {
89
89
  envKeys: Object.keys(caller.env ?? {}),
90
90
  fingerprint,
91
91
  keysDirExists,
92
+ source: caller.source ?? null,
92
93
  };
93
94
  });
94
95
  res.json(out);
@@ -1,21 +1,21 @@
1
1
  /**
2
- * Programmatic / non-interactive caller bootstrap (item E).
2
+ * Caller provisioning drawlatch is the sole issuer of caller key material.
3
3
  *
4
- * The interactive `drawlatch sync` handshake stays the path for *remote*
5
- * enrollment (a caller on another machine proving identity over the network).
6
- * This module is for the zero-friction local case: a co-located client that
7
- * shares drawlatch's filesystem can provision a caller complete with a fresh
8
- * keypair without any invite-code dance.
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).
9
12
  *
10
- * Two entry points:
11
- * - `createCallerWithKeys()` library function (also exposed as the
12
- * password-gated `POST /api/admin/callers` admin endpoint).
13
- * - `autoEnroll()` — loopback / shared-fs path: a client proves filesystem
14
- * access by presenting the one-time token drawlatch writes into the config
15
- * dir at startup, and gets a caller provisioned with zero interaction.
13
+ * `createCallerWithKeys()` (priv+pub on drawlatch disk) is retained for the
14
+ * password-gated `POST /api/admin/callers` create endpoint and tests.
16
15
  */
17
16
  import { type RemoteServerConfig } from '../shared/config.js';
18
- import type { SerializedPublicKeys } from '../shared/crypto/keys.js';
17
+ import { type SerializedPublicKeys } from '../shared/crypto/keys.js';
18
+ import type { CallerBundleV1 } from '../shared/protocol/caller-bundle.js';
19
19
  /** Valid caller alias: starts with a letter/number, then letters/numbers/-/_. */
20
20
  export declare const CALLER_ALIAS_REGEX: RegExp;
21
21
  export interface CreateCallerOptions {
@@ -36,36 +36,86 @@ export interface CreateCallerResult {
36
36
  connections: string[];
37
37
  }
38
38
  /**
39
- * Create a caller WITH a fresh keypair, registered in remote.config.json,
40
- * without the interactive sync handshake.
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
41
  *
42
42
  * Throws on invalid alias, or if the caller (config entry or key dir) exists.
43
43
  */
44
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
+ }
45
58
  /**
46
- * Delete a caller: removes its config entry, prefixed env vars, and key dir.
47
- * The `default` caller cannot be deleted.
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.
48
75
  */
49
- export declare function deleteCaller(alias: string): void;
50
- /** Path of the one-time enroll token file inside the config dir. */
51
- export declare function getEnrollTokenPath(): string;
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
+ }
52
94
  /**
53
- * Write a fresh one-time enroll token into the config dir (mode 0600).
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.
54
100
  *
55
- * Only a process with filesystem access to drawlatch's config dir can read it,
56
- * which is exactly the proof-of-co-location we want for `autoEnroll()`.
57
- * Called once at daemon startup; overwrites any stale token.
101
+ * Writes, under `<keysDir>`:
102
+ * - callers/<alias>/{signing,exchange}.{key,pub}.pem (0600 / 0644)
103
+ * - server/{signing,exchange}.pub.pem (0644)
58
104
  */
59
- export declare function writeEnrollToken(): string;
105
+ export declare function issueLocalCaller(opts: IssueLocalCallerOptions): IssueLocalCallerResult;
60
106
  /**
61
- * Auto-enroll a co-located caller by presenting the one-time enroll token.
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).
62
110
  *
63
- * Verifies the token proves filesystem access to the config dir, then either
64
- * creates a new caller with keys (if the alias is new) or returns the existing
65
- * one's metadata (idempotent). The token is single-use: it is rotated on every
66
- * successful enroll so a leaked token cannot be replayed.
111
+ * Replaces the retired enroll-token / `/sync/auto-enroll` path.
67
112
  */
68
- export declare function autoEnroll(token: string, alias: string, opts?: CreateCallerOptions): CreateCallerResult;
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;
69
119
  /** Re-exported for callers that want to introspect config without re-importing. */
70
120
  export type { RemoteServerConfig };
71
121
  //# sourceMappingURL=caller-bootstrap.d.ts.map