@mybase-dev/cli 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/mybase.js +288 -18
  2. package/package.json +1 -1
package/dist/mybase.js CHANGED
@@ -116,6 +116,8 @@ var KEY_RESOURCES = [
116
116
  { id: "vector", name: "Vector search", description: "Indexes, documents, and queries (read), and changing them (write)" },
117
117
  { id: "users", name: "Users", description: "Your app's users and sessions (read), revoking and deleting them, and sending sign-in events (write)" },
118
118
  { id: "captcha", name: "Captcha", description: "Captcha settings (read), and verifying tokens (write)" },
119
+ { id: "realtime", name: "Realtime", description: "Channels, members, and settings (read), and publishing messages (write)" },
120
+ { id: "sandbox", name: "Agent sandboxes", description: "Sandboxes, executions, files, artifacts, and browser connections" },
119
121
  { id: "audit", name: "Audit log", description: "Who did what: sign-ins, key and project changes, secret reads (read only)" }
120
122
  ];
121
123
  function parseDotenv(text) {
@@ -139,7 +141,7 @@ function parseDotenv(text) {
139
141
  // package.json
140
142
  var package_default = {
141
143
  name: "@mybase-dev/cli",
142
- version: "1.0.7",
144
+ version: "1.0.9",
143
145
  description: "The mybase CLI: projects, keys, cron, queues, cache, rate limits, analytics, docs, and agent setup for a self-hosted mybase.",
144
146
  type: "module",
145
147
  repository: {
@@ -261,15 +263,18 @@ function confirm(yes, warning, expected, what = expected) {
261
263
  var config = await loadConfig();
262
264
  var API_URL = (process.env.MYBASE_API_URL || config.url || DEFAULT_API_URL).replace(/\/$/, "");
263
265
  var TOKEN = process.env.MYBASE_TOKEN || config.token;
266
+ var apiToken = () => TOKEN;
264
267
  var CLI_VERSION = package_default.version;
265
268
  async function api(path, init) {
266
269
  const token = init?.token === undefined ? TOKEN : init.token;
267
270
  const base = init?.url ?? API_URL;
271
+ const headers = new Headers({ "Content-Type": "application/json", "User-Agent": `mybase-cli/${CLI_VERSION}`, ...token ? { Authorization: `Bearer ${token}` } : {} });
272
+ new Headers(init?.headers).forEach((value, key) => headers.set(key, value));
268
273
  let res;
269
274
  try {
270
275
  res = await fetch(`${base}${path}`, {
271
276
  ...init,
272
- headers: { "Content-Type": "application/json", "User-Agent": `mybase-cli/${CLI_VERSION}`, ...token ? { Authorization: `Bearer ${token}` } : {} }
277
+ headers
273
278
  });
274
279
  } catch {
275
280
  throw new CliError(`can't reach the API at ${base}. Check that it's running, or point MYBASE_API_URL / \`mybase login --url\` at it.`);
@@ -277,6 +282,8 @@ async function api(path, init) {
277
282
  init?.onStatus?.(res.status);
278
283
  if (res.status === 204)
279
284
  return;
285
+ if (res.ok && init?.raw)
286
+ return res;
280
287
  const body = await res.json().catch(() => ({}));
281
288
  if (res.status === 401 && !path.startsWith("/auth/login")) {
282
289
  const message = token ? "the API rejected your token. Run `mybase login` again." : "not signed in. Run `mybase login` (or set MYBASE_TOKEN).";
@@ -652,6 +659,9 @@ function contextMarkdown(c) {
652
659
  }
653
660
  if (c.captcha)
654
661
  out.push("## Captcha", "", `Script: ${c.captcha.scriptUrl} (data-project="${c.project}"). Check tokens with \`createCaptchaClient\` (\`captcha:write\` key) or Better Auth's captcha plugin via \`mybaseCaptcha()\`.`, "");
662
+ if (c.realtime) {
663
+ out.push("## Realtime", "", `Browsers: \`connectRealtime({ apiUrl, project: "${c.project}", token })\` (socket ${c.realtime.socketUrl}) with tokens your server signs via \`createRealtimeToken(MYBASE_REALTIME_SECRET, \u2026)\`. Publish from the server: POST ${c.realtime.publishUrl} (\`realtime:write\` key, \`createRealtimeClient\`).`, "");
664
+ }
655
665
  if (c.analytics)
656
666
  out.push("## Analytics", "", `Public write key \`${c.analytics.writeKey}\`, script ${c.analytics.scriptUrl}`, "");
657
667
  out.push("## Env vars", "", ...c.env.map((e) => `- \`${e.name}\`: ${e.description}`), "");
@@ -2077,6 +2087,145 @@ async function sync4(file, dryRun) {
2077
2087
  printSync(config.project, dryRun, await api(`${base6(config.project)}/rules`, { method: "PUT", body: JSON.stringify(body) }));
2078
2088
  }
2079
2089
 
2090
+ // src/realtime.ts
2091
+ var realtimeUsage = ` realtime enable <project> Turn on realtime and print the socket URL and secret
2092
+ realtime info <project> Settings, connections, and messages in the last 24 hours
2093
+ realtime key <project> [--rotate] Show (or replace) the secret that signs browser tokens and webhooks
2094
+ (rotating closes every open connection)
2095
+ realtime settings <project> [--origins a,b|none] [--public on|off] [--webhook <url>|none]
2096
+ [--events message,presence,channel] [--max-connections n]
2097
+ [--max-channels n] [--max-message-bytes n] [--max-rate n]
2098
+ realtime channels <project> [--prefix p] Channels with subscribers right now
2099
+ realtime publish <project> <channel> <event> [json] Send a message to everyone subscribed
2100
+ realtime disable <project> [--yes] Turn it off (closes every connection)`;
2101
+ var base7 = (project) => `/projects/${encodeURIComponent(project)}/realtime`;
2102
+ function printSettings3(s) {
2103
+ console.log(` socket ${s.socketUrl} (connectRealtime in @mybase-dev/sdk/realtime)`);
2104
+ console.log(` publish POST ${s.publishUrl} (realtime:write key)`);
2105
+ console.log(` secret ${s.secret} (server-side only: MYBASE_REALTIME_SECRET)`);
2106
+ console.log(` public ${s.publicChannels ? "on: public:\u2026 channels need no token" : "off: every connection needs a token"}`);
2107
+ console.log(` origins ${s.allowedOrigins.length ? s.allowedOrigins.join(", ") : "any"}`);
2108
+ console.log(` webhook ${s.webhookUrl ? `${s.webhookUrl} (${s.webhookEvents.join(", ") || "no events"})` : "none"}`);
2109
+ console.log(` limits ${s.maxConnections} connections, ${s.maxChannels} channels each, ${s.maxMessageBytes} B frames, ${s.maxRate}/s`);
2110
+ }
2111
+ var list2 = (value) => value === "none" ? [] : value.split(",").map((v) => v.trim()).filter(Boolean);
2112
+ function onOff(value, name) {
2113
+ if (value === undefined)
2114
+ return;
2115
+ if (value === "on" || value === "off")
2116
+ return value === "on";
2117
+ throw usageError(`--${name} takes on or off`);
2118
+ }
2119
+ async function realtime(sub, args) {
2120
+ const { positional, flags } = parseArgs(args);
2121
+ const [project, channel, event, data] = positional;
2122
+ switch (sub) {
2123
+ case "enable": {
2124
+ const p = need(project, "project name");
2125
+ const { linked } = await enableService(p, "realtime", flags);
2126
+ const s = await api(base7(p));
2127
+ print({ enabled: "realtime", project: p, linked, ...s }, () => {
2128
+ console.log(`Realtime enabled for "${p}"
2129
+ `);
2130
+ printSettings3(s);
2131
+ printLinked(p, linked, flags);
2132
+ });
2133
+ return true;
2134
+ }
2135
+ case "info": {
2136
+ const p = need(project, "project name");
2137
+ const [s, stats] = await Promise.all([api(base7(p)), api(`${base7(p)}/stats`)]);
2138
+ const { secret: _secret, ...settings } = s;
2139
+ print({ ...settings, stats }, () => {
2140
+ printSettings3({ ...s, secret: "(mybase realtime key)" });
2141
+ const out = stats.hours.reduce((n, h) => n + h.messagesOut, 0);
2142
+ console.log(`
2143
+ now ${stats.connections} connections (peak ${stats.peakConnections}), ${stats.channels} channels`);
2144
+ console.log(` 24 hours ${out} messages delivered`);
2145
+ if (stats.webhook.lastError)
2146
+ console.log(` webhook failing: ${stats.webhook.lastError} (${stats.webhook.queued} queued, ${stats.webhook.dropped} dropped)`);
2147
+ });
2148
+ return true;
2149
+ }
2150
+ case "key": {
2151
+ const p = need(project, "project name");
2152
+ if (flags.has("rotate"))
2153
+ confirm(args.includes("--yes"), `Every open realtime connection on "${p}" closes, and tokens signed with the old secret stop working.`, p, "the project name");
2154
+ const s = flags.has("rotate") ? await api(`${base7(p)}/secret/rotate`, { method: "POST" }) : await api(base7(p));
2155
+ print({ secret: s.secret }, () => {
2156
+ if (flags.has("rotate"))
2157
+ console.log(`Rotated. Update MYBASE_REALTIME_SECRET where your server signs tokens.
2158
+ `);
2159
+ console.log(s.secret);
2160
+ });
2161
+ return true;
2162
+ }
2163
+ case "settings": {
2164
+ const p = need(project, "project name");
2165
+ const body = {};
2166
+ const origins = flagString(flags, "origins");
2167
+ if (origins !== undefined)
2168
+ body.allowedOrigins = list2(origins);
2169
+ const pub = onOff(flagString(flags, "public"), "public");
2170
+ if (pub !== undefined)
2171
+ body.publicChannels = pub;
2172
+ const webhook = flagString(flags, "webhook");
2173
+ if (webhook !== undefined)
2174
+ body.webhookUrl = webhook === "none" ? null : webhook;
2175
+ const events = flagString(flags, "events");
2176
+ if (events !== undefined)
2177
+ body.webhookEvents = list2(events);
2178
+ const ints = { maxConnections: "max-connections", maxChannels: "max-channels", maxMessageBytes: "max-message-bytes", maxRate: "max-rate" };
2179
+ for (const [key, flag] of Object.entries(ints)) {
2180
+ const n = flagInt(flags, flag);
2181
+ if (n !== undefined)
2182
+ body[key] = n;
2183
+ }
2184
+ const s = Object.keys(body).length ? await api(base7(p), { method: "PATCH", body: JSON.stringify(body) }) : await api(base7(p));
2185
+ const { secret: _secret, ...shown } = s;
2186
+ print(shown, () => printSettings3({ ...s, secret: "(mybase realtime key)" }));
2187
+ return true;
2188
+ }
2189
+ case "channels": {
2190
+ const p = need(project, "project name");
2191
+ const prefix = flagString(flags, "prefix");
2192
+ const { channels } = await api(`${base7(p)}/channels${prefix ? `?prefix=${encodeURIComponent(prefix)}` : ""}`);
2193
+ print(channels, () => {
2194
+ if (!channels.length)
2195
+ return console.log("No channels have subscribers right now.");
2196
+ for (const c of channels)
2197
+ console.log(`${c.channel} ${c.subscribers} subscribers${c.members === undefined ? "" : `, ${c.members} members`}`);
2198
+ });
2199
+ return true;
2200
+ }
2201
+ case "publish": {
2202
+ const p = need(project, "project name");
2203
+ let payload = null;
2204
+ if (data !== undefined) {
2205
+ try {
2206
+ payload = JSON.parse(data);
2207
+ } catch {
2208
+ payload = data;
2209
+ }
2210
+ }
2211
+ const { ids } = await api(`${base7(p)}/publish`, {
2212
+ method: "POST",
2213
+ body: JSON.stringify({ channel: need(channel, "channel"), event: need(event, "event"), data: payload })
2214
+ });
2215
+ print({ id: ids[0] }, () => console.log(`Published ${event} to ${channel}`));
2216
+ return true;
2217
+ }
2218
+ case "disable": {
2219
+ const p = need(project, "project name");
2220
+ confirm(args.includes("--yes"), `Every open realtime connection on "${p}" closes, and pages listening for updates stop getting them.`, p, "the project name");
2221
+ await api(`/projects/${encodeURIComponent(p)}/services/realtime`, { method: "DELETE" });
2222
+ print({ disabled: "realtime", project: p }, () => console.log(`Realtime disabled for "${p}"`));
2223
+ return true;
2224
+ }
2225
+ }
2226
+ return false;
2227
+ }
2228
+
2080
2229
  // src/run.ts
2081
2230
  import { constants } from "os";
2082
2231
 
@@ -2113,8 +2262,8 @@ async function target(flags, needEnv = true) {
2113
2262
  }
2114
2263
  return { project, env: env ?? "" };
2115
2264
  }
2116
- var base7 = (project) => `/projects/${encodeURIComponent(project)}/secrets`;
2117
- var envPath = (project, env, rest = "") => `${base7(project)}/envs/${encodeURIComponent(env)}${rest}`;
2265
+ var base8 = (project) => `/projects/${encodeURIComponent(project)}/secrets`;
2266
+ var envPath = (project, env, rest = "") => `${base8(project)}/envs/${encodeURIComponent(env)}${rest}`;
2118
2267
  var fetchSecrets = (project, env) => api(`/secrets/${encodeURIComponent(project)}/${encodeURIComponent(env)}`);
2119
2268
  var describeEnv = (e) => [e.name, e.parent ? `branch of ${e.parent}` : "root", `${e.keys} keys`, `rev ${e.revision}`, e.linked ? "linked" : ""].filter(Boolean).join("\t");
2120
2269
  async function readStdin() {
@@ -2133,7 +2282,7 @@ async function secrets(sub, args) {
2133
2282
  case "enable": {
2134
2283
  const p = need(positional[0] ?? flagString(flags, "project"), "project name");
2135
2284
  await api(`/projects/${encodeURIComponent(p)}/services/secrets`, { method: "PUT" });
2136
- const overview = await api(base7(p));
2285
+ const overview = await api(base8(p));
2137
2286
  print({ enabled: "secrets", project: p, ...overview }, () => {
2138
2287
  console.log(`Secrets enabled for "${p}": ${overview.envs.map((e) => e.name).join(", ")}
2139
2288
  `);
@@ -2153,7 +2302,7 @@ async function secrets(sub, args) {
2153
2302
  }
2154
2303
  case "setup": {
2155
2304
  const { project, env } = await target(flags);
2156
- const overview = await api(base7(project));
2305
+ const overview = await api(base8(project));
2157
2306
  if (!overview.envs.some((e) => e.name === env)) {
2158
2307
  throw usageError(`"${project}" has no env "${env}" (envs: ${overview.envs.map((e) => e.name).join(", ")})`);
2159
2308
  }
@@ -2163,7 +2312,7 @@ async function secrets(sub, args) {
2163
2312
  }
2164
2313
  case "envs": {
2165
2314
  const { project } = await target(flags, false);
2166
- const overview = await api(base7(project));
2315
+ const overview = await api(base8(project));
2167
2316
  print(overview.envs, () => {
2168
2317
  for (const e of overview.envs)
2169
2318
  console.log(describeEnv(e));
@@ -2174,7 +2323,7 @@ async function secrets(sub, args) {
2174
2323
  const [action, name, next] = positional;
2175
2324
  const { project } = await target(flags, false);
2176
2325
  if (action === "create") {
2177
- const env = await api(`${base7(project)}/envs`, {
2326
+ const env = await api(`${base8(project)}/envs`, {
2178
2327
  method: "POST",
2179
2328
  body: JSON.stringify({ name: need(name, "env name"), parent: flagString(flags, "from") ?? null })
2180
2329
  });
@@ -2277,7 +2426,7 @@ async function secrets(sub, args) {
2277
2426
  const { project, env } = await target(flags);
2278
2427
  const envs = env.split(",").map((e) => e.trim()).filter(Boolean);
2279
2428
  confirm(yes, `This rotates the credentials of "${project}" (database password, storage key, cache password if the cache is on, and the API key link made before) and stores the new ones, with the other services' keys, in ${envs.join(", ")} plus any env linked before. Apps still using the old credentials will stop working.`, project, "the project name");
2280
- const result = await api(`${base7(project)}/link`, { method: "POST", body: JSON.stringify({ envs }) });
2429
+ const result = await api(`${base8(project)}/link`, { method: "POST", body: JSON.stringify({ envs }) });
2281
2430
  print(result, () => {
2282
2431
  console.log(`Rotated and stored in ${result.envs.join(", ")}: ${result.keys.join(", ")}`);
2283
2432
  console.log("Restart your apps with `mybase run` to pick them up.");
@@ -2287,7 +2436,7 @@ async function secrets(sub, args) {
2287
2436
  case "unlink": {
2288
2437
  const { project, env } = await target(flags);
2289
2438
  confirm(yes, `This removes the project credentials from "${env}". They stay valid; nothing is rotated.`, env, "the env name");
2290
- await api(`${base7(project)}/link/${encodeURIComponent(env)}`, { method: "DELETE" });
2439
+ await api(`${base8(project)}/link/${encodeURIComponent(env)}`, { method: "DELETE" });
2291
2440
  print({ unlinked: env, project }, () => console.log(`Removed the credentials from "${env}"`));
2292
2441
  return true;
2293
2442
  }
@@ -2365,6 +2514,93 @@ async function search(sub, args) {
2365
2514
  }
2366
2515
  }
2367
2516
 
2517
+ // src/sandbox.ts
2518
+ var sandboxUsage = ` sandbox enable <project> Enable isolated agent compute (requires sandbox worker)
2519
+ sandbox disable <project> [--yes] Stop sandboxes and delete their artifacts
2520
+ sandbox create <project> --key <key> [--ttl 300] [--idle 120]
2521
+ sandbox list|get|usage <project> [<id>]
2522
+ sandbox terminate <project> <id> [--yes]
2523
+ sandbox exec <project> <id> --key <key> [--code file.js | --command '["node","script.js"]'] [--timeout 30000]
2524
+ sandbox runs|artifacts|browser <project> <id>
2525
+ sandbox run <project> <id> <execution>
2526
+ sandbox cancel <project> <id> <execution> [--yes]
2527
+ sandbox upload|download <project> <id> <workspace-path> <local-file>
2528
+ sandbox export <project> <id> <workspace-path> [--type image/png]
2529
+ sandbox artifact <project> <id> <artifact-id> <local-file>
2530
+ Creation/execution return promptly. Poll get/run. Cancelling execution terminates sandbox.
2531
+ Browser credentials expire after 30s; authenticated CDP connection lasts at most 60s.`;
2532
+ async function sandbox(sub, args) {
2533
+ const known = ["enable", "disable", "create", "list", "get", "usage", "terminate", "exec", "runs", "run", "cancel", "upload", "download", "export", "artifacts", "artifact", "browser"];
2534
+ if (!known.includes(sub))
2535
+ return false;
2536
+ const { positional, flags } = parseArgs(args);
2537
+ const [project, id, item, local] = positional;
2538
+ const p = need(project, "project name");
2539
+ const base = `/sandbox/${encodeURIComponent(p)}`;
2540
+ const root = () => `${base}/${encodeURIComponent(need(id, "sandbox id"))}`;
2541
+ const value = (key) => {
2542
+ const found = flags.get(key);
2543
+ return typeof found === "string" ? found : undefined;
2544
+ };
2545
+ const number = (key) => value(key) === undefined ? undefined : Number(value(key));
2546
+ let result;
2547
+ if (sub === "enable") {
2548
+ result = await enableService(p, "sandbox", flags);
2549
+ } else if (sub === "disable") {
2550
+ confirm(flags.has("yes"), `This stops every sandbox and permanently deletes sandbox artifacts for "${p}".`, p, "the project name");
2551
+ await api(`/projects/${encodeURIComponent(p)}/services/sandbox`, { method: "DELETE" });
2552
+ result = { disabled: "sandbox", project: p };
2553
+ } else if (sub === "create")
2554
+ result = await api(base, { method: "POST", body: JSON.stringify({ key: need(value("key"), "--key"), ttlSeconds: number("ttl"), idleSeconds: number("idle"), profile: "browser" }) });
2555
+ else if (sub === "list" || sub === "usage")
2556
+ result = await api(sub === "list" ? base : `${base}/usage`);
2557
+ else if (sub === "get")
2558
+ result = await api(root());
2559
+ else if (sub === "terminate") {
2560
+ confirm(flags.has("yes"), `This stops sandbox "${need(id, "sandbox id")}" and deletes its temporary workspace.`, id, "the sandbox id");
2561
+ result = await api(root(), { method: "DELETE" });
2562
+ } else if (sub === "exec") {
2563
+ const codePath = value("code");
2564
+ const command = value("command");
2565
+ if (!!codePath === !!command)
2566
+ usageError(`provide exactly one of --code file.js or --command '["node","script.js"]'`);
2567
+ let argv;
2568
+ if (command) {
2569
+ try {
2570
+ argv = JSON.parse(command);
2571
+ } catch {
2572
+ usageError("--command must be a JSON argument array");
2573
+ }
2574
+ }
2575
+ result = await api(`${root()}/executions`, { method: "POST", body: JSON.stringify({ key: need(value("key"), "--key"), code: codePath ? await Bun.file(codePath).text() : undefined, command: argv, timeoutMs: number("timeout") }) });
2576
+ } else if (sub === "runs")
2577
+ result = await api(`${root()}/executions`);
2578
+ else if (sub === "run" || sub === "cancel") {
2579
+ if (sub === "cancel")
2580
+ confirm(flags.has("yes"), `Cancelling stops the entire sandbox "${need(id, "sandbox id")}" and deletes its temporary workspace.`, id, "the sandbox id");
2581
+ result = await api(`${root()}/executions/${encodeURIComponent(need(item, "execution id"))}`, { method: sub === "run" ? "GET" : "DELETE" });
2582
+ } else if (sub === "upload") {
2583
+ const bytes = await Bun.file(need(local, "local file")).arrayBuffer();
2584
+ await api(`${root()}/files?path=${encodeURIComponent(need(item, "workspace path"))}`, { method: "PUT", body: bytes, headers: { "Content-Type": "application/octet-stream" } });
2585
+ result = { uploaded: item, bytes: bytes.byteLength };
2586
+ } else if (sub === "download" || sub === "artifact") {
2587
+ const path = sub === "download" ? `${root()}/files?path=${encodeURIComponent(need(item, "workspace path"))}` : `${root()}/artifacts/${encodeURIComponent(need(item, "artifact id"))}`;
2588
+ const response = await api(path, { raw: true });
2589
+ const size = await Bun.write(need(local, "local file"), response);
2590
+ result = { file: local, bytes: size };
2591
+ } else if (sub === "export")
2592
+ result = await api(`${root()}/artifacts`, { method: "POST", body: JSON.stringify({ path: need(item, "workspace path"), type: value("type") }) });
2593
+ else if (sub === "artifacts")
2594
+ result = await api(`${root()}/artifacts`);
2595
+ else if (sub === "browser") {
2596
+ const connection = await api(`${root()}/browser`, { method: "POST", body: "{}" });
2597
+ connection.headers.Authorization = `Bearer ${apiToken()}`;
2598
+ result = connection;
2599
+ }
2600
+ print(result, () => console.log(JSON.stringify(result, null, 2)));
2601
+ return true;
2602
+ }
2603
+
2368
2604
  // ../../node_modules/.bun/@clack+core@1.5.1/node_modules/@clack/core/dist/index.mjs
2369
2605
  import { styleText } from "util";
2370
2606
  import { stdout, stdin } from "process";
@@ -3755,6 +3991,9 @@ var SERVICE_COLORS = {
3755
3991
  alerts: color.red,
3756
3992
  users: color.green,
3757
3993
  captcha: color.blue,
3994
+ realtime: color.magenta,
3995
+ sandbox: color.yellow,
3996
+ "sandbox-worker": color.yellow,
3758
3997
  "db-public": color.cyan,
3759
3998
  dev: color.gray
3760
3999
  };
@@ -3940,6 +4179,8 @@ services:
3940
4179
  # 1 for one restart turns the admin's 2FA off (lost phone and recovery codes).
3941
4180
  MYBASE_ADMIN_RESET_MFA: \${MYBASE_ADMIN_RESET_MFA:-}
3942
4181
  LOG_LEVEL: \${LOG_LEVEL:-info}
4182
+ MYBASE_SANDBOX_WORKER_URL: \${MYBASE_SANDBOX_WORKER_URL:-}
4183
+ MYBASE_SANDBOX_WORKER_KEY: \${MYBASE_SANDBOX_WORKER_KEY:-}
3943
4184
  ports:
3944
4185
  - "\${MYBASE_BIND:-0.0.0.0}:\${MYBASE_PORT:-4000}:4000"
3945
4186
 
@@ -4017,6 +4258,27 @@ services:
4017
4258
  - source: searxng
4018
4259
  target: /etc/searxng/settings.yml
4019
4260
 
4261
+ # Opt-in trusted agent host. Requires runsc/cgroup v2 on the Docker host and a pre-pulled
4262
+ # sandbox image. See docs/sandboxes.md. No guest ever receives this socket or worker key.
4263
+ sandbox-worker:
4264
+ image: \${MYBASE_SANDBOX_WORKER_IMAGE:-ghcr.io/landonharter/mybase-sandbox-worker:latest}
4265
+ profiles: [sandbox]
4266
+ restart: \${MYBASE_RESTART:-unless-stopped}
4267
+ stop_grace_period: 45s
4268
+ environment:
4269
+ MYBASE_SANDBOX_WORKER_KEY: \${MYBASE_SANDBOX_WORKER_KEY:-}
4270
+ MYBASE_SANDBOX_DENY_HOSTS: \${MYBASE_SANDBOX_DENY_HOSTS:-}
4271
+ MYBASE_SANDBOX_IMAGE: \${MYBASE_SANDBOX_IMAGE:-ghcr.io/landonharter/mybase-sandbox:latest}
4272
+ LOG_LEVEL: \${LOG_LEVEL:-info}
4273
+ volumes:
4274
+ - /var/run/docker.sock:/var/run/docker.sock
4275
+ healthcheck:
4276
+ test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:4010/health',{headers:{Authorization:'Bearer '+process.env.MYBASE_SANDBOX_WORKER_KEY}}).then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
4277
+ interval: 30s
4278
+ timeout: 5s
4279
+ start_period: 30s
4280
+ # Private Compose network only. No published worker or browser-debugging ports.
4281
+
4020
4282
  # Cloudflare Tunnel: public HTTPS without opening router ports. Off unless COMPOSE_PROFILES=tunnel.
4021
4283
  # Hostnames are set up in the Cloudflare dashboard (see the top of this file).
4022
4284
  cloudflared:
@@ -5682,7 +5944,7 @@ var usersUsage = ` users enable <project> Turn on users: an auth schema
5682
5944
  users revoke <project> <user-id> [<session-id>] Sign a user out (one session, or everywhere)
5683
5945
  users delete <project> <user-id> [--yes] Delete a user and their sessions
5684
5946
  users disable <project> [--yes] Turn it off, deleting every user account`;
5685
- var base8 = (project) => `/projects/${encodeURIComponent(project)}/users`;
5947
+ var base9 = (project) => `/projects/${encodeURIComponent(project)}/users`;
5686
5948
  function authSnippet(project, apiUrl) {
5687
5949
  return `// auth.ts: npm i better-auth pg @mybase-dev/sdk
5688
5950
  import { betterAuth } from "better-auth";
@@ -5713,7 +5975,7 @@ async function users(sub, args) {
5713
5975
  case "enable": {
5714
5976
  const p = need(project, "project name");
5715
5977
  const { linked } = await enableService(p, "users", flags);
5716
- const o = await api(base8(p));
5978
+ const o = await api(base9(p));
5717
5979
  print({ enabled: "users", project: p, linked, ...o }, () => {
5718
5980
  console.log(`Users enabled for "${p}". Better Auth's tables go in the "${o.schema}" schema.
5719
5981
  `);
@@ -5727,7 +5989,7 @@ async function users(sub, args) {
5727
5989
  }
5728
5990
  case "init": {
5729
5991
  const p = need(project, "project name");
5730
- const o = await api(base8(p));
5992
+ const o = await api(base9(p));
5731
5993
  const env = flagString(flags, "env");
5732
5994
  let stored = null;
5733
5995
  if (env) {
@@ -5750,7 +6012,7 @@ Set BETTER_AUTH_SECRET to 32+ random bytes (openssl rand -base64 32), or rerun w
5750
6012
  case "list": {
5751
6013
  const p = need(project, "project name");
5752
6014
  const search = flagString(flags, "search");
5753
- const list = await api(`${base8(p)}/accounts?limit=100${search ? `&search=${encodeURIComponent(search)}` : ""}`);
6015
+ const list = await api(`${base9(p)}/accounts?limit=100${search ? `&search=${encodeURIComponent(search)}` : ""}`);
5754
6016
  print(list, () => {
5755
6017
  if (list.users.length === 0)
5756
6018
  console.log(search ? "No users match." : "No users yet.");
@@ -5763,7 +6025,7 @@ Set BETTER_AUTH_SECRET to 32+ random bytes (openssl rand -base64 32), or rerun w
5763
6025
  }
5764
6026
  case "show": {
5765
6027
  const p = need(project, "project name");
5766
- const u = await api(`${base8(p)}/accounts/${encodeURIComponent(need(userId, "user id"))}`);
6028
+ const u = await api(`${base9(p)}/accounts/${encodeURIComponent(need(userId, "user id"))}`);
5767
6029
  print(u, () => {
5768
6030
  console.log(`${u.email ?? u.id}${u.name ? ` (${u.name})` : ""}${u.banned ? " BANNED" : ""}`);
5769
6031
  console.log(` id ${u.id}`);
@@ -5781,10 +6043,10 @@ Active sessions (${u.sessions.length}):`);
5781
6043
  const p = need(project, "project name");
5782
6044
  const id = encodeURIComponent(need(userId, "user id"));
5783
6045
  if (sessionId) {
5784
- await api(`${base8(p)}/accounts/${id}/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
6046
+ await api(`${base9(p)}/accounts/${id}/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
5785
6047
  print({ revoked: 1 }, () => console.log("Signed out that session."));
5786
6048
  } else {
5787
- const r = await api(`${base8(p)}/accounts/${id}/sessions`, { method: "DELETE" });
6049
+ const r = await api(`${base9(p)}/accounts/${id}/sessions`, { method: "DELETE" });
5788
6050
  print(r, () => console.log(`Signed out ${r.revoked} session(s).`));
5789
6051
  }
5790
6052
  return true;
@@ -5793,7 +6055,7 @@ Active sessions (${u.sessions.length}):`);
5793
6055
  const p = need(project, "project name");
5794
6056
  const id = need(userId, "user id");
5795
6057
  confirm(args.includes("--yes"), `This deletes user ${id} of "${p}" and signs them out.`, id, "the user id");
5796
- await api(`${base8(p)}/accounts/${encodeURIComponent(id)}`, { method: "DELETE" });
6058
+ await api(`${base9(p)}/accounts/${encodeURIComponent(id)}`, { method: "DELETE" });
5797
6059
  print({ deleted: id }, () => console.log(`Deleted user ${id}`));
5798
6060
  return true;
5799
6061
  }
@@ -5831,10 +6093,12 @@ var USAGE = {
5831
6093
  ratelimit: ratelimitUsage,
5832
6094
  queue: queueUsage,
5833
6095
  search: searchUsage,
6096
+ sandbox: sandboxUsage,
5834
6097
  secrets: secretsUsage,
5835
6098
  vector: vectorUsage,
5836
6099
  users: usersUsage,
5837
6100
  captcha: captchaUsage,
6101
+ realtime: realtimeUsage,
5838
6102
  run: runUsage
5839
6103
  };
5840
6104
  var GLOBAL_FLAGS = ` --json Print results as JSON (errors too, on stderr)
@@ -5877,6 +6141,8 @@ async function main(args) {
5877
6141
  return audit(args.slice(1));
5878
6142
  if (command === "alerts")
5879
6143
  return alerts(args.slice(1));
6144
+ if (command === "sandbox" && sub && await sandbox(sub, rest))
6145
+ return;
5880
6146
  if (command === "project" && sub === "create") {
5881
6147
  const name = need(rest[0], "project name");
5882
6148
  const project = await api("/projects", {
@@ -5967,6 +6233,10 @@ async function main(args) {
5967
6233
  if (await captcha(sub, rest))
5968
6234
  return;
5969
6235
  }
6236
+ if (command === "realtime" && sub) {
6237
+ if (await realtime(sub, rest))
6238
+ return;
6239
+ }
5970
6240
  if (command && command !== "help")
5971
6241
  throw usageError(`unknown command: mybase ${args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" ")}
5972
6242
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mybase-dev/cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "The mybase CLI: projects, keys, cron, queues, cache, rate limits, analytics, docs, and agent setup for a self-hosted mybase.",
5
5
  "type": "module",
6
6
  "repository": {