settld 0.2.0 → 0.2.2
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/README.md +21 -0
- package/SETTLD_VERSION +1 -1
- package/bin/settld.js +18 -0
- package/docs/QUICKSTART_MCP_HOSTS.md +78 -4
- package/docs/gitbook/quickstart.md +47 -4
- package/docs/integrations/openclaw/PUBLIC_QUICKSTART.md +13 -0
- package/docs/ops/VERCEL_MONOREPO_DEPLOY.md +42 -0
- package/package.json +2 -1
- package/scripts/ci/run-mcp-host-smoke.mjs +6 -0
- package/scripts/ci/run-production-cutover-gate.mjs +6 -0
- package/scripts/demo/mcp-paid-exa.mjs +18 -1
- package/scripts/setup/login.mjs +299 -0
- package/scripts/setup/onboard.mjs +281 -26
- package/scripts/setup/session-store.mjs +65 -0
- package/scripts/vercel/build-mkdocs.sh +3 -3
- package/scripts/vercel/ignore-dashboard.sh +26 -0
- package/scripts/vercel/ignore-mkdocs.sh +2 -0
- package/scripts/vercel/install-mkdocs.sh +2 -3
- package/scripts/wallet/cli.mjs +871 -0
- package/src/core/wallet-funding-coinbase.js +197 -0
- package/src/core/wallet-funding-hosted.js +155 -0
- package/src/core/wallet-provider-bootstrap.js +95 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { cookieHeaderFromSetCookie, defaultSessionPath, writeSavedSession } from "./session-store.mjs";
|
|
9
|
+
|
|
10
|
+
const FORMAT_OPTIONS = new Set(["text", "json"]);
|
|
11
|
+
|
|
12
|
+
function usage() {
|
|
13
|
+
const text = [
|
|
14
|
+
"usage:",
|
|
15
|
+
" settld login [flags]",
|
|
16
|
+
" node scripts/setup/login.mjs [flags]",
|
|
17
|
+
"",
|
|
18
|
+
"flags:",
|
|
19
|
+
" --base-url <url> Settld onboarding base URL (default: https://api.settld.work)",
|
|
20
|
+
" --tenant-id <id> Existing tenant ID (omit to create via public signup)",
|
|
21
|
+
" --email <email> Login email",
|
|
22
|
+
" --company <name> Company name (required when --tenant-id omitted)",
|
|
23
|
+
" --otp <code> OTP code (otherwise prompted)",
|
|
24
|
+
" --non-interactive Disable prompts; require explicit flags",
|
|
25
|
+
" --session-file <path> Session output path (default: ~/.settld/session.json)",
|
|
26
|
+
" --format <text|json> Output format (default: text)",
|
|
27
|
+
" --help Show this help"
|
|
28
|
+
].join("\n");
|
|
29
|
+
process.stderr.write(`${text}\n`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readArgValue(argv, index, rawArg) {
|
|
33
|
+
const arg = String(rawArg ?? "");
|
|
34
|
+
const eq = arg.indexOf("=");
|
|
35
|
+
if (eq >= 0) return { value: arg.slice(eq + 1), nextIndex: index };
|
|
36
|
+
return { value: String(argv[index + 1] ?? ""), nextIndex: index + 1 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseArgs(argv) {
|
|
40
|
+
const out = {
|
|
41
|
+
baseUrl: "https://api.settld.work",
|
|
42
|
+
tenantId: "",
|
|
43
|
+
email: "",
|
|
44
|
+
company: "",
|
|
45
|
+
otp: "",
|
|
46
|
+
nonInteractive: false,
|
|
47
|
+
sessionFile: defaultSessionPath(),
|
|
48
|
+
format: "text",
|
|
49
|
+
help: false
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
53
|
+
const arg = String(argv[i] ?? "");
|
|
54
|
+
if (!arg) continue;
|
|
55
|
+
|
|
56
|
+
if (arg === "--help" || arg === "-h") {
|
|
57
|
+
out.help = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg === "--non-interactive" || arg === "--yes") {
|
|
61
|
+
out.nonInteractive = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (arg === "--base-url" || arg.startsWith("--base-url=")) {
|
|
65
|
+
const parsed = readArgValue(argv, i, arg);
|
|
66
|
+
out.baseUrl = parsed.value;
|
|
67
|
+
i = parsed.nextIndex;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === "--tenant-id" || arg.startsWith("--tenant-id=")) {
|
|
71
|
+
const parsed = readArgValue(argv, i, arg);
|
|
72
|
+
out.tenantId = parsed.value;
|
|
73
|
+
i = parsed.nextIndex;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--email" || arg.startsWith("--email=")) {
|
|
77
|
+
const parsed = readArgValue(argv, i, arg);
|
|
78
|
+
out.email = parsed.value;
|
|
79
|
+
i = parsed.nextIndex;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg === "--company" || arg.startsWith("--company=")) {
|
|
83
|
+
const parsed = readArgValue(argv, i, arg);
|
|
84
|
+
out.company = parsed.value;
|
|
85
|
+
i = parsed.nextIndex;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg === "--otp" || arg.startsWith("--otp=")) {
|
|
89
|
+
const parsed = readArgValue(argv, i, arg);
|
|
90
|
+
out.otp = parsed.value;
|
|
91
|
+
i = parsed.nextIndex;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg === "--session-file" || arg.startsWith("--session-file=")) {
|
|
95
|
+
const parsed = readArgValue(argv, i, arg);
|
|
96
|
+
out.sessionFile = parsed.value;
|
|
97
|
+
i = parsed.nextIndex;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg === "--format" || arg.startsWith("--format=")) {
|
|
101
|
+
const parsed = readArgValue(argv, i, arg);
|
|
102
|
+
out.format = String(parsed.value ?? "").trim().toLowerCase();
|
|
103
|
+
i = parsed.nextIndex;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!FORMAT_OPTIONS.has(out.format)) throw new Error("--format must be text|json");
|
|
110
|
+
out.baseUrl = String(out.baseUrl ?? "").trim().replace(/\/+$/, "");
|
|
111
|
+
out.tenantId = String(out.tenantId ?? "").trim();
|
|
112
|
+
out.email = String(out.email ?? "").trim().toLowerCase();
|
|
113
|
+
out.company = String(out.company ?? "").trim();
|
|
114
|
+
out.otp = String(out.otp ?? "").trim();
|
|
115
|
+
out.sessionFile = path.resolve(process.cwd(), String(out.sessionFile ?? "").trim() || defaultSessionPath());
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function mustHttpUrl(value, label) {
|
|
120
|
+
const raw = String(value ?? "").trim();
|
|
121
|
+
let parsed;
|
|
122
|
+
try {
|
|
123
|
+
parsed = new URL(raw);
|
|
124
|
+
} catch {
|
|
125
|
+
throw new Error(`${label} must be a valid URL`);
|
|
126
|
+
}
|
|
127
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
128
|
+
throw new Error(`${label} must use http/https`);
|
|
129
|
+
}
|
|
130
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function requestJson(url, { method, body, headers = {}, fetchImpl = fetch } = {}) {
|
|
134
|
+
const res = await fetchImpl(url, {
|
|
135
|
+
method,
|
|
136
|
+
headers: {
|
|
137
|
+
"content-type": "application/json",
|
|
138
|
+
...headers
|
|
139
|
+
},
|
|
140
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
141
|
+
});
|
|
142
|
+
const text = await res.text();
|
|
143
|
+
let json = null;
|
|
144
|
+
try {
|
|
145
|
+
json = text ? JSON.parse(text) : null;
|
|
146
|
+
} catch {
|
|
147
|
+
json = null;
|
|
148
|
+
}
|
|
149
|
+
return { res, text, json };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function promptLine(rl, label, { defaultValue = "", required = true } = {}) {
|
|
153
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
154
|
+
const value = String(await rl.question(`${label}${suffix}: `) ?? "").trim() || String(defaultValue ?? "").trim();
|
|
155
|
+
if (value || !required) return value;
|
|
156
|
+
throw new Error(`${label} is required`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function printBanner(stdout = process.stdout) {
|
|
160
|
+
stdout.write("Settld login\n");
|
|
161
|
+
stdout.write("============\n");
|
|
162
|
+
stdout.write("Sign in with OTP and save local session for one-command setup.\n\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function runLogin({
|
|
166
|
+
argv = process.argv.slice(2),
|
|
167
|
+
stdin = process.stdin,
|
|
168
|
+
stdout = process.stdout,
|
|
169
|
+
fetchImpl = fetch,
|
|
170
|
+
writeSavedSessionImpl = writeSavedSession
|
|
171
|
+
} = {}) {
|
|
172
|
+
const args = parseArgs(argv);
|
|
173
|
+
if (args.help) {
|
|
174
|
+
usage();
|
|
175
|
+
return { ok: true, code: 0 };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const interactive = !args.nonInteractive;
|
|
179
|
+
const state = {
|
|
180
|
+
baseUrl: args.baseUrl,
|
|
181
|
+
tenantId: args.tenantId,
|
|
182
|
+
email: args.email,
|
|
183
|
+
company: args.company,
|
|
184
|
+
otp: args.otp,
|
|
185
|
+
sessionFile: args.sessionFile,
|
|
186
|
+
format: args.format
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (interactive) printBanner(stdout);
|
|
190
|
+
const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
|
|
191
|
+
try {
|
|
192
|
+
if (interactive) {
|
|
193
|
+
state.baseUrl = await promptLine(rl, "Settld base URL", { defaultValue: state.baseUrl || "https://api.settld.work" });
|
|
194
|
+
state.tenantId = await promptLine(rl, "Tenant ID (optional for new signup)", { defaultValue: state.tenantId, required: false });
|
|
195
|
+
state.email = (await promptLine(rl, "Email", { defaultValue: state.email })).toLowerCase();
|
|
196
|
+
if (!state.tenantId) {
|
|
197
|
+
state.company = await promptLine(rl, "Company name", { defaultValue: state.company });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const baseUrl = mustHttpUrl(state.baseUrl, "base URL");
|
|
202
|
+
if (!state.email) throw new Error("email is required");
|
|
203
|
+
if (!state.tenantId && !state.company) throw new Error("company is required when tenant ID is omitted");
|
|
204
|
+
|
|
205
|
+
let tenantId = state.tenantId;
|
|
206
|
+
if (!tenantId) {
|
|
207
|
+
const signup = await requestJson(`${baseUrl}/v1/public/signup`, {
|
|
208
|
+
method: "POST",
|
|
209
|
+
body: { email: state.email, company: state.company },
|
|
210
|
+
fetchImpl
|
|
211
|
+
});
|
|
212
|
+
if (!signup.res.ok) {
|
|
213
|
+
const code = typeof signup.json?.code === "string" ? signup.json.code : "";
|
|
214
|
+
const message = typeof signup.json?.message === "string" ? signup.json.message : signup.text;
|
|
215
|
+
if (code === "SIGNUP_DISABLED") {
|
|
216
|
+
throw new Error("Public signup is disabled for this environment. Use an existing tenant ID or bootstrap key flow.");
|
|
217
|
+
}
|
|
218
|
+
throw new Error(`public signup failed (${signup.res.status}): ${message || "unknown error"}`);
|
|
219
|
+
}
|
|
220
|
+
tenantId = String(signup.json?.tenantId ?? "").trim();
|
|
221
|
+
if (!tenantId) throw new Error("public signup response missing tenantId");
|
|
222
|
+
if (interactive) stdout.write(`Created tenant: ${tenantId}\n`);
|
|
223
|
+
} else {
|
|
224
|
+
const otpRequest = await requestJson(`${baseUrl}/v1/tenants/${encodeURIComponent(tenantId)}/buyer/login/otp`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
body: { email: state.email },
|
|
227
|
+
fetchImpl
|
|
228
|
+
});
|
|
229
|
+
if (!otpRequest.res.ok) {
|
|
230
|
+
const message = typeof otpRequest.json?.message === "string" ? otpRequest.json.message : otpRequest.text;
|
|
231
|
+
throw new Error(`otp request failed (${otpRequest.res.status}): ${message || "unknown error"}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!state.otp && interactive) {
|
|
236
|
+
state.otp = await promptLine(rl, "OTP code", { required: true });
|
|
237
|
+
}
|
|
238
|
+
if (!state.otp) throw new Error("otp code is required (pass --otp in non-interactive mode)");
|
|
239
|
+
|
|
240
|
+
const login = await requestJson(`${baseUrl}/v1/tenants/${encodeURIComponent(tenantId)}/buyer/login`, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
body: { email: state.email, code: state.otp },
|
|
243
|
+
fetchImpl
|
|
244
|
+
});
|
|
245
|
+
if (!login.res.ok) {
|
|
246
|
+
const message = typeof login.json?.message === "string" ? login.json.message : login.text;
|
|
247
|
+
throw new Error(`login failed (${login.res.status}): ${message || "unknown error"}`);
|
|
248
|
+
}
|
|
249
|
+
const setCookie = login.res.headers.get("set-cookie") ?? "";
|
|
250
|
+
const cookie = cookieHeaderFromSetCookie(setCookie);
|
|
251
|
+
if (!cookie) throw new Error("login response missing session cookie");
|
|
252
|
+
|
|
253
|
+
const session = await writeSavedSessionImpl({
|
|
254
|
+
sessionPath: state.sessionFile,
|
|
255
|
+
session: {
|
|
256
|
+
baseUrl,
|
|
257
|
+
tenantId,
|
|
258
|
+
email: state.email,
|
|
259
|
+
cookie,
|
|
260
|
+
expiresAt: typeof login.json?.expiresAt === "string" ? login.json.expiresAt : null
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const payload = {
|
|
265
|
+
ok: true,
|
|
266
|
+
schemaVersion: "SettldLoginResult.v1",
|
|
267
|
+
baseUrl: session.baseUrl,
|
|
268
|
+
tenantId: session.tenantId,
|
|
269
|
+
email: session.email,
|
|
270
|
+
sessionFile: state.sessionFile,
|
|
271
|
+
expiresAt: session.expiresAt ?? null
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
if (state.format === "json") {
|
|
275
|
+
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
276
|
+
} else {
|
|
277
|
+
stdout.write(`Login saved.\n`);
|
|
278
|
+
stdout.write(`Tenant: ${session.tenantId}\n`);
|
|
279
|
+
stdout.write(`Session file: ${state.sessionFile}\n`);
|
|
280
|
+
stdout.write(`Next: run \`settld setup\`.\n`);
|
|
281
|
+
}
|
|
282
|
+
return payload;
|
|
283
|
+
} finally {
|
|
284
|
+
if (rl) rl.close();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function main(argv = process.argv.slice(2)) {
|
|
289
|
+
try {
|
|
290
|
+
await runLogin({ argv });
|
|
291
|
+
} catch (err) {
|
|
292
|
+
process.stderr.write(`${err?.message ?? String(err)}\n`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
298
|
+
main();
|
|
299
|
+
}
|