lakebed 0.0.20 → 0.0.21

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 CHANGED
@@ -142,7 +142,7 @@ npx lakebed build [capsule-dir] --target anonymous [--out .lakebed/artifacts/app
142
142
  npx lakebed deploy [capsule-dir] [--api <url>] [--public-inspect] [--json]
143
143
  npx lakebed claim [capsule-dir] [--api <url>] [--json]
144
144
  npx lakebed domains add <subdomain.lakebed.app> [--api <url>] [--json]
145
- npx lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
145
+ npx lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--dashboard-root-url <url>] [--app-base-domain <domain>] [--role all|api-dashboard|runner]
146
146
  npx lakebed inspect <deploy-id-or-url> [--api <url>] [--inspect-token <token>] [--json]
147
147
  npx lakebed db list [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
148
148
  npx lakebed db dump [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
@@ -179,10 +179,19 @@ npx lakebed deploy --api http://localhost:8787
179
179
  In production, set `PUBLIC_ROOT_URL` to the deploy API origin and `LAKEBED_APP_BASE_DOMAIN` to the app domain without the wildcard prefix:
180
180
 
181
181
  ```sh
182
- PUBLIC_ROOT_URL=https://api.lakebed.app
182
+ PUBLIC_ROOT_URL=https://api.lakebed.dev
183
+ LAKEBED_DASHBOARD_ROOT_URL=https://dashboard.lakebed.dev
183
184
  LAKEBED_APP_BASE_DOMAIN=lakebed.app
184
185
  ```
185
186
 
187
+ The staged Railway split uses dedicated service entrypoints:
188
+
189
+ ```sh
190
+ node packages/lakebed/src/services/all-in-one.js
191
+ node packages/lakebed/src/services/api-dashboard.js
192
+ node packages/lakebed/src/services/capsule-runner.js
193
+ ```
194
+
186
195
  With a verified `*.lakebed.app` custom domain on the runner, deploy responses use `https://<slug>.lakebed.app`.
187
196
 
188
197
  Anonymous deploy creation is relaxed for local `localhost` runners. When `PUBLIC_ROOT_URL` points at a non-local origin, the runner defaults to 50 anonymous deploy creates per forwarded client per UTC day and 5,000 globally. Override those with:
@@ -205,7 +214,7 @@ LAKEBED_ANONYMOUS_CLEANUP_RETENTION=7d
205
214
  LAKEBED_ANONYMOUS_CLEANUP_INTERVAL=1h
206
215
  ```
207
216
 
208
- Deploy responses include claim metadata. Configure GitHub OAuth on the runner, then run `npx lakebed claim` to open the claim page and attach the anonymous deploy to a developer account:
217
+ Deploy responses include claim metadata. Configure GitHub OAuth on the API and dashboard service, then run `npx lakebed claim` to open the claim page and attach the anonymous deploy to a developer account:
209
218
 
210
219
  ```sh
211
220
  LAKEBED_GITHUB_CLIENT_ID=...
@@ -214,7 +223,7 @@ LAKEBED_SESSION_SECRET=...
214
223
  LAKEBED_SERVER_ENV_SECRET=...
215
224
  ```
216
225
 
217
- Claimed deploys are listed at `/deploys` on the deploy API origin. They keep the same resource limits as anonymous deploys and do not expire. Anonymous deploys cannot use outbound `fetch` or hosted server env; after a deploy is claimed, `npx lakebed deploy` can update it with a source-backed server artifact that supports async handlers, server-side fetch, and `.env.lakebed.server` sync. If the first deploy already needs server-side `fetch` or server env, `npx lakebed deploy` creates a claim-required preview, saves its claim metadata, and prints the `npx lakebed claim` command. Run that command, then run `npx lakebed deploy` again to publish the real source-backed app. Set `LAKEBED_SERVER_ENV_SECRET` on Postgres-backed runners to encrypt stored server env values.
226
+ Claimed deploys are listed at `/deploys` on the dashboard origin. They keep the same resource limits as anonymous deploys and do not expire. Anonymous deploys cannot use outbound `fetch` or hosted server env; after a deploy is claimed, `npx lakebed deploy` can update it with a source-backed server artifact that supports async handlers, server-side fetch, and `.env.lakebed.server` sync. If the first deploy already needs server-side `fetch` or server env, `npx lakebed deploy` creates a claim-required preview, saves its claim metadata, and prints the `npx lakebed claim` command. Run that command, then run `npx lakebed deploy` again to publish the real source-backed app. Set `LAKEBED_SERVER_ENV_SECRET` on Postgres-backed runners to encrypt stored server env values.
218
227
 
219
228
  Hosted inspection is private by default. `npx lakebed inspect`, `npx lakebed db list`, `npx lakebed db dump`, and `npx lakebed logs` send the saved claim token automatically when run from the capsule directory.
220
229
 
@@ -228,4 +237,4 @@ Reserved product names such as `api`, `admin`, `docs`, and `www` are rejected.
228
237
 
229
238
  ## Admin Dashboard
230
239
 
231
- Set `LAKEBED_ADMIN_PASSWORD` on the anonymous deploy runner, then open `/admin` on the runner origin. The password is exchanged for an HttpOnly cookie so the dashboard stays unlocked until the cookie expires or the password changes. The resource table can terminate active deploys while preserving their resource history, and the users table can set per-user request and mutation limit overrides for claimed deploys.
240
+ Set `LAKEBED_ADMIN_PASSWORD` on the API and dashboard service, then open `/admin` on the dashboard origin. The password is exchanged for an HttpOnly cookie so the dashboard stays unlocked until the cookie expires or the password changes. The resource table can terminate active deploys while preserving their resource history, and the users table can set per-user request and mutation limit overrides for claimed deploys.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lakebed",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "Agent-native CLI and runtime for building and deploying Lakebed capsules.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -29,6 +29,9 @@
29
29
  "src/runtime.js",
30
30
  "src/server.d.ts",
31
31
  "src/server.js",
32
+ "src/services/all-in-one.js",
33
+ "src/services/api-dashboard.js",
34
+ "src/services/capsule-runner.js",
32
35
  "src/source-runtime-loader.mjs",
33
36
  "src/source-runtime-worker.js",
34
37
  "src/source-runtime.js",
@@ -52,9 +55,6 @@
52
55
  "publishConfig": {
53
56
  "access": "public"
54
57
  },
55
- "scripts": {
56
- "check": "node --check src/cli.js && node --check src/runtime.js && node --check src/server.js && node --check src/client.js && node --check src/source-store.js && node --check src/source-runtime.js && node --check src/source-runtime-worker.js && node --check src/source-runtime-loader.mjs && node --check src/anonymous.js && node --check src/anonymous-server.js && node --check src/auth.js && node --check src/version.js"
57
- },
58
58
  "dependencies": {
59
59
  "esbuild": "^0.27.1",
60
60
  "pg": "^8.16.3",
@@ -63,5 +63,8 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/ws": "^8.18.1"
66
+ },
67
+ "scripts": {
68
+ "check": "node --check src/cli.js && node --check src/runtime.js && node --check src/server.js && node --check src/client.js && node --check src/source-store.js && node --check src/source-runtime.js && node --check src/source-runtime-worker.js && node --check src/source-runtime-loader.mjs && node --check src/anonymous.js && node --check src/anonymous-server.js && node --check src/auth.js && node --check src/version.js && node --check src/services/all-in-one.js && node --check src/services/api-dashboard.js && node --check src/services/capsule-runner.js"
66
69
  }
67
- }
70
+ }
@@ -405,16 +405,47 @@ function normalizeLakebedSubdomainInput(value, appBaseDomain) {
405
405
  };
406
406
  }
407
407
 
408
- function appUrlForSlug({ appBaseDomain, publicRootUrl, slug }) {
409
- if (appBaseDomain) {
410
- return `https://${slug}.${appBaseDomain}`;
408
+ function normalizeServerRole(value) {
409
+ const role = String(value ?? "all").trim().toLowerCase();
410
+ if (role === "all" || role === "api-dashboard" || role === "runner") {
411
+ return role;
411
412
  }
412
413
 
413
- return `${publicRootUrl}/d/${slug}`;
414
+ throw new Error(`Unsupported LAKEBED_SERVER_ROLE: ${role}. Expected all, api-dashboard, or runner.`);
414
415
  }
415
416
 
416
- function claimUrlForDeploy({ deployId, publicRootUrl, token }) {
417
- return `${publicRootUrl}/claim/${deployId}/${token}`;
417
+ function roleServesApiDashboard(role) {
418
+ return role === "all" || role === "api-dashboard";
419
+ }
420
+
421
+ function roleServesRunner(role) {
422
+ return role === "all" || role === "runner";
423
+ }
424
+
425
+ function isApiDashboardRoute(pathname) {
426
+ return (
427
+ pathname === "/deploys" ||
428
+ pathname === "/dashboard" ||
429
+ pathname === "/auth" ||
430
+ (pathname.startsWith("/auth/") && pathname !== "/auth/callback") ||
431
+ pathname === "/admin" ||
432
+ pathname.startsWith("/admin/") ||
433
+ pathname === "/v1" ||
434
+ pathname.startsWith("/v1/") ||
435
+ pathname.startsWith("/claim/")
436
+ );
437
+ }
438
+
439
+ function appUrlForSlug({ appBaseDomain, slug }) {
440
+ if (!appBaseDomain) {
441
+ throw new Error("LAKEBED_APP_BASE_DOMAIN is required to create hosted deploy URLs.");
442
+ }
443
+
444
+ return `https://${slug}.${appBaseDomain}`;
445
+ }
446
+
447
+ function claimUrlForDeploy({ dashboardRootUrl, deployId, token }) {
448
+ return `${dashboardRootUrl}/claim/${deployId}/${token}`;
418
449
  }
419
450
 
420
451
  function inspectUrls(url) {
@@ -426,11 +457,11 @@ function inspectUrls(url) {
426
457
  };
427
458
  }
428
459
 
429
- function responseForDeploy({ deploy, token }) {
460
+ function responseForDeploy({ dashboardRootUrl, deploy, token }) {
430
461
  return {
431
462
  claimed: Boolean(deploy.ownerId),
432
463
  claimedAt: deploy.claimedAt ?? undefined,
433
- claimUrl: token ? claimUrlForDeploy({ deployId: deploy.id, publicRootUrl: deploy.publicRootUrl, token }) : undefined,
464
+ claimUrl: token ? claimUrlForDeploy({ dashboardRootUrl: dashboardRootUrl ?? deploy.publicRootUrl, deployId: deploy.id, token }) : undefined,
434
465
  deployId: deploy.id,
435
466
  expiresAt: deploy.expiresAt,
436
467
  inspect: inspectUrls(deploy.url),
@@ -494,21 +525,6 @@ function isClientShellRequest(req, pathname) {
494
525
  return req.method === "GET" && wantsHtml(req) && !isReservedClientShellPath(pathname);
495
526
  }
496
527
 
497
- function parsePathDeploy(url) {
498
- const parts = url.pathname.split("/").filter(Boolean);
499
- if (parts[0] !== "d" || !parts[1]) {
500
- return null;
501
- }
502
-
503
- const prefix = `/d/${parts[1]}`;
504
- const appPath = url.pathname === prefix ? "/" : url.pathname.slice(prefix.length) || "/";
505
- return {
506
- appPath,
507
- basePath: prefix,
508
- slug: parts[1]
509
- };
510
- }
511
-
512
528
  function parseHostDeploy({ appBaseDomain, host, url }) {
513
529
  if (!appBaseDomain) {
514
530
  return null;
@@ -532,6 +548,10 @@ function parseHostDeploy({ appBaseDomain, host, url }) {
532
548
  };
533
549
  }
534
550
 
551
+ function isDisabledPathDeployRoute(pathname) {
552
+ return pathname === "/d" || pathname.startsWith("/d/");
553
+ }
554
+
535
555
  function quotaLimitForBucket(bucket, deploy) {
536
556
  if (bucket === "mutations") {
537
557
  return deploy.limits.mutationsPerDay;
@@ -5716,7 +5736,7 @@ async function loadDeployByRoute({ appBaseDomain, host, store, url }) {
5716
5736
  hostname: domain.hostname,
5717
5737
  deployId: domain.deployId
5718
5738
  }
5719
- : parseHostDeploy({ appBaseDomain, host, url }) ?? parsePathDeploy(url);
5739
+ : parseHostDeploy({ appBaseDomain, host, url });
5720
5740
  if (!route) {
5721
5741
  return null;
5722
5742
  }
@@ -5934,17 +5954,23 @@ async function serveInspect({ adminPassword, artifact, currentDeveloper, deploy,
5934
5954
  export async function startAnonymousServer({
5935
5955
  adminPassword = adminPasswordFromEnv(),
5936
5956
  appBaseDomain = process.env.LAKEBED_APP_BASE_DOMAIN ?? "",
5957
+ dashboardRootUrl = process.env.LAKEBED_DASHBOARD_ROOT_URL,
5937
5958
  developerSessionSecret = process.env.LAKEBED_SESSION_SECRET ?? "",
5938
5959
  githubOAuth = githubOAuthFromEnv(),
5939
5960
  port = Number(process.env.PORT ?? 8787),
5940
5961
  publicRootUrl,
5941
5962
  quiet = false,
5963
+ role = process.env.LAKEBED_SERVER_ROLE ?? "all",
5942
5964
  shooBaseUrl = shooBaseUrlFromEnv(),
5943
5965
  sourceRuntime,
5944
5966
  store
5945
5967
  } = {}) {
5968
+ const resolvedRole = normalizeServerRole(role);
5969
+ const servesApiDashboard = roleServesApiDashboard(resolvedRole);
5970
+ const servesRunner = roleServesRunner(resolvedRole);
5946
5971
  const resolvedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
5947
5972
  const resolvedPublicRootUrl = normalizePublicRootUrl(publicRootUrl ?? process.env.PUBLIC_ROOT_URL, port);
5973
+ const resolvedDashboardRootUrl = normalizePublicRootUrl(dashboardRootUrl ?? resolvedPublicRootUrl, port);
5948
5974
  const deployCreationPolicy = anonymousDeployCreationPolicy({ publicRootUrl: resolvedPublicRootUrl });
5949
5975
  const clientTrafficPolicy = anonymousClientTrafficPolicy({ publicRootUrl: resolvedPublicRootUrl });
5950
5976
  const cleanupPolicy = cleanupPolicyFromEnv();
@@ -5952,7 +5978,7 @@ export async function startAnonymousServer({
5952
5978
  const resolvedDeveloperSessionSecret =
5953
5979
  developerSessionSecret || resolvedGithubOAuth?.sessionSecret || resolvedGithubOAuth?.clientSecret || adminPassword || "";
5954
5980
  const resolvedStore = store ?? (await createAnonymousStoreFromEnv());
5955
- const resolvedSourceRuntime = sourceRuntime === undefined ? createSourceRuntimeFromEnv() : sourceRuntime;
5981
+ const resolvedSourceRuntime = sourceRuntime === undefined && servesRunner ? createSourceRuntimeFromEnv() : sourceRuntime;
5956
5982
  await resolvedStore.initialize();
5957
5983
  const subscriptions = new Map();
5958
5984
  let cleanupInterval = null;
@@ -6191,7 +6217,7 @@ export async function startAnonymousServer({
6191
6217
  return;
6192
6218
  }
6193
6219
 
6194
- if (req.method === "GET" && (requestUrl.pathname === "/deploys" || requestUrl.pathname === "/dashboard")) {
6220
+ if (servesApiDashboard && req.method === "GET" && (requestUrl.pathname === "/deploys" || requestUrl.pathname === "/dashboard")) {
6195
6221
  const authConfigured = developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret);
6196
6222
  const user = authConfigured ? currentDeveloper(req) : null;
6197
6223
  sendText(
@@ -6207,7 +6233,7 @@ export async function startAnonymousServer({
6207
6233
  return;
6208
6234
  }
6209
6235
 
6210
- if (req.method === "GET" && requestUrl.pathname === "/v1/me") {
6236
+ if (servesApiDashboard && req.method === "GET" && requestUrl.pathname === "/v1/me") {
6211
6237
  const user = currentDeveloper(req);
6212
6238
  if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret) || !user) {
6213
6239
  sendJson(res, 401, { error: "Developer authentication required." });
@@ -6218,7 +6244,7 @@ export async function startAnonymousServer({
6218
6244
  return;
6219
6245
  }
6220
6246
 
6221
- if (req.method === "GET" && requestUrl.pathname === "/v1/me/deploys") {
6247
+ if (servesApiDashboard && req.method === "GET" && requestUrl.pathname === "/v1/me/deploys") {
6222
6248
  const user = currentDeveloper(req);
6223
6249
  if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret) || !user) {
6224
6250
  sendJson(res, 401, { error: "Developer authentication required." });
@@ -6230,7 +6256,7 @@ export async function startAnonymousServer({
6230
6256
  }
6231
6257
 
6232
6258
  const developerTerminateMatch =
6233
- req.method === "POST" ? requestUrl.pathname.match(/^\/v1\/me\/deploys\/([^/]+)\/terminate$/) : null;
6259
+ servesApiDashboard && req.method === "POST" ? requestUrl.pathname.match(/^\/v1\/me\/deploys\/([^/]+)\/terminate$/) : null;
6234
6260
  if (developerTerminateMatch) {
6235
6261
  const user = currentDeveloper(req);
6236
6262
  if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret) || !user) {
@@ -6266,7 +6292,7 @@ export async function startAnonymousServer({
6266
6292
  return;
6267
6293
  }
6268
6294
 
6269
- if (req.method === "GET" && requestUrl.pathname === "/auth/github") {
6295
+ if (servesApiDashboard && req.method === "GET" && requestUrl.pathname === "/auth/github") {
6270
6296
  if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret)) {
6271
6297
  sendText(res, 503, "GitHub sign-in is not configured.\n", { "Content-Type": "text/plain; charset=utf-8" });
6272
6298
  return;
@@ -6284,7 +6310,7 @@ export async function startAnonymousServer({
6284
6310
  );
6285
6311
  const authorizeUrl = new URL(resolvedGithubOAuth.authorizeUrl);
6286
6312
  authorizeUrl.searchParams.set("client_id", resolvedGithubOAuth.clientId);
6287
- authorizeUrl.searchParams.set("redirect_uri", githubRedirectUri(resolvedGithubOAuth, resolvedPublicRootUrl));
6313
+ authorizeUrl.searchParams.set("redirect_uri", githubRedirectUri(resolvedGithubOAuth, resolvedDashboardRootUrl));
6288
6314
  authorizeUrl.searchParams.set("scope", "read:user");
6289
6315
  authorizeUrl.searchParams.set("state", state);
6290
6316
  redirect(res, authorizeUrl.href, {
@@ -6293,7 +6319,7 @@ export async function startAnonymousServer({
6293
6319
  return;
6294
6320
  }
6295
6321
 
6296
- if (req.method === "GET" && requestUrl.pathname === "/auth/github/callback") {
6322
+ if (servesApiDashboard && req.method === "GET" && requestUrl.pathname === "/auth/github/callback") {
6297
6323
  if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret)) {
6298
6324
  sendText(res, 503, "GitHub sign-in is not configured.\n", { "Content-Type": "text/plain; charset=utf-8" });
6299
6325
  return;
@@ -6339,7 +6365,7 @@ export async function startAnonymousServer({
6339
6365
  const user = await githubUserFromCode({
6340
6366
  code,
6341
6367
  githubOAuth: resolvedGithubOAuth,
6342
- redirectUri: githubRedirectUri(resolvedGithubOAuth, resolvedPublicRootUrl)
6368
+ redirectUri: githubRedirectUri(resolvedGithubOAuth, resolvedDashboardRootUrl)
6343
6369
  });
6344
6370
  redirect(res, normalizeReturnTo(statePayload.returnTo), {
6345
6371
  "Set-Cookie": [developerCookie(user, resolvedDeveloperSessionSecret, secure), clearOauthStateCookie(secure)]
@@ -6347,14 +6373,14 @@ export async function startAnonymousServer({
6347
6373
  return;
6348
6374
  }
6349
6375
 
6350
- if (req.method === "GET" && requestUrl.pathname === "/auth/logout") {
6376
+ if (servesApiDashboard && req.method === "GET" && requestUrl.pathname === "/auth/logout") {
6351
6377
  redirect(res, "/deploys", {
6352
6378
  "Set-Cookie": clearDeveloperCookie(isSecureRequest(req))
6353
6379
  });
6354
6380
  return;
6355
6381
  }
6356
6382
 
6357
- const claimMatch = req.method === "GET" ? requestUrl.pathname.match(/^\/claim\/([^/]+)\/([^/]+)$/) : null;
6383
+ const claimMatch = servesApiDashboard && req.method === "GET" ? requestUrl.pathname.match(/^\/claim\/([^/]+)\/([^/]+)$/) : null;
6358
6384
  if (claimMatch) {
6359
6385
  if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret)) {
6360
6386
  sendText(res, 503, "GitHub sign-in is not configured.\n", { "Content-Type": "text/plain; charset=utf-8" });
@@ -6392,6 +6418,7 @@ export async function startAnonymousServer({
6392
6418
  }
6393
6419
 
6394
6420
  if (
6421
+ servesApiDashboard &&
6395
6422
  req.method === "GET" &&
6396
6423
  (requestUrl.pathname === "/admin" ||
6397
6424
  requestUrl.pathname === "/admin/" ||
@@ -6403,7 +6430,7 @@ export async function startAnonymousServer({
6403
6430
  return;
6404
6431
  }
6405
6432
 
6406
- if (requestUrl.pathname === "/admin/api/login" && req.method === "POST") {
6433
+ if (servesApiDashboard && requestUrl.pathname === "/admin/api/login" && req.method === "POST") {
6407
6434
  if (!isAdminConfigured(adminPassword)) {
6408
6435
  sendJson(res, 503, { error: "Lakebed admin is not configured. Set LAKEBED_ADMIN_PASSWORD." });
6409
6436
  return;
@@ -6419,12 +6446,12 @@ export async function startAnonymousServer({
6419
6446
  return;
6420
6447
  }
6421
6448
 
6422
- if (requestUrl.pathname === "/admin/api/logout" && req.method === "POST") {
6449
+ if (servesApiDashboard && requestUrl.pathname === "/admin/api/logout" && req.method === "POST") {
6423
6450
  sendJson(res, 200, { ok: true }, { "Set-Cookie": adminCookie("", 0, isSecureRequest(req)) });
6424
6451
  return;
6425
6452
  }
6426
6453
 
6427
- if (requestUrl.pathname === "/admin/api/summary" && req.method === "GET") {
6454
+ if (servesApiDashboard && requestUrl.pathname === "/admin/api/summary" && req.method === "GET") {
6428
6455
  if (!isAdminConfigured(adminPassword)) {
6429
6456
  sendJson(res, 503, { error: "Lakebed admin is not configured. Set LAKEBED_ADMIN_PASSWORD." });
6430
6457
  return;
@@ -6440,7 +6467,7 @@ export async function startAnonymousServer({
6440
6467
  }
6441
6468
 
6442
6469
  const adminUserDetailMatch =
6443
- req.method === "GET" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)$/) : null;
6470
+ servesApiDashboard && req.method === "GET" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)$/) : null;
6444
6471
  if (adminUserDetailMatch) {
6445
6472
  if (!isAdminConfigured(adminPassword)) {
6446
6473
  sendJson(res, 503, { error: "Lakebed admin is not configured. Set LAKEBED_ADMIN_PASSWORD." });
@@ -6463,7 +6490,7 @@ export async function startAnonymousServer({
6463
6490
  }
6464
6491
 
6465
6492
  const userLimitsMatch =
6466
- req.method === "PUT" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)\/limits$/) : null;
6493
+ servesApiDashboard && req.method === "PUT" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)\/limits$/) : null;
6467
6494
  if (userLimitsMatch) {
6468
6495
  if (!isAdminConfigured(adminPassword)) {
6469
6496
  sendJson(res, 503, { error: "Lakebed admin is not configured. Set LAKEBED_ADMIN_PASSWORD." });
@@ -6499,7 +6526,7 @@ export async function startAnonymousServer({
6499
6526
  }
6500
6527
 
6501
6528
  const terminateMatch =
6502
- req.method === "POST"
6529
+ servesApiDashboard && req.method === "POST"
6503
6530
  ? requestUrl.pathname.match(/^\/admin\/api\/deploys\/([^/]+)\/terminate$/)
6504
6531
  : null;
6505
6532
  if (terminateMatch) {
@@ -6533,7 +6560,7 @@ export async function startAnonymousServer({
6533
6560
  }
6534
6561
 
6535
6562
  const deployDomainsMatch = requestUrl.pathname.match(/^\/v1\/deploys\/([^/]+)\/domains$/);
6536
- if (deployDomainsMatch && (req.method === "GET" || req.method === "POST")) {
6563
+ if (servesApiDashboard && deployDomainsMatch && (req.method === "GET" || req.method === "POST")) {
6537
6564
  const deployId = decodeURIComponent(deployDomainsMatch[1]);
6538
6565
  const currentDeploy = await resolvedStore.getDeployById(deployId);
6539
6566
  if (!currentDeploy) {
@@ -6616,7 +6643,12 @@ export async function startAnonymousServer({
6616
6643
  return;
6617
6644
  }
6618
6645
 
6619
- if (req.method === "POST" && requestUrl.pathname === "/v1/anonymous-deploys") {
6646
+ if (servesApiDashboard && req.method === "POST" && requestUrl.pathname === "/v1/anonymous-deploys") {
6647
+ if (!resolvedAppBaseDomain) {
6648
+ sendJson(res, 503, { error: "LAKEBED_APP_BASE_DOMAIN is required to create hosted deploys." });
6649
+ return;
6650
+ }
6651
+
6620
6652
  await enforceAnonymousDeployCreation(req);
6621
6653
  const body = await readJsonBody(req);
6622
6654
  const payload = validateAnonymousDeployPayload(body);
@@ -6642,11 +6674,16 @@ export async function startAnonymousServer({
6642
6674
  serverEnv: payload.serverEnv
6643
6675
  });
6644
6676
  await resolvedStore.appendLog(deploy.id, "info", "anonymous deploy created", { artifactHash: deploy.artifactHash });
6645
- sendJson(res, 201, responseForDeploy({ deploy, token }));
6677
+ sendJson(res, 201, responseForDeploy({ dashboardRootUrl: resolvedDashboardRootUrl, deploy, token }));
6646
6678
  return;
6647
6679
  }
6648
6680
 
6649
- if ((req.method === "PUT" || req.method === "PATCH") && requestUrl.pathname.startsWith("/v1/deploys/")) {
6681
+ if (servesApiDashboard && (req.method === "PUT" || req.method === "PATCH") && requestUrl.pathname.startsWith("/v1/deploys/")) {
6682
+ if (!resolvedAppBaseDomain) {
6683
+ sendJson(res, 503, { error: "LAKEBED_APP_BASE_DOMAIN is required to update hosted deploys." });
6684
+ return;
6685
+ }
6686
+
6650
6687
  const deployId = requestUrl.pathname.slice("/v1/deploys/".length);
6651
6688
  const currentDeploy = await resolvedStore.getDeployById(deployId);
6652
6689
  if (!currentDeploy) {
@@ -6692,23 +6729,43 @@ export async function startAnonymousServer({
6692
6729
  await refreshDeploySubscriptions(deploy);
6693
6730
  refreshDeployClients(deploy);
6694
6731
  await publishDeploy(deploy.id);
6695
- sendJson(res, 200, responseForDeploy({ deploy }));
6732
+ sendJson(res, 200, responseForDeploy({ dashboardRootUrl: resolvedDashboardRootUrl, deploy }));
6696
6733
  return;
6697
6734
  }
6698
6735
 
6699
- if (req.method === "GET" && requestUrl.pathname.startsWith("/v1/deploys/")) {
6736
+ if (servesApiDashboard && req.method === "GET" && requestUrl.pathname.startsWith("/v1/deploys/")) {
6700
6737
  const id = requestUrl.pathname.slice("/v1/deploys/".length);
6701
6738
  const deploy = (await resolvedStore.getDeployById(id)) ?? (await resolvedStore.getDeployBySlug(id));
6702
6739
  if (!deploy) {
6703
6740
  sendJson(res, 404, { error: "Unknown deploy." });
6704
6741
  return;
6705
6742
  }
6706
- sendJson(res, 200, responseForDeploy({ deploy }));
6743
+ sendJson(res, 200, responseForDeploy({ dashboardRootUrl: resolvedDashboardRootUrl, deploy }));
6744
+ return;
6745
+ }
6746
+
6747
+ if (isDisabledPathDeployRoute(requestUrl.pathname)) {
6748
+ sendText(res, 404, "Path-based deploy URLs are no longer supported.\n", { "Content-Type": "text/plain; charset=utf-8" });
6749
+ return;
6750
+ }
6751
+
6752
+ if (!servesRunner) {
6753
+ if (req.method === "GET" && requestUrl.pathname === "/") {
6754
+ redirect(res, "/deploys");
6755
+ return;
6756
+ }
6757
+
6758
+ sendText(res, 404, "Not found\n", { "Content-Type": "text/plain; charset=utf-8" });
6707
6759
  return;
6708
6760
  }
6709
6761
 
6710
6762
  const loaded = await loadDeployByRoute({ appBaseDomain: resolvedAppBaseDomain, host, store: resolvedStore, url: requestUrl });
6711
6763
  if (!loaded) {
6764
+ if (!servesApiDashboard && isApiDashboardRoute(requestUrl.pathname)) {
6765
+ sendText(res, 404, "Not found\n", { "Content-Type": "text/plain; charset=utf-8" });
6766
+ return;
6767
+ }
6768
+
6712
6769
  sendText(res, 200, "Lakebed anonymous deploy runner\n", { "Content-Type": "text/plain; charset=utf-8" });
6713
6770
  return;
6714
6771
  }
@@ -6919,6 +6976,11 @@ export async function startAnonymousServer({
6919
6976
  });
6920
6977
 
6921
6978
  server.on("upgrade", async (req, socket, head) => {
6979
+ if (!servesRunner) {
6980
+ socket.destroy();
6981
+ return;
6982
+ }
6983
+
6922
6984
  const host = req.headers.host ?? "localhost";
6923
6985
  const requestUrl = new URL(req.url ?? "/", `http://${host}`);
6924
6986
  const loaded = await loadDeployByRoute({ appBaseDomain: resolvedAppBaseDomain, host, store: resolvedStore, url: requestUrl });
@@ -6971,8 +7033,10 @@ export async function startAnonymousServer({
6971
7033
 
6972
7034
  return {
6973
7035
  appBaseDomain: resolvedAppBaseDomain,
7036
+ dashboardRootUrl: resolvedDashboardRootUrl,
6974
7037
  port,
6975
7038
  publicRootUrl: resolvedPublicRootUrl,
7039
+ role: resolvedRole,
6976
7040
  store: resolvedStore,
6977
7041
  url: resolvedPublicRootUrl,
6978
7042
  async close() {
package/src/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile } from "node:child_process";
3
- import { createServer } from "node:http";
3
+ import { createServer, request as httpRequest } from "node:http";
4
+ import { request as httpsRequest } from "node:https";
4
5
  import { existsSync, realpathSync } from "node:fs";
5
6
  import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
6
7
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -29,7 +30,7 @@ const root = process.cwd();
29
30
  const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
30
31
  const packageNodeModules = resolve(packageDir, "node_modules");
31
32
  const sourceNamespace = "lakebed-source";
32
- const defaultDeployApiUrl = "https://api.lakebed.app";
33
+ const defaultDeployApiUrl = "https://api.lakebed.dev";
33
34
  const execFileAsync = promisify(execFile);
34
35
  const endpointBodyMaxBytes = 2 * 1024 * 1024;
35
36
 
@@ -44,7 +45,7 @@ Usage:
44
45
  npx lakebed deploy [capsule-dir] [--api <url>] [--public-inspect] [--json]
45
46
  npx lakebed claim [capsule-dir] [--api <url>] [--json]
46
47
  npx lakebed domains add <subdomain.lakebed.app> [--api <url>] [--json]
47
- npx lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
48
+ npx lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--dashboard-root-url <url>] [--app-base-domain <domain>] [--role all|api-dashboard|runner]
48
49
  npx lakebed inspect <deploy-id-or-url> [--api <url>] [--inspect-token <token>] [--json]
49
50
  npx lakebed run-many [capsule-dir] [--count 20] [--base-port 4000]
50
51
  npx lakebed auth as <name>
@@ -69,10 +70,12 @@ const optionsWithValues = new Set([
69
70
  "--app-base-domain",
70
71
  "--base-port",
71
72
  "--count",
73
+ "--dashboard-root-url",
72
74
  "--inspect-token",
73
75
  "--out",
74
76
  "--port",
75
77
  "--public-root-url",
78
+ "--role",
76
79
  "--target",
77
80
  "--template"
78
81
  ]);
@@ -1590,8 +1593,10 @@ async function anonymousServerCommand(args) {
1590
1593
  const port = readNumberArg(args, "--port", Number(process.env.PORT ?? 8787));
1591
1594
  await startAnonymousServer({
1592
1595
  appBaseDomain: readArg(args, "--app-base-domain", process.env.LAKEBED_APP_BASE_DOMAIN ?? ""),
1596
+ dashboardRootUrl: readArg(args, "--dashboard-root-url", process.env.LAKEBED_DASHBOARD_ROOT_URL),
1593
1597
  port,
1594
- publicRootUrl: readArg(args, "--public-root-url", process.env.PUBLIC_ROOT_URL ?? `http://localhost:${port}`)
1598
+ publicRootUrl: readArg(args, "--public-root-url", process.env.PUBLIC_ROOT_URL ?? `http://localhost:${port}`),
1599
+ role: readArg(args, "--role", process.env.LAKEBED_SERVER_ROLE ?? "all")
1595
1600
  });
1596
1601
  await new Promise(() => {});
1597
1602
  }
@@ -1620,6 +1625,61 @@ async function resolveDeployUrl(target, args, metadata) {
1620
1625
  }
1621
1626
  }
1622
1627
 
1628
+ async function resolveHostedTarget(target, args, metadata) {
1629
+ if (!target) {
1630
+ throw new Error("Expected a deploy ID or URL.");
1631
+ }
1632
+
1633
+ try {
1634
+ const url = new URL(target);
1635
+ return { api: "", url: url.href.replace(/\/+$/g, "") };
1636
+ } catch {
1637
+ const api = deployLookupApiUrl(target, args, metadata);
1638
+ const response = await fetch(`${api}/v1/deploys/${encodeURIComponent(target)}`);
1639
+ const deploy = await readResponseJson(response);
1640
+ return { api, url: deploy.url.replace(/\/+$/g, "") };
1641
+ }
1642
+ }
1643
+
1644
+ function isLocalApiUrl(value) {
1645
+ try {
1646
+ const { hostname } = new URL(value);
1647
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
1648
+ } catch {
1649
+ return false;
1650
+ }
1651
+ }
1652
+
1653
+ async function requestWithHostHeader(url, headers) {
1654
+ const requestUrl = new URL(url);
1655
+ const transport = requestUrl.protocol === "https:" ? httpsRequest : httpRequest;
1656
+ return new Promise((resolveRequest, rejectRequest) => {
1657
+ const req = transport(
1658
+ {
1659
+ headers,
1660
+ hostname: requestUrl.hostname,
1661
+ method: "GET",
1662
+ path: `${requestUrl.pathname}${requestUrl.search}`,
1663
+ port: requestUrl.port
1664
+ },
1665
+ (res) => {
1666
+ const chunks = [];
1667
+ res.on("data", (chunk) => chunks.push(chunk));
1668
+ res.on("end", () => {
1669
+ const body = Buffer.concat(chunks).toString("utf8");
1670
+ resolveRequest({
1671
+ ok: (res.statusCode ?? 500) >= 200 && (res.statusCode ?? 500) < 300,
1672
+ status: res.statusCode ?? 500,
1673
+ text: async () => body
1674
+ });
1675
+ });
1676
+ }
1677
+ );
1678
+ req.on("error", rejectRequest);
1679
+ req.end();
1680
+ });
1681
+ }
1682
+
1623
1683
  function inspectTokenForHostedTarget({ args, metadata, target, url }) {
1624
1684
  const explicitToken = readArg(args, "--inspect-token", process.env.LAKEBED_INSPECT_TOKEN ?? "");
1625
1685
  if (explicitToken) {
@@ -1642,11 +1702,22 @@ function inspectTokenForHostedTarget({ args, metadata, target, url }) {
1642
1702
 
1643
1703
  async function hostedJson(target, path, args) {
1644
1704
  const metadata = await readDeployMetadata(root);
1645
- const url = await resolveDeployUrl(target, args, metadata);
1705
+ const { api, url } = await resolveHostedTarget(target, args, metadata);
1646
1706
  const inspectToken = inspectTokenForHostedTarget({ args, metadata, target, url });
1647
- const response = await fetch(`${url}${path}`, {
1648
- headers: inspectToken ? { Authorization: `Bearer ${inspectToken}` } : {}
1649
- });
1707
+ const headers = inspectToken ? { Authorization: `Bearer ${inspectToken}` } : {};
1708
+ let response;
1709
+ try {
1710
+ response = await fetch(`${url}${path}`, { headers });
1711
+ } catch (error) {
1712
+ if (!api || !isLocalApiUrl(api)) {
1713
+ throw error;
1714
+ }
1715
+
1716
+ response = await requestWithHostHeader(`${api}${path}`, {
1717
+ ...headers,
1718
+ Host: new URL(url).host
1719
+ });
1720
+ }
1650
1721
  return readResponseJson(response);
1651
1722
  }
1652
1723
 
@@ -1761,7 +1832,7 @@ async function dbCommand(args) {
1761
1832
  }
1762
1833
 
1763
1834
  function agentInstructionsTemplate() {
1764
- return `# Building with Lakebed.
1835
+ return `# Lakebed App Instructions
1765
1836
 
1766
1837
  This directory is for a Lakebed "capsule". Lakebed is an all-inclusive suite of tools to build web applications purely from code and a CLI.
1767
1838
 
@@ -1814,6 +1885,10 @@ npx lakebed db dump --port 3000
1814
1885
  npx lakebed logs --port 3000
1815
1886
  \`\`\`
1816
1887
 
1888
+ ## External endpoints
1889
+
1890
+ Use \`endpoint({ method, path }, handler)\` from \`lakebed/server\` when the app needs to expose an HTTP route for webhooks or other non-Lakebed clients. Endpoint handlers receive request data including \`headers.get(name)\`, URL params, query params, and body helpers.
1891
+
1817
1892
  ## Additional resources
1818
1893
 
1819
1894
  - [Lakebed docs](https://docs.lakebed.dev/)
@@ -0,0 +1,5 @@
1
+ import { startAnonymousServer } from "../anonymous-server.js";
2
+
3
+ await startAnonymousServer({
4
+ role: "all"
5
+ });
@@ -0,0 +1,7 @@
1
+ import { startAnonymousServer } from "../anonymous-server.js";
2
+
3
+ await startAnonymousServer({
4
+ dashboardRootUrl: process.env.LAKEBED_DASHBOARD_ROOT_URL ?? "https://dashboard.lakebed.dev",
5
+ publicRootUrl: process.env.PUBLIC_ROOT_URL ?? "https://api.lakebed.dev",
6
+ role: "api-dashboard"
7
+ });
@@ -0,0 +1,5 @@
1
+ import { startAnonymousServer } from "../anonymous-server.js";
2
+
3
+ await startAnonymousServer({
4
+ role: "runner"
5
+ });
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export const LAKEBED_VERSION = "0.0.20";
1
+ export const LAKEBED_VERSION = "0.0.21";