@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 +181 -7
- package/dist/remote/admin-mutations.d.ts +6 -0
- package/dist/remote/admin-mutations.js +71 -1
- package/dist/remote/admin-types.d.ts +4 -0
- package/dist/remote/admin.js +1 -0
- package/dist/remote/caller-bootstrap.d.ts +81 -31
- package/dist/remote/caller-bootstrap.js +204 -75
- package/dist/remote/server.js +26 -39
- package/dist/shared/config.d.ts +7 -0
- package/dist/shared/crypto/index.d.ts +1 -1
- package/dist/shared/crypto/index.js +1 -1
- package/dist/shared/crypto/key-manager.d.ts +14 -0
- package/dist/shared/crypto/key-manager.js +25 -3
- 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/frontend/dist/assets/index-BdCSPSZK.js +282 -0
- package/frontend/dist/assets/{index-D4k5QKBP.css → index-BmK26bY2.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-IzwnSOmu.js +0 -267
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.
|
|
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(`
|
|
313
|
-
console.log(` drawlatch
|
|
314
|
-
console.log(`
|
|
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
|
-
|
|
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
|
|
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 {
|
package/dist/remote/admin.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Caller provisioning — drawlatch is the sole issuer of caller key material.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
|
40
|
-
*
|
|
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
|
-
*
|
|
47
|
-
*
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
|
105
|
+
export declare function issueLocalCaller(opts: IssueLocalCallerOptions): IssueLocalCallerResult;
|
|
60
106
|
/**
|
|
61
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|