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 +14 -5
- package/package.json +8 -5
- package/src/anonymous-server.js +112 -48
- package/src/cli.js +84 -9
- package/src/services/all-in-one.js +5 -0
- package/src/services/api-dashboard.js +7 -0
- package/src/services/capsule-runner.js +5 -0
- package/src/version.js +1 -1
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
}
|
package/src/anonymous-server.js
CHANGED
|
@@ -405,16 +405,47 @@ function normalizeLakebedSubdomainInput(value, appBaseDomain) {
|
|
|
405
405
|
};
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
414
|
+
throw new Error(`Unsupported LAKEBED_SERVER_ROLE: ${role}. Expected all, api-dashboard, or runner.`);
|
|
414
415
|
}
|
|
415
416
|
|
|
416
|
-
function
|
|
417
|
-
return
|
|
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({
|
|
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 })
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
|
1705
|
+
const { api, url } = await resolveHostedTarget(target, args, metadata);
|
|
1646
1706
|
const inspectToken = inspectTokenForHostedTarget({ args, metadata, target, url });
|
|
1647
|
-
const
|
|
1648
|
-
|
|
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 `#
|
|
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,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
|
+
});
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const LAKEBED_VERSION = "0.0.
|
|
1
|
+
export const LAKEBED_VERSION = "0.0.21";
|