@mnemom/mnemom 0.7.0
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/LICENSE +191 -0
- package/README.md +123 -0
- package/dist/commands/agents.d.ts +15 -0
- package/dist/commands/agents.js +303 -0
- package/dist/commands/auth.d.ts +5 -0
- package/dist/commands/auth.js +60 -0
- package/dist/commands/card.d.ts +23 -0
- package/dist/commands/card.js +460 -0
- package/dist/commands/claim.d.ts +1 -0
- package/dist/commands/claim.js +72 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +763 -0
- package/dist/commands/integrity.d.ts +1 -0
- package/dist/commands/integrity.js +49 -0
- package/dist/commands/license.d.ts +3 -0
- package/dist/commands/license.js +163 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +73 -0
- package/dist/commands/migrate-config.d.ts +2 -0
- package/dist/commands/migrate-config.js +72 -0
- package/dist/commands/policy.d.ts +31 -0
- package/dist/commands/policy.js +543 -0
- package/dist/commands/register.d.ts +6 -0
- package/dist/commands/register.js +362 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +383 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +381 -0
- package/dist/lib/api.d.ts +133 -0
- package/dist/lib/api.js +207 -0
- package/dist/lib/auth.d.ts +60 -0
- package/dist/lib/auth.js +281 -0
- package/dist/lib/config.d.ts +105 -0
- package/dist/lib/config.js +253 -0
- package/dist/lib/format.d.ts +35 -0
- package/dist/lib/format.js +60 -0
- package/dist/lib/model-cache.d.ts +16 -0
- package/dist/lib/model-cache.js +138 -0
- package/dist/lib/models.d.ts +41 -0
- package/dist/lib/models.js +357 -0
- package/dist/lib/openclaw.d.ts +221 -0
- package/dist/lib/openclaw.js +474 -0
- package/dist/lib/prompt.d.ts +26 -0
- package/dist/lib/prompt.js +150 -0
- package/dist/smoltbot-shim.d.ts +2 -0
- package/dist/smoltbot-shim.js +7 -0
- package/package.json +61 -0
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { getApiUrl } from "./config.js";
|
|
2
|
+
import { resolveAuth } from "./auth.js";
|
|
3
|
+
export const API_BASE = getApiUrl();
|
|
4
|
+
/** Sanitize file-sourced data before use in outbound HTTP requests. */
|
|
5
|
+
function sanitizeForHttp(data) {
|
|
6
|
+
return String(data).trim();
|
|
7
|
+
}
|
|
8
|
+
/** Validate a URL before use in HTTP requests to prevent injection. */
|
|
9
|
+
function validateUrl(url) {
|
|
10
|
+
const parsed = new URL(url);
|
|
11
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
12
|
+
throw new Error(`Invalid URL protocol: ${parsed.protocol}`);
|
|
13
|
+
}
|
|
14
|
+
return parsed.href;
|
|
15
|
+
}
|
|
16
|
+
async function fetchApi(endpoint) {
|
|
17
|
+
const url = validateUrl(`${API_BASE}${endpoint}`);
|
|
18
|
+
const response = await fetch(url);
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
const error = (await response.json().catch(() => ({
|
|
21
|
+
error: "unknown",
|
|
22
|
+
message: response.statusText,
|
|
23
|
+
})));
|
|
24
|
+
throw new Error(error.message || `API request failed: ${response.status}`);
|
|
25
|
+
}
|
|
26
|
+
return response.json();
|
|
27
|
+
}
|
|
28
|
+
export async function postApi(endpoint, body) {
|
|
29
|
+
const url = validateUrl(`${API_BASE}${endpoint}`);
|
|
30
|
+
const cred = await resolveAuth();
|
|
31
|
+
const headers = {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
};
|
|
34
|
+
if (cred.type === "jwt") {
|
|
35
|
+
headers["Authorization"] = `Bearer ${cred.token}`;
|
|
36
|
+
}
|
|
37
|
+
else if (cred.type === "api-key") {
|
|
38
|
+
headers["X-Mnemom-Api-Key"] = cred.key;
|
|
39
|
+
}
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers,
|
|
43
|
+
body: JSON.stringify(body),
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const error = (await response.json().catch(() => ({
|
|
47
|
+
error: "unknown",
|
|
48
|
+
message: `HTTP ${response.status}`,
|
|
49
|
+
})));
|
|
50
|
+
const msg = "message" in error ? error.message : JSON.stringify(error);
|
|
51
|
+
// Preserve conflict_agent_id in the error message for 409 handling upstream
|
|
52
|
+
if (response.status === 409 && "conflict_agent_id" in error) {
|
|
53
|
+
throw new Error(`409: ${msg} (conflict: ${error.conflict_agent_id})`);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`${response.status}: ${msg}`);
|
|
56
|
+
}
|
|
57
|
+
return response.json();
|
|
58
|
+
}
|
|
59
|
+
export async function verifyBinding(agentId, keyHash) {
|
|
60
|
+
return postApi(`/v1/agents/${agentId}/verify-binding`, { key_hash: keyHash });
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build auth headers from the best available credential.
|
|
64
|
+
* Returns empty object when unauthenticated (read-only calls).
|
|
65
|
+
*/
|
|
66
|
+
async function authHeaders() {
|
|
67
|
+
const cred = await resolveAuth();
|
|
68
|
+
switch (cred.type) {
|
|
69
|
+
case "jwt":
|
|
70
|
+
return { Authorization: `Bearer ${sanitizeForHttp(cred.token)}` };
|
|
71
|
+
case "api-key":
|
|
72
|
+
return { "X-Mnemom-Api-Key": sanitizeForHttp(cred.key) };
|
|
73
|
+
case "none":
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function getAgent(id) {
|
|
78
|
+
return fetchApi(`/v1/agents/${id}`);
|
|
79
|
+
}
|
|
80
|
+
export async function listAgents() {
|
|
81
|
+
const url = validateUrl(`${API_BASE}/v1/agents?limit=100`);
|
|
82
|
+
const response = await fetch(url, { headers: await authHeaders() });
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
if (response.status === 401) {
|
|
85
|
+
throw new Error("Not authenticated. Run `smoltbot login` or set MNEMOM_API_KEY.");
|
|
86
|
+
}
|
|
87
|
+
const err = await response.json().catch(() => ({ error: "unknown" }));
|
|
88
|
+
throw new Error(err.message || `Failed to list agents: ${response.status}`);
|
|
89
|
+
}
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
return data.agents ?? [];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Look up an agent in the authenticated user's account by name.
|
|
95
|
+
* Tries exact match first, then single partial match.
|
|
96
|
+
* Throws if multiple agents partially match (ambiguous).
|
|
97
|
+
* Returns null if no match found.
|
|
98
|
+
* Note: capped at 100 agents by listAgents().
|
|
99
|
+
*/
|
|
100
|
+
export async function getAgentByName(name) {
|
|
101
|
+
const agents = await listAgents();
|
|
102
|
+
const lower = name.toLowerCase();
|
|
103
|
+
const exact = agents.find(a => a.name?.toLowerCase() === lower);
|
|
104
|
+
if (exact)
|
|
105
|
+
return exact;
|
|
106
|
+
const partials = agents.filter(a => a.name?.toLowerCase().includes(lower));
|
|
107
|
+
if (partials.length === 1)
|
|
108
|
+
return partials[0];
|
|
109
|
+
if (partials.length > 1) {
|
|
110
|
+
const names = partials.map(a => a.name ?? a.id).join(", ");
|
|
111
|
+
throw new Error(`Multiple agents match '${name}': ${names}. Use a more specific name.`);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
export async function getIntegrity(id) {
|
|
116
|
+
return fetchApi(`/v1/integrity/${id}`);
|
|
117
|
+
}
|
|
118
|
+
export async function getTraces(id, limit = 10) {
|
|
119
|
+
return fetchApi(`/v1/traces?agent_id=${id}&limit=${limit}`);
|
|
120
|
+
}
|
|
121
|
+
export async function getCard(agentId) {
|
|
122
|
+
try {
|
|
123
|
+
return await fetchApi(`/v1/agents/${agentId}/card`);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
if (message.includes("404") || message.includes("not found")) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export async function updateCard(agentId, cardJson) {
|
|
134
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/card`);
|
|
135
|
+
const response = await fetch(url, {
|
|
136
|
+
method: "PATCH",
|
|
137
|
+
headers: { "Content-Type": "application/json", ...(await authHeaders()) },
|
|
138
|
+
body: sanitizeForHttp(JSON.stringify({ card_json: cardJson })),
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const error = (await response.json().catch(() => ({
|
|
142
|
+
error: "unknown",
|
|
143
|
+
message: response.statusText,
|
|
144
|
+
})));
|
|
145
|
+
throw new Error(error.message || `Card update failed: ${response.status}`);
|
|
146
|
+
}
|
|
147
|
+
return response.json();
|
|
148
|
+
}
|
|
149
|
+
export async function reverifyAgent(agentId) {
|
|
150
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/reverify`);
|
|
151
|
+
const response = await fetch(url, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: { "Content-Type": "application/json", ...(await authHeaders()) },
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
const error = (await response.json().catch(() => ({
|
|
157
|
+
error: "unknown",
|
|
158
|
+
message: response.statusText,
|
|
159
|
+
})));
|
|
160
|
+
throw new Error(error.message || `Reverify failed: ${response.status}`);
|
|
161
|
+
}
|
|
162
|
+
return response.json();
|
|
163
|
+
}
|
|
164
|
+
export async function getPolicy(agentId) {
|
|
165
|
+
try {
|
|
166
|
+
return await fetchApi(`/v1/agents/${agentId}/policy`);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
if (message.includes("404") || message.includes("not found")) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export async function publishPolicy(agentId, policyJson) {
|
|
177
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/policy`);
|
|
178
|
+
const response = await fetch(url, {
|
|
179
|
+
method: "PUT",
|
|
180
|
+
headers: { "Content-Type": "application/json", ...(await authHeaders()) },
|
|
181
|
+
body: sanitizeForHttp(JSON.stringify({ policy_json: policyJson })),
|
|
182
|
+
});
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const error = (await response.json().catch(() => ({
|
|
185
|
+
error: "unknown",
|
|
186
|
+
message: response.statusText,
|
|
187
|
+
})));
|
|
188
|
+
throw new Error(error.message || `Policy publish failed: ${response.status}`);
|
|
189
|
+
}
|
|
190
|
+
return response.json();
|
|
191
|
+
}
|
|
192
|
+
export async function testPolicyHistorical(agentId, policyJson, limit = 50) {
|
|
193
|
+
const url = validateUrl(`${API_BASE}/v1/policies/evaluate/historical`);
|
|
194
|
+
const response = await fetch(url, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: { "Content-Type": "application/json", ...(await authHeaders()) },
|
|
197
|
+
body: sanitizeForHttp(JSON.stringify({ agent_id: agentId, policy_json: policyJson, limit })),
|
|
198
|
+
});
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
const error = (await response.json().catch(() => ({
|
|
201
|
+
error: "unknown",
|
|
202
|
+
message: response.statusText,
|
|
203
|
+
})));
|
|
204
|
+
throw new Error(error.message || `Historical evaluation failed: ${response.status}`);
|
|
205
|
+
}
|
|
206
|
+
return response.json();
|
|
207
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type AuthTokens } from "./config.js";
|
|
2
|
+
export type AuthCredential = {
|
|
3
|
+
type: "jwt";
|
|
4
|
+
token: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: "api-key";
|
|
7
|
+
key: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "none";
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Get a valid access token, or null if not authenticated.
|
|
13
|
+
*
|
|
14
|
+
* Resolution order:
|
|
15
|
+
* 1. SMOLTBOT_TOKEN environment variable (CI / non-interactive)
|
|
16
|
+
* 2. Stored token from config (auto-refreshes if expired)
|
|
17
|
+
*/
|
|
18
|
+
export declare function getAccessToken(): Promise<string | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Get a valid access token or exit with a helpful message.
|
|
21
|
+
*/
|
|
22
|
+
export declare function requireAccessToken(): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Get the Mnemom API key from env var or config.
|
|
25
|
+
*
|
|
26
|
+
* Resolution order:
|
|
27
|
+
* 1. MNEMOM_API_KEY environment variable
|
|
28
|
+
* 2. Stored mnemomApiKey from config
|
|
29
|
+
*/
|
|
30
|
+
export declare function getMnemomApiKey(): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the best available auth credential.
|
|
33
|
+
*
|
|
34
|
+
* Resolution order:
|
|
35
|
+
* 1. JWT (SMOLTBOT_TOKEN env or stored token with auto-refresh)
|
|
36
|
+
* 2. API key (MNEMOM_API_KEY env or config mnemomApiKey)
|
|
37
|
+
* 3. None
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveAuth(): Promise<AuthCredential>;
|
|
40
|
+
/**
|
|
41
|
+
* Require authentication (JWT or API key) or exit with a helpful message.
|
|
42
|
+
*/
|
|
43
|
+
export declare function requireAuth(): Promise<AuthCredential & {
|
|
44
|
+
type: "jwt" | "api-key";
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Authenticate via browser-based login flow.
|
|
48
|
+
*
|
|
49
|
+
* 1. Start a local HTTP server on a random port
|
|
50
|
+
* 2. Generate a random `state` nonce for CSRF protection
|
|
51
|
+
* 3. Open the browser to the API's CLI login page
|
|
52
|
+
* 4. Wait for the login page to POST tokens back to localhost
|
|
53
|
+
* 5. Verify state, store tokens, and close the server
|
|
54
|
+
*/
|
|
55
|
+
export declare function loginWithBrowser(): Promise<AuthTokens>;
|
|
56
|
+
/**
|
|
57
|
+
* Authenticate with email + password via the API auth proxy.
|
|
58
|
+
* Used by --no-browser fallback.
|
|
59
|
+
*/
|
|
60
|
+
export declare function loginWithPassword(email: string, password: string): Promise<AuthTokens>;
|
package/dist/lib/auth.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import * as http from "node:http";
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
3
|
+
import { exec } from "node:child_process";
|
|
4
|
+
import { getApiUrl, getWebsiteUrl, getAuthInfo, saveAuthTokens, loadConfig } from "./config.js";
|
|
5
|
+
/** Sanitize file-sourced data before use in outbound HTTP requests. */
|
|
6
|
+
function sanitizeForHttp(data) {
|
|
7
|
+
return String(data).trim();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get a valid access token, or null if not authenticated.
|
|
11
|
+
*
|
|
12
|
+
* Resolution order:
|
|
13
|
+
* 1. SMOLTBOT_TOKEN environment variable (CI / non-interactive)
|
|
14
|
+
* 2. Stored token from config (auto-refreshes if expired)
|
|
15
|
+
*/
|
|
16
|
+
export async function getAccessToken() {
|
|
17
|
+
// 1. Env var override (CI / non-interactive)
|
|
18
|
+
const envToken = process.env.SMOLTBOT_TOKEN;
|
|
19
|
+
if (envToken)
|
|
20
|
+
return envToken;
|
|
21
|
+
// 2. Stored token
|
|
22
|
+
const auth = getAuthInfo();
|
|
23
|
+
if (!auth)
|
|
24
|
+
return null;
|
|
25
|
+
// Check expiry (with 60s buffer)
|
|
26
|
+
const now = Math.floor(Date.now() / 1000);
|
|
27
|
+
if (auth.expiresAt > now + 60) {
|
|
28
|
+
return auth.accessToken;
|
|
29
|
+
}
|
|
30
|
+
// 3. Auto-refresh
|
|
31
|
+
const refreshed = await refreshAccessToken(auth.refreshToken);
|
|
32
|
+
if (refreshed)
|
|
33
|
+
return refreshed.accessToken;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get a valid access token or exit with a helpful message.
|
|
38
|
+
*/
|
|
39
|
+
export async function requireAccessToken() {
|
|
40
|
+
const token = await getAccessToken();
|
|
41
|
+
if (!token) {
|
|
42
|
+
console.error("Authentication required. Run `smoltbot login` first.");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
return token;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the Mnemom API key from env var or config.
|
|
49
|
+
*
|
|
50
|
+
* Resolution order:
|
|
51
|
+
* 1. MNEMOM_API_KEY environment variable
|
|
52
|
+
* 2. Stored mnemomApiKey from config
|
|
53
|
+
*/
|
|
54
|
+
export function getMnemomApiKey() {
|
|
55
|
+
const envKey = process.env.MNEMOM_API_KEY;
|
|
56
|
+
if (envKey)
|
|
57
|
+
return envKey;
|
|
58
|
+
const config = loadConfig();
|
|
59
|
+
return config?.mnemomApiKey ?? null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the best available auth credential.
|
|
63
|
+
*
|
|
64
|
+
* Resolution order:
|
|
65
|
+
* 1. JWT (SMOLTBOT_TOKEN env or stored token with auto-refresh)
|
|
66
|
+
* 2. API key (MNEMOM_API_KEY env or config mnemomApiKey)
|
|
67
|
+
* 3. None
|
|
68
|
+
*/
|
|
69
|
+
export async function resolveAuth() {
|
|
70
|
+
const jwt = await getAccessToken();
|
|
71
|
+
if (jwt)
|
|
72
|
+
return { type: "jwt", token: jwt };
|
|
73
|
+
const apiKey = getMnemomApiKey();
|
|
74
|
+
if (apiKey)
|
|
75
|
+
return { type: "api-key", key: apiKey };
|
|
76
|
+
return { type: "none" };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Require authentication (JWT or API key) or exit with a helpful message.
|
|
80
|
+
*/
|
|
81
|
+
export async function requireAuth() {
|
|
82
|
+
const cred = await resolveAuth();
|
|
83
|
+
if (cred.type === "none") {
|
|
84
|
+
console.error("Authentication required. Run `smoltbot login` or set MNEMOM_API_KEY.");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
return cred;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Authenticate via browser-based login flow.
|
|
91
|
+
*
|
|
92
|
+
* 1. Start a local HTTP server on a random port
|
|
93
|
+
* 2. Generate a random `state` nonce for CSRF protection
|
|
94
|
+
* 3. Open the browser to the API's CLI login page
|
|
95
|
+
* 4. Wait for the login page to POST tokens back to localhost
|
|
96
|
+
* 5. Verify state, store tokens, and close the server
|
|
97
|
+
*/
|
|
98
|
+
export async function loginWithBrowser() {
|
|
99
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
100
|
+
const { port, tokenPromise, close } = await startCallbackServer(state);
|
|
101
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
102
|
+
const loginUrl = `${getWebsiteUrl()}/login?cli_callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
|
|
103
|
+
console.log("Opening browser to authenticate...");
|
|
104
|
+
console.log(`If the browser doesn't open, visit:\n ${loginUrl}\n`);
|
|
105
|
+
openBrowser(loginUrl);
|
|
106
|
+
console.log("Waiting for authentication...");
|
|
107
|
+
try {
|
|
108
|
+
const tokens = await tokenPromise;
|
|
109
|
+
saveAuthTokens(tokens);
|
|
110
|
+
return tokens;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Start a local HTTP server that listens for the auth callback POST.
|
|
118
|
+
* Returns the assigned port, a promise that resolves with tokens, and a close function.
|
|
119
|
+
*/
|
|
120
|
+
async function startCallbackServer(expectedState) {
|
|
121
|
+
let resolveTokens;
|
|
122
|
+
let rejectTokens;
|
|
123
|
+
const tokenPromise = new Promise((resolve, reject) => {
|
|
124
|
+
resolveTokens = resolve;
|
|
125
|
+
rejectTokens = reject;
|
|
126
|
+
});
|
|
127
|
+
const server = http.createServer((req, res) => {
|
|
128
|
+
// Handle CORS preflight for the POST from the browser page
|
|
129
|
+
if (req.method === "OPTIONS") {
|
|
130
|
+
res.writeHead(200, {
|
|
131
|
+
"Access-Control-Allow-Origin": "*",
|
|
132
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
133
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
134
|
+
});
|
|
135
|
+
res.end();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (req.method !== "POST" || req.url !== "/callback") {
|
|
139
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
140
|
+
res.end("Not found");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
let body = "";
|
|
144
|
+
req.on("data", (chunk) => {
|
|
145
|
+
body += chunk.toString();
|
|
146
|
+
// Limit body size to prevent abuse
|
|
147
|
+
if (body.length > 1_000_000) {
|
|
148
|
+
req.destroy();
|
|
149
|
+
rejectTokens(new Error("Callback body too large"));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
req.on("end", () => {
|
|
153
|
+
try {
|
|
154
|
+
const data = JSON.parse(body);
|
|
155
|
+
if (data.state !== expectedState) {
|
|
156
|
+
res.writeHead(403, {
|
|
157
|
+
"Content-Type": "text/html",
|
|
158
|
+
"Access-Control-Allow-Origin": "*",
|
|
159
|
+
});
|
|
160
|
+
res.end("<html><body><h2>Authentication failed</h2><p>State mismatch. Please try again.</p></body></html>");
|
|
161
|
+
rejectTokens(new Error("State mismatch — possible CSRF attack"));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const tokens = {
|
|
165
|
+
accessToken: data.access_token,
|
|
166
|
+
refreshToken: data.refresh_token,
|
|
167
|
+
expiresAt: Math.floor(Date.now() / 1000) + data.expires_in,
|
|
168
|
+
userId: data.user_id,
|
|
169
|
+
email: data.user_email,
|
|
170
|
+
};
|
|
171
|
+
res.writeHead(200, {
|
|
172
|
+
"Content-Type": "text/html",
|
|
173
|
+
"Access-Control-Allow-Origin": "*",
|
|
174
|
+
});
|
|
175
|
+
res.end(`<html><body style="font-family:system-ui;text-align:center;padding:60px">
|
|
176
|
+
<h2>Authenticated!</h2>
|
|
177
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
178
|
+
</body></html>`);
|
|
179
|
+
resolveTokens(tokens);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
res.writeHead(400, {
|
|
183
|
+
"Content-Type": "text/html",
|
|
184
|
+
"Access-Control-Allow-Origin": "*",
|
|
185
|
+
});
|
|
186
|
+
res.end("<html><body><h2>Authentication failed</h2><p>Invalid callback data.</p></body></html>");
|
|
187
|
+
rejectTokens(new Error("Invalid callback data"));
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
// Listen on port 0, wait for the server to be ready before reading the address
|
|
192
|
+
const port = await new Promise((resolve) => {
|
|
193
|
+
server.listen(0, "127.0.0.1", () => {
|
|
194
|
+
resolve(server.address().port);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
// Auto-timeout after 5 minutes
|
|
198
|
+
const timeout = setTimeout(() => {
|
|
199
|
+
rejectTokens(new Error("Login timed out. Please try again."));
|
|
200
|
+
server.close();
|
|
201
|
+
}, 5 * 60 * 1000);
|
|
202
|
+
return {
|
|
203
|
+
port,
|
|
204
|
+
tokenPromise,
|
|
205
|
+
close: () => {
|
|
206
|
+
clearTimeout(timeout);
|
|
207
|
+
server.close();
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Open a URL in the user's default browser.
|
|
213
|
+
*/
|
|
214
|
+
function openBrowser(url) {
|
|
215
|
+
const cmd = process.platform === "darwin"
|
|
216
|
+
? "open"
|
|
217
|
+
: process.platform === "win32"
|
|
218
|
+
? "start"
|
|
219
|
+
: "xdg-open";
|
|
220
|
+
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Authenticate with email + password via the API auth proxy.
|
|
224
|
+
* Used by --no-browser fallback.
|
|
225
|
+
*/
|
|
226
|
+
export async function loginWithPassword(email, password) {
|
|
227
|
+
const url = `${getApiUrl()}/v1/auth/login`;
|
|
228
|
+
const res = await fetch(url, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: { "Content-Type": "application/json" },
|
|
231
|
+
body: JSON.stringify({ email, password }),
|
|
232
|
+
});
|
|
233
|
+
if (!res.ok) {
|
|
234
|
+
const body = (await res.json().catch(() => ({})));
|
|
235
|
+
throw new Error(body.error || body.message || "Login failed");
|
|
236
|
+
}
|
|
237
|
+
const data = (await res.json());
|
|
238
|
+
const tokens = {
|
|
239
|
+
accessToken: data.access_token,
|
|
240
|
+
refreshToken: data.refresh_token,
|
|
241
|
+
expiresAt: Math.floor(Date.now() / 1000) + data.expires_in,
|
|
242
|
+
userId: data.user.id,
|
|
243
|
+
email: data.user.email,
|
|
244
|
+
};
|
|
245
|
+
saveAuthTokens(tokens);
|
|
246
|
+
return tokens;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Refresh an expired access token.
|
|
250
|
+
* Returns new tokens on success, null on failure.
|
|
251
|
+
*/
|
|
252
|
+
async function refreshAccessToken(refreshToken) {
|
|
253
|
+
if (!refreshToken || typeof refreshToken !== "string") {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const url = new URL(`${getApiUrl()}/v1/auth/refresh`).href;
|
|
257
|
+
try {
|
|
258
|
+
const res = await fetch(url, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: { "Content-Type": "application/json" },
|
|
261
|
+
body: sanitizeForHttp(JSON.stringify({ refresh_token: String(refreshToken) })),
|
|
262
|
+
});
|
|
263
|
+
if (!res.ok)
|
|
264
|
+
return null;
|
|
265
|
+
const data = (await res.json());
|
|
266
|
+
// Preserve existing user info from stored auth
|
|
267
|
+
const existing = getAuthInfo();
|
|
268
|
+
const tokens = {
|
|
269
|
+
accessToken: data.access_token,
|
|
270
|
+
refreshToken: data.refresh_token,
|
|
271
|
+
expiresAt: Math.floor(Date.now() / 1000) + data.expires_in,
|
|
272
|
+
userId: existing?.userId ?? "",
|
|
273
|
+
email: existing?.email ?? "",
|
|
274
|
+
};
|
|
275
|
+
saveAuthTokens(tokens);
|
|
276
|
+
return tokens;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export declare const CONFIG_DIR: string;
|
|
2
|
+
export declare const CONFIG_FILE: string;
|
|
3
|
+
export type Environment = "production" | "staging" | "local";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the active environment.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order:
|
|
8
|
+
* 1. `SMOLTBOT_ENV` environment variable
|
|
9
|
+
* 2. Defaults to `production`
|
|
10
|
+
*/
|
|
11
|
+
export declare function getEnvironment(): Environment;
|
|
12
|
+
export declare function getApiUrl(): string;
|
|
13
|
+
export declare function getGatewayUrl(): string;
|
|
14
|
+
export declare function getWebsiteUrl(): string;
|
|
15
|
+
export interface ConfigV1 {
|
|
16
|
+
agentId: string;
|
|
17
|
+
email?: string;
|
|
18
|
+
gateway?: string;
|
|
19
|
+
openclawConfigured?: boolean;
|
|
20
|
+
providers?: string[];
|
|
21
|
+
mnemomApiKey?: string;
|
|
22
|
+
licenseJwt?: string;
|
|
23
|
+
configuredAt?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface AgentConfig {
|
|
26
|
+
agentId: string;
|
|
27
|
+
openclawConfigured?: boolean;
|
|
28
|
+
providers?: string[];
|
|
29
|
+
configuredAt?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface AuthTokens {
|
|
32
|
+
accessToken: string;
|
|
33
|
+
refreshToken: string;
|
|
34
|
+
expiresAt: number;
|
|
35
|
+
userId: string;
|
|
36
|
+
email: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ConfigV2 {
|
|
39
|
+
version: 2;
|
|
40
|
+
defaultAgent: string;
|
|
41
|
+
gateway: string;
|
|
42
|
+
mnemomApiKey?: string;
|
|
43
|
+
licenseJwt?: string;
|
|
44
|
+
agents: Record<string, AgentConfig>;
|
|
45
|
+
auth?: AuthTokens;
|
|
46
|
+
}
|
|
47
|
+
/** Backward-compatible alias so existing imports keep working. */
|
|
48
|
+
export type Config = ConfigV2;
|
|
49
|
+
/**
|
|
50
|
+
* Migrate a v1 config into v2 format.
|
|
51
|
+
* All agent-specific fields move into `agents.default`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function migrateConfig(raw: ConfigV1): ConfigV2;
|
|
54
|
+
export declare function configExists(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Load the config file.
|
|
57
|
+
* If the file is v1 (no `version` field), it is automatically migrated to v2
|
|
58
|
+
* and written back to disk before returning.
|
|
59
|
+
*/
|
|
60
|
+
export declare function loadConfig(): ConfigV2 | null;
|
|
61
|
+
export declare function saveConfig(config: ConfigV2): void;
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the active agent config.
|
|
64
|
+
*
|
|
65
|
+
* Resolution order:
|
|
66
|
+
* 1. Explicit `agentName` parameter (--agent flag)
|
|
67
|
+
* 2. `SMOLTBOT_AGENT` environment variable
|
|
68
|
+
*
|
|
69
|
+
* Returns `null` if no agent is specified or the agent is not found.
|
|
70
|
+
* Callers must require --agent or SMOLTBOT_AGENT for agent-scoped commands.
|
|
71
|
+
*/
|
|
72
|
+
export declare function getActiveAgent(agentName?: string): AgentConfig | null;
|
|
73
|
+
/**
|
|
74
|
+
* Require an explicit agent selection. Exits with a helpful error if
|
|
75
|
+
* no agent was specified via --agent or SMOLTBOT_AGENT.
|
|
76
|
+
*
|
|
77
|
+
* Falls back to API lookup if the agent is not in local config:
|
|
78
|
+
* - smolt-XXXXXXXX IDs: public endpoint, no auth required
|
|
79
|
+
* - Names: authenticated account listing (requires `smoltbot login`)
|
|
80
|
+
*/
|
|
81
|
+
export declare function requireAgent(agentName?: string): Promise<AgentConfig>;
|
|
82
|
+
export declare function generateAgentId(): string;
|
|
83
|
+
/**
|
|
84
|
+
* Compute the 16-char agent_hash for a given API key and optional agent name.
|
|
85
|
+
* Matches the gateway's hashApiKey() and the POST /v1/agents/:id/rekey expected format.
|
|
86
|
+
*
|
|
87
|
+
* Unnamed agent: SHA256(apiKey).slice(0, 16)
|
|
88
|
+
* Named agent: SHA256(apiKey + '|' + name).slice(0, 16)
|
|
89
|
+
*/
|
|
90
|
+
export declare function computeAgentHash(apiKey: string, name?: string | null): string;
|
|
91
|
+
/**
|
|
92
|
+
* Derive agent ID deterministically from an API key.
|
|
93
|
+
* Uses SHA-256 to match the gateway's hashApiKey (Web Crypto SHA-256, first 16 hex chars).
|
|
94
|
+
* The agent ID is "smolt-" + first 8 hex chars of the SHA-256 digest.
|
|
95
|
+
*/
|
|
96
|
+
export declare function deriveAgentId(apiKey: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* Derive agent ID deterministically from an API key *and* a name.
|
|
99
|
+
* Allows multiple named agents to share one API key with distinct IDs.
|
|
100
|
+
* Uses SHA-256 to match the gateway's hashApiKey(apiKey + '|' + name).
|
|
101
|
+
*/
|
|
102
|
+
export declare function deriveAgentIdWithName(apiKey: string, name: string): string;
|
|
103
|
+
export declare function saveAuthTokens(tokens: AuthTokens): void;
|
|
104
|
+
export declare function clearAuthTokens(): void;
|
|
105
|
+
export declare function getAuthInfo(): AuthTokens | null;
|