lakebed 0.0.20 → 0.0.22
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 +5 -2
- package/src/anonymous-server.js +167 -48
- package/src/cli.js +101 -16
- 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.22",
|
|
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",
|
|
@@ -53,7 +56,7 @@
|
|
|
53
56
|
"access": "public"
|
|
54
57
|
},
|
|
55
58
|
"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"
|
|
59
|
+
"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"
|
|
57
60
|
},
|
|
58
61
|
"dependencies": {
|
|
59
62
|
"esbuild": "^0.27.1",
|
package/src/anonymous-server.js
CHANGED
|
@@ -405,16 +405,89 @@ 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 isDashboardBrowserRoute(pathname) {
|
|
440
|
+
return (
|
|
441
|
+
pathname === "/" ||
|
|
442
|
+
pathname === "/deploys" ||
|
|
443
|
+
pathname === "/dashboard" ||
|
|
444
|
+
pathname === "/auth" ||
|
|
445
|
+
pathname.startsWith("/auth/") ||
|
|
446
|
+
pathname === "/admin" ||
|
|
447
|
+
pathname.startsWith("/admin/") ||
|
|
448
|
+
pathname.startsWith("/claim/")
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function normalizedUrlHost(value) {
|
|
453
|
+
try {
|
|
454
|
+
return new URL(value).host.toLowerCase();
|
|
455
|
+
} catch {
|
|
456
|
+
return "";
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function dashboardRedirectLocation({ dashboardRootUrl, requestUrl }) {
|
|
461
|
+
const pathname = requestUrl.pathname === "/" ? "/deploys" : requestUrl.pathname;
|
|
462
|
+
const target = new URL(pathname, dashboardRootUrl);
|
|
463
|
+
target.search = requestUrl.search;
|
|
464
|
+
return target.href;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function shouldRedirectToDashboardHost({ dashboardRootUrl, host, publicRootUrl, requestUrl }) {
|
|
468
|
+
if (!isDashboardBrowserRoute(requestUrl.pathname)) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const publicHost = normalizedUrlHost(publicRootUrl);
|
|
473
|
+
const dashboardHost = normalizedUrlHost(dashboardRootUrl);
|
|
474
|
+
if (!publicHost || !dashboardHost || publicHost === dashboardHost) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return String(host).toLowerCase() === publicHost;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function appUrlForSlug({ appBaseDomain, slug }) {
|
|
482
|
+
if (!appBaseDomain) {
|
|
483
|
+
throw new Error("LAKEBED_APP_BASE_DOMAIN is required to create hosted deploy URLs.");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return `https://${slug}.${appBaseDomain}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function claimUrlForDeploy({ dashboardRootUrl, deployId, token }) {
|
|
490
|
+
return `${dashboardRootUrl}/claim/${deployId}/${token}`;
|
|
418
491
|
}
|
|
419
492
|
|
|
420
493
|
function inspectUrls(url) {
|
|
@@ -426,11 +499,11 @@ function inspectUrls(url) {
|
|
|
426
499
|
};
|
|
427
500
|
}
|
|
428
501
|
|
|
429
|
-
function responseForDeploy({ deploy, token }) {
|
|
502
|
+
function responseForDeploy({ dashboardRootUrl, deploy, token }) {
|
|
430
503
|
return {
|
|
431
504
|
claimed: Boolean(deploy.ownerId),
|
|
432
505
|
claimedAt: deploy.claimedAt ?? undefined,
|
|
433
|
-
claimUrl: token ? claimUrlForDeploy({
|
|
506
|
+
claimUrl: token ? claimUrlForDeploy({ dashboardRootUrl: dashboardRootUrl ?? deploy.publicRootUrl, deployId: deploy.id, token }) : undefined,
|
|
434
507
|
deployId: deploy.id,
|
|
435
508
|
expiresAt: deploy.expiresAt,
|
|
436
509
|
inspect: inspectUrls(deploy.url),
|
|
@@ -494,21 +567,6 @@ function isClientShellRequest(req, pathname) {
|
|
|
494
567
|
return req.method === "GET" && wantsHtml(req) && !isReservedClientShellPath(pathname);
|
|
495
568
|
}
|
|
496
569
|
|
|
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
570
|
function parseHostDeploy({ appBaseDomain, host, url }) {
|
|
513
571
|
if (!appBaseDomain) {
|
|
514
572
|
return null;
|
|
@@ -532,6 +590,10 @@ function parseHostDeploy({ appBaseDomain, host, url }) {
|
|
|
532
590
|
};
|
|
533
591
|
}
|
|
534
592
|
|
|
593
|
+
function isDisabledPathDeployRoute(pathname) {
|
|
594
|
+
return pathname === "/d" || pathname.startsWith("/d/");
|
|
595
|
+
}
|
|
596
|
+
|
|
535
597
|
function quotaLimitForBucket(bucket, deploy) {
|
|
536
598
|
if (bucket === "mutations") {
|
|
537
599
|
return deploy.limits.mutationsPerDay;
|
|
@@ -5716,7 +5778,7 @@ async function loadDeployByRoute({ appBaseDomain, host, store, url }) {
|
|
|
5716
5778
|
hostname: domain.hostname,
|
|
5717
5779
|
deployId: domain.deployId
|
|
5718
5780
|
}
|
|
5719
|
-
: parseHostDeploy({ appBaseDomain, host, url })
|
|
5781
|
+
: parseHostDeploy({ appBaseDomain, host, url });
|
|
5720
5782
|
if (!route) {
|
|
5721
5783
|
return null;
|
|
5722
5784
|
}
|
|
@@ -5934,17 +5996,23 @@ async function serveInspect({ adminPassword, artifact, currentDeveloper, deploy,
|
|
|
5934
5996
|
export async function startAnonymousServer({
|
|
5935
5997
|
adminPassword = adminPasswordFromEnv(),
|
|
5936
5998
|
appBaseDomain = process.env.LAKEBED_APP_BASE_DOMAIN ?? "",
|
|
5999
|
+
dashboardRootUrl = process.env.LAKEBED_DASHBOARD_ROOT_URL,
|
|
5937
6000
|
developerSessionSecret = process.env.LAKEBED_SESSION_SECRET ?? "",
|
|
5938
6001
|
githubOAuth = githubOAuthFromEnv(),
|
|
5939
6002
|
port = Number(process.env.PORT ?? 8787),
|
|
5940
6003
|
publicRootUrl,
|
|
5941
6004
|
quiet = false,
|
|
6005
|
+
role = process.env.LAKEBED_SERVER_ROLE ?? "all",
|
|
5942
6006
|
shooBaseUrl = shooBaseUrlFromEnv(),
|
|
5943
6007
|
sourceRuntime,
|
|
5944
6008
|
store
|
|
5945
6009
|
} = {}) {
|
|
6010
|
+
const resolvedRole = normalizeServerRole(role);
|
|
6011
|
+
const servesApiDashboard = roleServesApiDashboard(resolvedRole);
|
|
6012
|
+
const servesRunner = roleServesRunner(resolvedRole);
|
|
5946
6013
|
const resolvedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
|
|
5947
6014
|
const resolvedPublicRootUrl = normalizePublicRootUrl(publicRootUrl ?? process.env.PUBLIC_ROOT_URL, port);
|
|
6015
|
+
const resolvedDashboardRootUrl = normalizePublicRootUrl(dashboardRootUrl ?? resolvedPublicRootUrl, port);
|
|
5948
6016
|
const deployCreationPolicy = anonymousDeployCreationPolicy({ publicRootUrl: resolvedPublicRootUrl });
|
|
5949
6017
|
const clientTrafficPolicy = anonymousClientTrafficPolicy({ publicRootUrl: resolvedPublicRootUrl });
|
|
5950
6018
|
const cleanupPolicy = cleanupPolicyFromEnv();
|
|
@@ -5952,7 +6020,7 @@ export async function startAnonymousServer({
|
|
|
5952
6020
|
const resolvedDeveloperSessionSecret =
|
|
5953
6021
|
developerSessionSecret || resolvedGithubOAuth?.sessionSecret || resolvedGithubOAuth?.clientSecret || adminPassword || "";
|
|
5954
6022
|
const resolvedStore = store ?? (await createAnonymousStoreFromEnv());
|
|
5955
|
-
const resolvedSourceRuntime = sourceRuntime === undefined ? createSourceRuntimeFromEnv() : sourceRuntime;
|
|
6023
|
+
const resolvedSourceRuntime = sourceRuntime === undefined && servesRunner ? createSourceRuntimeFromEnv() : sourceRuntime;
|
|
5956
6024
|
await resolvedStore.initialize();
|
|
5957
6025
|
const subscriptions = new Map();
|
|
5958
6026
|
let cleanupInterval = null;
|
|
@@ -6191,7 +6259,20 @@ export async function startAnonymousServer({
|
|
|
6191
6259
|
return;
|
|
6192
6260
|
}
|
|
6193
6261
|
|
|
6194
|
-
if (
|
|
6262
|
+
if (
|
|
6263
|
+
servesApiDashboard &&
|
|
6264
|
+
shouldRedirectToDashboardHost({
|
|
6265
|
+
dashboardRootUrl: resolvedDashboardRootUrl,
|
|
6266
|
+
host,
|
|
6267
|
+
publicRootUrl: resolvedPublicRootUrl,
|
|
6268
|
+
requestUrl
|
|
6269
|
+
})
|
|
6270
|
+
) {
|
|
6271
|
+
redirect(res, dashboardRedirectLocation({ dashboardRootUrl: resolvedDashboardRootUrl, requestUrl }));
|
|
6272
|
+
return;
|
|
6273
|
+
}
|
|
6274
|
+
|
|
6275
|
+
if (servesApiDashboard && req.method === "GET" && (requestUrl.pathname === "/deploys" || requestUrl.pathname === "/dashboard")) {
|
|
6195
6276
|
const authConfigured = developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret);
|
|
6196
6277
|
const user = authConfigured ? currentDeveloper(req) : null;
|
|
6197
6278
|
sendText(
|
|
@@ -6207,7 +6288,7 @@ export async function startAnonymousServer({
|
|
|
6207
6288
|
return;
|
|
6208
6289
|
}
|
|
6209
6290
|
|
|
6210
|
-
if (req.method === "GET" && requestUrl.pathname === "/v1/me") {
|
|
6291
|
+
if (servesApiDashboard && req.method === "GET" && requestUrl.pathname === "/v1/me") {
|
|
6211
6292
|
const user = currentDeveloper(req);
|
|
6212
6293
|
if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret) || !user) {
|
|
6213
6294
|
sendJson(res, 401, { error: "Developer authentication required." });
|
|
@@ -6218,7 +6299,7 @@ export async function startAnonymousServer({
|
|
|
6218
6299
|
return;
|
|
6219
6300
|
}
|
|
6220
6301
|
|
|
6221
|
-
if (req.method === "GET" && requestUrl.pathname === "/v1/me/deploys") {
|
|
6302
|
+
if (servesApiDashboard && req.method === "GET" && requestUrl.pathname === "/v1/me/deploys") {
|
|
6222
6303
|
const user = currentDeveloper(req);
|
|
6223
6304
|
if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret) || !user) {
|
|
6224
6305
|
sendJson(res, 401, { error: "Developer authentication required." });
|
|
@@ -6230,7 +6311,7 @@ export async function startAnonymousServer({
|
|
|
6230
6311
|
}
|
|
6231
6312
|
|
|
6232
6313
|
const developerTerminateMatch =
|
|
6233
|
-
req.method === "POST" ? requestUrl.pathname.match(/^\/v1\/me\/deploys\/([^/]+)\/terminate$/) : null;
|
|
6314
|
+
servesApiDashboard && req.method === "POST" ? requestUrl.pathname.match(/^\/v1\/me\/deploys\/([^/]+)\/terminate$/) : null;
|
|
6234
6315
|
if (developerTerminateMatch) {
|
|
6235
6316
|
const user = currentDeveloper(req);
|
|
6236
6317
|
if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret) || !user) {
|
|
@@ -6266,7 +6347,7 @@ export async function startAnonymousServer({
|
|
|
6266
6347
|
return;
|
|
6267
6348
|
}
|
|
6268
6349
|
|
|
6269
|
-
if (req.method === "GET" && requestUrl.pathname === "/auth/github") {
|
|
6350
|
+
if (servesApiDashboard && req.method === "GET" && requestUrl.pathname === "/auth/github") {
|
|
6270
6351
|
if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret)) {
|
|
6271
6352
|
sendText(res, 503, "GitHub sign-in is not configured.\n", { "Content-Type": "text/plain; charset=utf-8" });
|
|
6272
6353
|
return;
|
|
@@ -6284,7 +6365,7 @@ export async function startAnonymousServer({
|
|
|
6284
6365
|
);
|
|
6285
6366
|
const authorizeUrl = new URL(resolvedGithubOAuth.authorizeUrl);
|
|
6286
6367
|
authorizeUrl.searchParams.set("client_id", resolvedGithubOAuth.clientId);
|
|
6287
|
-
authorizeUrl.searchParams.set("redirect_uri", githubRedirectUri(resolvedGithubOAuth,
|
|
6368
|
+
authorizeUrl.searchParams.set("redirect_uri", githubRedirectUri(resolvedGithubOAuth, resolvedDashboardRootUrl));
|
|
6288
6369
|
authorizeUrl.searchParams.set("scope", "read:user");
|
|
6289
6370
|
authorizeUrl.searchParams.set("state", state);
|
|
6290
6371
|
redirect(res, authorizeUrl.href, {
|
|
@@ -6293,7 +6374,7 @@ export async function startAnonymousServer({
|
|
|
6293
6374
|
return;
|
|
6294
6375
|
}
|
|
6295
6376
|
|
|
6296
|
-
if (req.method === "GET" && requestUrl.pathname === "/auth/github/callback") {
|
|
6377
|
+
if (servesApiDashboard && req.method === "GET" && requestUrl.pathname === "/auth/github/callback") {
|
|
6297
6378
|
if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret)) {
|
|
6298
6379
|
sendText(res, 503, "GitHub sign-in is not configured.\n", { "Content-Type": "text/plain; charset=utf-8" });
|
|
6299
6380
|
return;
|
|
@@ -6339,7 +6420,7 @@ export async function startAnonymousServer({
|
|
|
6339
6420
|
const user = await githubUserFromCode({
|
|
6340
6421
|
code,
|
|
6341
6422
|
githubOAuth: resolvedGithubOAuth,
|
|
6342
|
-
redirectUri: githubRedirectUri(resolvedGithubOAuth,
|
|
6423
|
+
redirectUri: githubRedirectUri(resolvedGithubOAuth, resolvedDashboardRootUrl)
|
|
6343
6424
|
});
|
|
6344
6425
|
redirect(res, normalizeReturnTo(statePayload.returnTo), {
|
|
6345
6426
|
"Set-Cookie": [developerCookie(user, resolvedDeveloperSessionSecret, secure), clearOauthStateCookie(secure)]
|
|
@@ -6347,14 +6428,14 @@ export async function startAnonymousServer({
|
|
|
6347
6428
|
return;
|
|
6348
6429
|
}
|
|
6349
6430
|
|
|
6350
|
-
if (req.method === "GET" && requestUrl.pathname === "/auth/logout") {
|
|
6431
|
+
if (servesApiDashboard && req.method === "GET" && requestUrl.pathname === "/auth/logout") {
|
|
6351
6432
|
redirect(res, "/deploys", {
|
|
6352
6433
|
"Set-Cookie": clearDeveloperCookie(isSecureRequest(req))
|
|
6353
6434
|
});
|
|
6354
6435
|
return;
|
|
6355
6436
|
}
|
|
6356
6437
|
|
|
6357
|
-
const claimMatch = req.method === "GET" ? requestUrl.pathname.match(/^\/claim\/([^/]+)\/([^/]+)$/) : null;
|
|
6438
|
+
const claimMatch = servesApiDashboard && req.method === "GET" ? requestUrl.pathname.match(/^\/claim\/([^/]+)\/([^/]+)$/) : null;
|
|
6358
6439
|
if (claimMatch) {
|
|
6359
6440
|
if (!developerAuthConfigured(resolvedGithubOAuth, resolvedDeveloperSessionSecret)) {
|
|
6360
6441
|
sendText(res, 503, "GitHub sign-in is not configured.\n", { "Content-Type": "text/plain; charset=utf-8" });
|
|
@@ -6392,6 +6473,7 @@ export async function startAnonymousServer({
|
|
|
6392
6473
|
}
|
|
6393
6474
|
|
|
6394
6475
|
if (
|
|
6476
|
+
servesApiDashboard &&
|
|
6395
6477
|
req.method === "GET" &&
|
|
6396
6478
|
(requestUrl.pathname === "/admin" ||
|
|
6397
6479
|
requestUrl.pathname === "/admin/" ||
|
|
@@ -6403,7 +6485,7 @@ export async function startAnonymousServer({
|
|
|
6403
6485
|
return;
|
|
6404
6486
|
}
|
|
6405
6487
|
|
|
6406
|
-
if (requestUrl.pathname === "/admin/api/login" && req.method === "POST") {
|
|
6488
|
+
if (servesApiDashboard && requestUrl.pathname === "/admin/api/login" && req.method === "POST") {
|
|
6407
6489
|
if (!isAdminConfigured(adminPassword)) {
|
|
6408
6490
|
sendJson(res, 503, { error: "Lakebed admin is not configured. Set LAKEBED_ADMIN_PASSWORD." });
|
|
6409
6491
|
return;
|
|
@@ -6419,12 +6501,12 @@ export async function startAnonymousServer({
|
|
|
6419
6501
|
return;
|
|
6420
6502
|
}
|
|
6421
6503
|
|
|
6422
|
-
if (requestUrl.pathname === "/admin/api/logout" && req.method === "POST") {
|
|
6504
|
+
if (servesApiDashboard && requestUrl.pathname === "/admin/api/logout" && req.method === "POST") {
|
|
6423
6505
|
sendJson(res, 200, { ok: true }, { "Set-Cookie": adminCookie("", 0, isSecureRequest(req)) });
|
|
6424
6506
|
return;
|
|
6425
6507
|
}
|
|
6426
6508
|
|
|
6427
|
-
if (requestUrl.pathname === "/admin/api/summary" && req.method === "GET") {
|
|
6509
|
+
if (servesApiDashboard && requestUrl.pathname === "/admin/api/summary" && req.method === "GET") {
|
|
6428
6510
|
if (!isAdminConfigured(adminPassword)) {
|
|
6429
6511
|
sendJson(res, 503, { error: "Lakebed admin is not configured. Set LAKEBED_ADMIN_PASSWORD." });
|
|
6430
6512
|
return;
|
|
@@ -6440,7 +6522,7 @@ export async function startAnonymousServer({
|
|
|
6440
6522
|
}
|
|
6441
6523
|
|
|
6442
6524
|
const adminUserDetailMatch =
|
|
6443
|
-
req.method === "GET" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)$/) : null;
|
|
6525
|
+
servesApiDashboard && req.method === "GET" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)$/) : null;
|
|
6444
6526
|
if (adminUserDetailMatch) {
|
|
6445
6527
|
if (!isAdminConfigured(adminPassword)) {
|
|
6446
6528
|
sendJson(res, 503, { error: "Lakebed admin is not configured. Set LAKEBED_ADMIN_PASSWORD." });
|
|
@@ -6463,7 +6545,7 @@ export async function startAnonymousServer({
|
|
|
6463
6545
|
}
|
|
6464
6546
|
|
|
6465
6547
|
const userLimitsMatch =
|
|
6466
|
-
req.method === "PUT" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)\/limits$/) : null;
|
|
6548
|
+
servesApiDashboard && req.method === "PUT" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)\/limits$/) : null;
|
|
6467
6549
|
if (userLimitsMatch) {
|
|
6468
6550
|
if (!isAdminConfigured(adminPassword)) {
|
|
6469
6551
|
sendJson(res, 503, { error: "Lakebed admin is not configured. Set LAKEBED_ADMIN_PASSWORD." });
|
|
@@ -6499,7 +6581,7 @@ export async function startAnonymousServer({
|
|
|
6499
6581
|
}
|
|
6500
6582
|
|
|
6501
6583
|
const terminateMatch =
|
|
6502
|
-
req.method === "POST"
|
|
6584
|
+
servesApiDashboard && req.method === "POST"
|
|
6503
6585
|
? requestUrl.pathname.match(/^\/admin\/api\/deploys\/([^/]+)\/terminate$/)
|
|
6504
6586
|
: null;
|
|
6505
6587
|
if (terminateMatch) {
|
|
@@ -6533,7 +6615,7 @@ export async function startAnonymousServer({
|
|
|
6533
6615
|
}
|
|
6534
6616
|
|
|
6535
6617
|
const deployDomainsMatch = requestUrl.pathname.match(/^\/v1\/deploys\/([^/]+)\/domains$/);
|
|
6536
|
-
if (deployDomainsMatch && (req.method === "GET" || req.method === "POST")) {
|
|
6618
|
+
if (servesApiDashboard && deployDomainsMatch && (req.method === "GET" || req.method === "POST")) {
|
|
6537
6619
|
const deployId = decodeURIComponent(deployDomainsMatch[1]);
|
|
6538
6620
|
const currentDeploy = await resolvedStore.getDeployById(deployId);
|
|
6539
6621
|
if (!currentDeploy) {
|
|
@@ -6616,7 +6698,12 @@ export async function startAnonymousServer({
|
|
|
6616
6698
|
return;
|
|
6617
6699
|
}
|
|
6618
6700
|
|
|
6619
|
-
if (req.method === "POST" && requestUrl.pathname === "/v1/anonymous-deploys") {
|
|
6701
|
+
if (servesApiDashboard && req.method === "POST" && requestUrl.pathname === "/v1/anonymous-deploys") {
|
|
6702
|
+
if (!resolvedAppBaseDomain) {
|
|
6703
|
+
sendJson(res, 503, { error: "LAKEBED_APP_BASE_DOMAIN is required to create hosted deploys." });
|
|
6704
|
+
return;
|
|
6705
|
+
}
|
|
6706
|
+
|
|
6620
6707
|
await enforceAnonymousDeployCreation(req);
|
|
6621
6708
|
const body = await readJsonBody(req);
|
|
6622
6709
|
const payload = validateAnonymousDeployPayload(body);
|
|
@@ -6642,11 +6729,16 @@ export async function startAnonymousServer({
|
|
|
6642
6729
|
serverEnv: payload.serverEnv
|
|
6643
6730
|
});
|
|
6644
6731
|
await resolvedStore.appendLog(deploy.id, "info", "anonymous deploy created", { artifactHash: deploy.artifactHash });
|
|
6645
|
-
sendJson(res, 201, responseForDeploy({ deploy, token }));
|
|
6732
|
+
sendJson(res, 201, responseForDeploy({ dashboardRootUrl: resolvedDashboardRootUrl, deploy, token }));
|
|
6646
6733
|
return;
|
|
6647
6734
|
}
|
|
6648
6735
|
|
|
6649
|
-
if ((req.method === "PUT" || req.method === "PATCH") && requestUrl.pathname.startsWith("/v1/deploys/")) {
|
|
6736
|
+
if (servesApiDashboard && (req.method === "PUT" || req.method === "PATCH") && requestUrl.pathname.startsWith("/v1/deploys/")) {
|
|
6737
|
+
if (!resolvedAppBaseDomain) {
|
|
6738
|
+
sendJson(res, 503, { error: "LAKEBED_APP_BASE_DOMAIN is required to update hosted deploys." });
|
|
6739
|
+
return;
|
|
6740
|
+
}
|
|
6741
|
+
|
|
6650
6742
|
const deployId = requestUrl.pathname.slice("/v1/deploys/".length);
|
|
6651
6743
|
const currentDeploy = await resolvedStore.getDeployById(deployId);
|
|
6652
6744
|
if (!currentDeploy) {
|
|
@@ -6692,23 +6784,43 @@ export async function startAnonymousServer({
|
|
|
6692
6784
|
await refreshDeploySubscriptions(deploy);
|
|
6693
6785
|
refreshDeployClients(deploy);
|
|
6694
6786
|
await publishDeploy(deploy.id);
|
|
6695
|
-
sendJson(res, 200, responseForDeploy({ deploy }));
|
|
6787
|
+
sendJson(res, 200, responseForDeploy({ dashboardRootUrl: resolvedDashboardRootUrl, deploy }));
|
|
6696
6788
|
return;
|
|
6697
6789
|
}
|
|
6698
6790
|
|
|
6699
|
-
if (req.method === "GET" && requestUrl.pathname.startsWith("/v1/deploys/")) {
|
|
6791
|
+
if (servesApiDashboard && req.method === "GET" && requestUrl.pathname.startsWith("/v1/deploys/")) {
|
|
6700
6792
|
const id = requestUrl.pathname.slice("/v1/deploys/".length);
|
|
6701
6793
|
const deploy = (await resolvedStore.getDeployById(id)) ?? (await resolvedStore.getDeployBySlug(id));
|
|
6702
6794
|
if (!deploy) {
|
|
6703
6795
|
sendJson(res, 404, { error: "Unknown deploy." });
|
|
6704
6796
|
return;
|
|
6705
6797
|
}
|
|
6706
|
-
sendJson(res, 200, responseForDeploy({ deploy }));
|
|
6798
|
+
sendJson(res, 200, responseForDeploy({ dashboardRootUrl: resolvedDashboardRootUrl, deploy }));
|
|
6799
|
+
return;
|
|
6800
|
+
}
|
|
6801
|
+
|
|
6802
|
+
if (isDisabledPathDeployRoute(requestUrl.pathname)) {
|
|
6803
|
+
sendText(res, 404, "Path-based deploy URLs are no longer supported.\n", { "Content-Type": "text/plain; charset=utf-8" });
|
|
6804
|
+
return;
|
|
6805
|
+
}
|
|
6806
|
+
|
|
6807
|
+
if (!servesRunner) {
|
|
6808
|
+
if (req.method === "GET" && requestUrl.pathname === "/") {
|
|
6809
|
+
redirect(res, "/deploys");
|
|
6810
|
+
return;
|
|
6811
|
+
}
|
|
6812
|
+
|
|
6813
|
+
sendText(res, 404, "Not found\n", { "Content-Type": "text/plain; charset=utf-8" });
|
|
6707
6814
|
return;
|
|
6708
6815
|
}
|
|
6709
6816
|
|
|
6710
6817
|
const loaded = await loadDeployByRoute({ appBaseDomain: resolvedAppBaseDomain, host, store: resolvedStore, url: requestUrl });
|
|
6711
6818
|
if (!loaded) {
|
|
6819
|
+
if (!servesApiDashboard && isApiDashboardRoute(requestUrl.pathname)) {
|
|
6820
|
+
sendText(res, 404, "Not found\n", { "Content-Type": "text/plain; charset=utf-8" });
|
|
6821
|
+
return;
|
|
6822
|
+
}
|
|
6823
|
+
|
|
6712
6824
|
sendText(res, 200, "Lakebed anonymous deploy runner\n", { "Content-Type": "text/plain; charset=utf-8" });
|
|
6713
6825
|
return;
|
|
6714
6826
|
}
|
|
@@ -6919,6 +7031,11 @@ export async function startAnonymousServer({
|
|
|
6919
7031
|
});
|
|
6920
7032
|
|
|
6921
7033
|
server.on("upgrade", async (req, socket, head) => {
|
|
7034
|
+
if (!servesRunner) {
|
|
7035
|
+
socket.destroy();
|
|
7036
|
+
return;
|
|
7037
|
+
}
|
|
7038
|
+
|
|
6922
7039
|
const host = req.headers.host ?? "localhost";
|
|
6923
7040
|
const requestUrl = new URL(req.url ?? "/", `http://${host}`);
|
|
6924
7041
|
const loaded = await loadDeployByRoute({ appBaseDomain: resolvedAppBaseDomain, host, store: resolvedStore, url: requestUrl });
|
|
@@ -6971,8 +7088,10 @@ export async function startAnonymousServer({
|
|
|
6971
7088
|
|
|
6972
7089
|
return {
|
|
6973
7090
|
appBaseDomain: resolvedAppBaseDomain,
|
|
7091
|
+
dashboardRootUrl: resolvedDashboardRootUrl,
|
|
6974
7092
|
port,
|
|
6975
7093
|
publicRootUrl: resolvedPublicRootUrl,
|
|
7094
|
+
role: resolvedRole,
|
|
6976
7095
|
store: resolvedStore,
|
|
6977
7096
|
url: resolvedPublicRootUrl,
|
|
6978
7097
|
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,8 @@ 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";
|
|
34
|
+
const legacyDeployApiUrl = "https://api.lakebed.app";
|
|
33
35
|
const execFileAsync = promisify(execFile);
|
|
34
36
|
const endpointBodyMaxBytes = 2 * 1024 * 1024;
|
|
35
37
|
|
|
@@ -44,7 +46,7 @@ Usage:
|
|
|
44
46
|
npx lakebed deploy [capsule-dir] [--api <url>] [--public-inspect] [--json]
|
|
45
47
|
npx lakebed claim [capsule-dir] [--api <url>] [--json]
|
|
46
48
|
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>]
|
|
49
|
+
npx lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--dashboard-root-url <url>] [--app-base-domain <domain>] [--role all|api-dashboard|runner]
|
|
48
50
|
npx lakebed inspect <deploy-id-or-url> [--api <url>] [--inspect-token <token>] [--json]
|
|
49
51
|
npx lakebed run-many [capsule-dir] [--count 20] [--base-port 4000]
|
|
50
52
|
npx lakebed auth as <name>
|
|
@@ -69,10 +71,12 @@ const optionsWithValues = new Set([
|
|
|
69
71
|
"--app-base-domain",
|
|
70
72
|
"--base-port",
|
|
71
73
|
"--count",
|
|
74
|
+
"--dashboard-root-url",
|
|
72
75
|
"--inspect-token",
|
|
73
76
|
"--out",
|
|
74
77
|
"--port",
|
|
75
78
|
"--public-root-url",
|
|
79
|
+
"--role",
|
|
76
80
|
"--target",
|
|
77
81
|
"--template"
|
|
78
82
|
]);
|
|
@@ -1190,12 +1194,12 @@ function claimTokenFromDeployResponse(deployed) {
|
|
|
1190
1194
|
return null;
|
|
1191
1195
|
}
|
|
1192
1196
|
|
|
1193
|
-
function claimUrlFromDeployMetadata(metadata) {
|
|
1197
|
+
function claimUrlFromDeployMetadata(metadata, api = metadata?.api) {
|
|
1194
1198
|
if (!metadata?.api || !metadata?.deployId || !metadata?.claimToken) {
|
|
1195
1199
|
return null;
|
|
1196
1200
|
}
|
|
1197
1201
|
|
|
1198
|
-
return `${
|
|
1202
|
+
return `${normalizeHostedUrl(api)}/claim/${encodeURIComponent(metadata.deployId)}/${encodeURIComponent(metadata.claimToken)}`;
|
|
1199
1203
|
}
|
|
1200
1204
|
|
|
1201
1205
|
function claimCommandText({ api, capsuleArg }) {
|
|
@@ -1327,6 +1331,15 @@ function normalizeHostedUrl(value) {
|
|
|
1327
1331
|
}
|
|
1328
1332
|
}
|
|
1329
1333
|
|
|
1334
|
+
function canonicalDeployApiUrl(value) {
|
|
1335
|
+
const normalized = normalizeHostedUrl(value);
|
|
1336
|
+
return normalized === legacyDeployApiUrl ? defaultDeployApiUrl : normalized;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function deployApiUrlsMatch(left, right) {
|
|
1340
|
+
return canonicalDeployApiUrl(left) === canonicalDeployApiUrl(right);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1330
1343
|
async function readResponseJson(response) {
|
|
1331
1344
|
const body = await response.text();
|
|
1332
1345
|
if (!response.ok) {
|
|
@@ -1351,7 +1364,7 @@ async function deployCommand(args) {
|
|
|
1351
1364
|
const inspectPolicy = hasFlag(args, "--public-inspect") ? "public" : undefined;
|
|
1352
1365
|
const metadata = await readDeployMetadata(capsuleDir);
|
|
1353
1366
|
const canUpdate =
|
|
1354
|
-
metadata?.api
|
|
1367
|
+
deployApiUrlsMatch(metadata?.api, api) && typeof metadata?.deployId === "string" && typeof metadata?.claimToken === "string";
|
|
1355
1368
|
let currentDeploy = null;
|
|
1356
1369
|
if (canUpdate) {
|
|
1357
1370
|
const currentResponse = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`);
|
|
@@ -1495,11 +1508,11 @@ async function claimCommand(args) {
|
|
|
1495
1508
|
throw new Error(`No Lakebed deploy metadata found at ${deployMetadataPath(capsuleDir)}. Run npx lakebed deploy from this project first.`);
|
|
1496
1509
|
}
|
|
1497
1510
|
|
|
1498
|
-
if (metadata.api
|
|
1511
|
+
if (!deployApiUrlsMatch(metadata.api, api)) {
|
|
1499
1512
|
throw new Error(`Saved deploy metadata is for ${metadata.api}, but this command is using ${api}. Pass --api ${metadata.api} to claim it.`);
|
|
1500
1513
|
}
|
|
1501
1514
|
|
|
1502
|
-
const claimUrl = claimUrlFromDeployMetadata(metadata);
|
|
1515
|
+
const claimUrl = claimUrlFromDeployMetadata(metadata, api);
|
|
1503
1516
|
if (!claimUrl) {
|
|
1504
1517
|
throw new Error("This project does not have a saved claim token. Redeploy to create a new claim URL.");
|
|
1505
1518
|
}
|
|
@@ -1558,7 +1571,7 @@ async function domainsCommand(args) {
|
|
|
1558
1571
|
if (!metadata) {
|
|
1559
1572
|
throw new Error(`No Lakebed deploy metadata found at ${deployMetadataPath(capsuleDir)}. Run npx lakebed deploy from this project first.`);
|
|
1560
1573
|
}
|
|
1561
|
-
if (metadata.api
|
|
1574
|
+
if (!deployApiUrlsMatch(metadata.api, api)) {
|
|
1562
1575
|
throw new Error(`Saved deploy metadata is for ${metadata.api}, but this command is using ${api}. Pass --api ${metadata.api} to use it.`);
|
|
1563
1576
|
}
|
|
1564
1577
|
if (!metadata.deployId || !metadata.claimToken) {
|
|
@@ -1590,15 +1603,17 @@ async function anonymousServerCommand(args) {
|
|
|
1590
1603
|
const port = readNumberArg(args, "--port", Number(process.env.PORT ?? 8787));
|
|
1591
1604
|
await startAnonymousServer({
|
|
1592
1605
|
appBaseDomain: readArg(args, "--app-base-domain", process.env.LAKEBED_APP_BASE_DOMAIN ?? ""),
|
|
1606
|
+
dashboardRootUrl: readArg(args, "--dashboard-root-url", process.env.LAKEBED_DASHBOARD_ROOT_URL),
|
|
1593
1607
|
port,
|
|
1594
|
-
publicRootUrl: readArg(args, "--public-root-url", process.env.PUBLIC_ROOT_URL ?? `http://localhost:${port}`)
|
|
1608
|
+
publicRootUrl: readArg(args, "--public-root-url", process.env.PUBLIC_ROOT_URL ?? `http://localhost:${port}`),
|
|
1609
|
+
role: readArg(args, "--role", process.env.LAKEBED_SERVER_ROLE ?? "all")
|
|
1595
1610
|
});
|
|
1596
1611
|
await new Promise(() => {});
|
|
1597
1612
|
}
|
|
1598
1613
|
|
|
1599
1614
|
function deployLookupApiUrl(target, args, metadata) {
|
|
1600
1615
|
if (!hasExplicitOption(args, "--api") && metadata?.api && metadata.deployId === target) {
|
|
1601
|
-
return
|
|
1616
|
+
return canonicalDeployApiUrl(metadata.api);
|
|
1602
1617
|
}
|
|
1603
1618
|
|
|
1604
1619
|
return deployApiUrl(args);
|
|
@@ -1620,6 +1635,61 @@ async function resolveDeployUrl(target, args, metadata) {
|
|
|
1620
1635
|
}
|
|
1621
1636
|
}
|
|
1622
1637
|
|
|
1638
|
+
async function resolveHostedTarget(target, args, metadata) {
|
|
1639
|
+
if (!target) {
|
|
1640
|
+
throw new Error("Expected a deploy ID or URL.");
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
try {
|
|
1644
|
+
const url = new URL(target);
|
|
1645
|
+
return { api: "", url: url.href.replace(/\/+$/g, "") };
|
|
1646
|
+
} catch {
|
|
1647
|
+
const api = deployLookupApiUrl(target, args, metadata);
|
|
1648
|
+
const response = await fetch(`${api}/v1/deploys/${encodeURIComponent(target)}`);
|
|
1649
|
+
const deploy = await readResponseJson(response);
|
|
1650
|
+
return { api, url: deploy.url.replace(/\/+$/g, "") };
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function isLocalApiUrl(value) {
|
|
1655
|
+
try {
|
|
1656
|
+
const { hostname } = new URL(value);
|
|
1657
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
1658
|
+
} catch {
|
|
1659
|
+
return false;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async function requestWithHostHeader(url, headers) {
|
|
1664
|
+
const requestUrl = new URL(url);
|
|
1665
|
+
const transport = requestUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
|
1666
|
+
return new Promise((resolveRequest, rejectRequest) => {
|
|
1667
|
+
const req = transport(
|
|
1668
|
+
{
|
|
1669
|
+
headers,
|
|
1670
|
+
hostname: requestUrl.hostname,
|
|
1671
|
+
method: "GET",
|
|
1672
|
+
path: `${requestUrl.pathname}${requestUrl.search}`,
|
|
1673
|
+
port: requestUrl.port
|
|
1674
|
+
},
|
|
1675
|
+
(res) => {
|
|
1676
|
+
const chunks = [];
|
|
1677
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
1678
|
+
res.on("end", () => {
|
|
1679
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
1680
|
+
resolveRequest({
|
|
1681
|
+
ok: (res.statusCode ?? 500) >= 200 && (res.statusCode ?? 500) < 300,
|
|
1682
|
+
status: res.statusCode ?? 500,
|
|
1683
|
+
text: async () => body
|
|
1684
|
+
});
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
);
|
|
1688
|
+
req.on("error", rejectRequest);
|
|
1689
|
+
req.end();
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1623
1693
|
function inspectTokenForHostedTarget({ args, metadata, target, url }) {
|
|
1624
1694
|
const explicitToken = readArg(args, "--inspect-token", process.env.LAKEBED_INSPECT_TOKEN ?? "");
|
|
1625
1695
|
if (explicitToken) {
|
|
@@ -1642,11 +1712,22 @@ function inspectTokenForHostedTarget({ args, metadata, target, url }) {
|
|
|
1642
1712
|
|
|
1643
1713
|
async function hostedJson(target, path, args) {
|
|
1644
1714
|
const metadata = await readDeployMetadata(root);
|
|
1645
|
-
const url = await
|
|
1715
|
+
const { api, url } = await resolveHostedTarget(target, args, metadata);
|
|
1646
1716
|
const inspectToken = inspectTokenForHostedTarget({ args, metadata, target, url });
|
|
1647
|
-
const
|
|
1648
|
-
|
|
1649
|
-
|
|
1717
|
+
const headers = inspectToken ? { Authorization: `Bearer ${inspectToken}` } : {};
|
|
1718
|
+
let response;
|
|
1719
|
+
try {
|
|
1720
|
+
response = await fetch(`${url}${path}`, { headers });
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
if (!api || !isLocalApiUrl(api)) {
|
|
1723
|
+
throw error;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
response = await requestWithHostHeader(`${api}${path}`, {
|
|
1727
|
+
...headers,
|
|
1728
|
+
Host: new URL(url).host
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1650
1731
|
return readResponseJson(response);
|
|
1651
1732
|
}
|
|
1652
1733
|
|
|
@@ -1761,7 +1842,7 @@ async function dbCommand(args) {
|
|
|
1761
1842
|
}
|
|
1762
1843
|
|
|
1763
1844
|
function agentInstructionsTemplate() {
|
|
1764
|
-
return `#
|
|
1845
|
+
return `# Lakebed App Instructions
|
|
1765
1846
|
|
|
1766
1847
|
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
1848
|
|
|
@@ -1814,6 +1895,10 @@ npx lakebed db dump --port 3000
|
|
|
1814
1895
|
npx lakebed logs --port 3000
|
|
1815
1896
|
\`\`\`
|
|
1816
1897
|
|
|
1898
|
+
## External endpoints
|
|
1899
|
+
|
|
1900
|
+
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.
|
|
1901
|
+
|
|
1817
1902
|
## Additional resources
|
|
1818
1903
|
|
|
1819
1904
|
- [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.22";
|