lakebed 0.0.14 → 0.0.15
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 +3 -3
- package/package.json +1 -1
- package/src/anonymous-server.js +11 -14
- package/src/cli.js +53 -17
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ lakebed new [name] [--template todo] [--no-git]
|
|
|
122
122
|
lakebed create [name] [--template todo] [--no-git]
|
|
123
123
|
lakebed dev [capsule-dir] [--port 3000]
|
|
124
124
|
lakebed build [capsule-dir] --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
125
|
-
lakebed deploy [capsule-dir] [--
|
|
125
|
+
lakebed deploy [capsule-dir] [--api <url>] [--json]
|
|
126
126
|
lakebed claim [capsule-dir] [--api <url>] [--json]
|
|
127
127
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
|
|
128
128
|
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
@@ -167,7 +167,7 @@ LAKEBED_APP_BASE_DOMAIN=lakebed.app
|
|
|
167
167
|
|
|
168
168
|
With a verified `*.lakebed.app` custom domain on the runner, deploy responses use `https://<slug>.lakebed.app`.
|
|
169
169
|
|
|
170
|
-
Deploy responses include
|
|
170
|
+
Deploy responses include claim metadata. Configure GitHub OAuth on the runner, then run `lakebed claim` to open the claim page and attach the anonymous deploy to a developer account:
|
|
171
171
|
|
|
172
172
|
```sh
|
|
173
173
|
LAKEBED_GITHUB_CLIENT_ID=...
|
|
@@ -176,7 +176,7 @@ LAKEBED_SESSION_SECRET=...
|
|
|
176
176
|
LAKEBED_SERVER_ENV_SECRET=...
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
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, `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, `lakebed deploy` creates a claim-required preview, saves its claim metadata, and prints the claim
|
|
179
|
+
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, `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, `lakebed deploy` creates a claim-required preview, saves its claim metadata, and prints the `lakebed claim` command. Run that command, then run `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.
|
|
180
180
|
|
|
181
181
|
## Admin Dashboard
|
|
182
182
|
|
package/package.json
CHANGED
package/src/anonymous-server.js
CHANGED
|
@@ -19,6 +19,11 @@ function now() {
|
|
|
19
19
|
return new Date().toISOString();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function anonymousDeployExpiresAt() {
|
|
23
|
+
const ttlSeconds = parseTtlSeconds();
|
|
24
|
+
return new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
function dayWindowStart() {
|
|
23
28
|
return `${new Date().toISOString().slice(0, 10)}T00:00:00.000Z`;
|
|
24
29
|
}
|
|
@@ -2609,7 +2614,7 @@ export class MemoryAnonymousStore {
|
|
|
2609
2614
|
});
|
|
2610
2615
|
}
|
|
2611
2616
|
|
|
2612
|
-
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl,
|
|
2617
|
+
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, serverEnv }) {
|
|
2613
2618
|
const deployId = createDeployId();
|
|
2614
2619
|
const normalizedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
|
|
2615
2620
|
let slug = createSlug();
|
|
@@ -2619,8 +2624,7 @@ export class MemoryAnonymousStore {
|
|
|
2619
2624
|
|
|
2620
2625
|
const token = createClaimToken();
|
|
2621
2626
|
const createdAt = now();
|
|
2622
|
-
const
|
|
2623
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
2627
|
+
const expiresAt = anonymousDeployExpiresAt();
|
|
2624
2628
|
const url = appUrlForSlug({ appBaseDomain: normalizedAppBaseDomain, publicRootUrl, slug });
|
|
2625
2629
|
|
|
2626
2630
|
this.storeArtifact({
|
|
@@ -2665,7 +2669,6 @@ export class MemoryAnonymousStore {
|
|
|
2665
2669
|
clientBundleHash,
|
|
2666
2670
|
deployId,
|
|
2667
2671
|
publicRootUrl,
|
|
2668
|
-
requestedTtlSeconds,
|
|
2669
2672
|
serverEnv
|
|
2670
2673
|
}) {
|
|
2671
2674
|
const currentDeploy = await this.getStoredDeployById(deployId);
|
|
@@ -2674,7 +2677,6 @@ export class MemoryAnonymousStore {
|
|
|
2674
2677
|
}
|
|
2675
2678
|
|
|
2676
2679
|
const updatedAt = now();
|
|
2677
|
-
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
2678
2680
|
const nextAppBaseDomain = normalizeAppBaseDomain(appBaseDomain ?? currentDeploy.appBaseDomain);
|
|
2679
2681
|
const nextPublicRootUrl = publicRootUrl ?? currentDeploy.publicRootUrl;
|
|
2680
2682
|
const deploy = {
|
|
@@ -2682,7 +2684,7 @@ export class MemoryAnonymousStore {
|
|
|
2682
2684
|
appBaseDomain: nextAppBaseDomain,
|
|
2683
2685
|
artifactHash,
|
|
2684
2686
|
clientBundleHash,
|
|
2685
|
-
expiresAt: currentDeploy.ownerId ? null :
|
|
2687
|
+
expiresAt: currentDeploy.ownerId ? null : anonymousDeployExpiresAt(),
|
|
2686
2688
|
publicRootUrl: nextPublicRootUrl,
|
|
2687
2689
|
url: appUrlForSlug({ appBaseDomain: nextAppBaseDomain, publicRootUrl: nextPublicRootUrl, slug: currentDeploy.slug }),
|
|
2688
2690
|
status: "active",
|
|
@@ -3318,11 +3320,10 @@ export class PostgresAnonymousStore {
|
|
|
3318
3320
|
);
|
|
3319
3321
|
}
|
|
3320
3322
|
|
|
3321
|
-
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl,
|
|
3323
|
+
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, serverEnv }) {
|
|
3322
3324
|
const createdAt = now();
|
|
3323
3325
|
const normalizedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
|
|
3324
|
-
const
|
|
3325
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
3326
|
+
const expiresAt = anonymousDeployExpiresAt();
|
|
3326
3327
|
const token = createClaimToken();
|
|
3327
3328
|
const deployId = createDeployId();
|
|
3328
3329
|
|
|
@@ -3397,7 +3398,6 @@ export class PostgresAnonymousStore {
|
|
|
3397
3398
|
clientBundleHash,
|
|
3398
3399
|
deployId,
|
|
3399
3400
|
publicRootUrl,
|
|
3400
|
-
requestedTtlSeconds,
|
|
3401
3401
|
serverEnv
|
|
3402
3402
|
}) {
|
|
3403
3403
|
const currentDeploy = await this.getBaseDeployById(deployId);
|
|
@@ -3406,8 +3406,7 @@ export class PostgresAnonymousStore {
|
|
|
3406
3406
|
}
|
|
3407
3407
|
|
|
3408
3408
|
const updatedAt = now();
|
|
3409
|
-
const
|
|
3410
|
-
const expiresAt = currentDeploy.ownerId ? null : new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
3409
|
+
const expiresAt = currentDeploy.ownerId ? null : anonymousDeployExpiresAt();
|
|
3411
3410
|
const nextAppBaseDomain = normalizeAppBaseDomain(appBaseDomain ?? currentDeploy.appBaseDomain);
|
|
3412
3411
|
const nextPublicRootUrl = publicRootUrl ?? currentDeploy.publicRootUrl;
|
|
3413
3412
|
const url = appUrlForSlug({ appBaseDomain: nextAppBaseDomain, publicRootUrl: nextPublicRootUrl, slug: currentDeploy.slug });
|
|
@@ -4393,7 +4392,6 @@ export async function startAnonymousServer({
|
|
|
4393
4392
|
clientBundleBase64: payload.clientBundleBase64,
|
|
4394
4393
|
clientBundleHash: payload.clientBundleHash,
|
|
4395
4394
|
publicRootUrl: resolvedPublicRootUrl,
|
|
4396
|
-
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined,
|
|
4397
4395
|
serverEnv: payload.serverEnv
|
|
4398
4396
|
});
|
|
4399
4397
|
await resolvedStore.appendLog(deploy.id, "info", "anonymous deploy created", { artifactHash: deploy.artifactHash });
|
|
@@ -4428,7 +4426,6 @@ export async function startAnonymousServer({
|
|
|
4428
4426
|
clientBundleHash: payload.clientBundleHash,
|
|
4429
4427
|
deployId,
|
|
4430
4428
|
publicRootUrl: resolvedPublicRootUrl,
|
|
4431
|
-
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined,
|
|
4432
4429
|
serverEnv: payload.serverEnv
|
|
4433
4430
|
});
|
|
4434
4431
|
if (!deploy) {
|
package/src/cli.js
CHANGED
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
SERVER_ENV_FILE,
|
|
18
18
|
createAnonymousArtifact,
|
|
19
19
|
createClaimedArtifact,
|
|
20
|
-
parseTtlSeconds,
|
|
21
20
|
stableStringify,
|
|
22
21
|
validateServerEnvValues
|
|
23
22
|
} from "./anonymous.js";
|
|
@@ -41,7 +40,7 @@ Usage:
|
|
|
41
40
|
lakebed create [name] [--template todo] [--no-git]
|
|
42
41
|
lakebed dev [capsule-dir] [--port 3000]
|
|
43
42
|
lakebed build [capsule-dir] --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
44
|
-
lakebed deploy [capsule-dir] [--
|
|
43
|
+
lakebed deploy [capsule-dir] [--api <url>] [--json]
|
|
45
44
|
lakebed claim [capsule-dir] [--api <url>] [--json]
|
|
46
45
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
|
|
47
46
|
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
@@ -72,8 +71,7 @@ const optionsWithValues = new Set([
|
|
|
72
71
|
"--port",
|
|
73
72
|
"--public-root-url",
|
|
74
73
|
"--target",
|
|
75
|
-
"--template"
|
|
76
|
-
"--ttl"
|
|
74
|
+
"--template"
|
|
77
75
|
]);
|
|
78
76
|
|
|
79
77
|
function positionals(args) {
|
|
@@ -948,12 +946,39 @@ function claimUrlFromDeployMetadata(metadata) {
|
|
|
948
946
|
return `${String(metadata.api).replace(/\/+$/g, "")}/claim/${encodeURIComponent(metadata.deployId)}/${encodeURIComponent(metadata.claimToken)}`;
|
|
949
947
|
}
|
|
950
948
|
|
|
951
|
-
function
|
|
949
|
+
function claimCommandText({ api, capsuleArg }) {
|
|
950
|
+
const parts = ["lakebed", "claim"];
|
|
951
|
+
if (capsuleArg) {
|
|
952
|
+
parts.push(capsuleArg);
|
|
953
|
+
}
|
|
954
|
+
if (api !== defaultDeployApiUrl) {
|
|
955
|
+
parts.push("--api", api);
|
|
956
|
+
}
|
|
957
|
+
return parts.map(shellQuote).join(" ");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function browserOpenInvocation(url) {
|
|
961
|
+
if (process.platform === "darwin") {
|
|
962
|
+
return { command: "open", args: [url] };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (process.platform === "win32") {
|
|
966
|
+
return { command: "cmd", args: ["/c", "start", "", url] };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return { command: "xdg-open", args: [url] };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function openUrlInBrowser(url) {
|
|
973
|
+
const invocation = browserOpenInvocation(url);
|
|
974
|
+
await execFileAsync(invocation.command, invocation.args, { windowsHide: true });
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function deployRequestBody(envelope, { serverEnv } = {}) {
|
|
952
978
|
const body = {
|
|
953
979
|
artifact: envelope.artifact,
|
|
954
980
|
clientBundle: envelope.clientBundle,
|
|
955
|
-
clientVersion: LAKEBED_VERSION
|
|
956
|
-
requestedTtlSeconds: ttl
|
|
981
|
+
clientVersion: LAKEBED_VERSION
|
|
957
982
|
};
|
|
958
983
|
if (serverEnv !== undefined) {
|
|
959
984
|
body.serverEnv = {
|
|
@@ -1012,6 +1037,10 @@ async function readResponseJson(response) {
|
|
|
1012
1037
|
}
|
|
1013
1038
|
|
|
1014
1039
|
async function deployCommand(args) {
|
|
1040
|
+
if (args.some((arg) => arg === "--ttl" || arg.startsWith("--ttl="))) {
|
|
1041
|
+
throw new Error("lakebed deploy no longer accepts --ttl. Deploy expiry is set by the server.");
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1015
1044
|
const [capsuleArg] = positionals(args);
|
|
1016
1045
|
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
|
1017
1046
|
const sourceStore = await createMemorySourceStoreFromDirectory(capsuleDir);
|
|
@@ -1019,7 +1048,6 @@ async function deployCommand(args) {
|
|
|
1019
1048
|
const serverEnv = await readCapsuleServerEnv(sourceStore);
|
|
1020
1049
|
const serverEnvKeys = Object.keys(serverEnv).sort();
|
|
1021
1050
|
const hasServerEnvValues = serverEnvKeys.length > 0;
|
|
1022
|
-
const ttl = parseTtlSeconds(readArg(args, "--ttl", "7d"));
|
|
1023
1051
|
const api = deployApiUrl(args);
|
|
1024
1052
|
const metadata = await readDeployMetadata(capsuleDir);
|
|
1025
1053
|
const canUpdate =
|
|
@@ -1071,7 +1099,7 @@ async function deployCommand(args) {
|
|
|
1071
1099
|
|
|
1072
1100
|
if (canUpdate) {
|
|
1073
1101
|
response = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`, {
|
|
1074
|
-
body: deployRequestBody(envelope,
|
|
1102
|
+
body: deployRequestBody(envelope, { serverEnv: serverEnvForUpdate }),
|
|
1075
1103
|
headers: {
|
|
1076
1104
|
"Authorization": `Bearer ${metadata.claimToken}`,
|
|
1077
1105
|
"Content-Type": "application/json"
|
|
@@ -1092,7 +1120,7 @@ async function deployCommand(args) {
|
|
|
1092
1120
|
}
|
|
1093
1121
|
|
|
1094
1122
|
response ??= await fetch(`${api}/v1/anonymous-deploys`, {
|
|
1095
|
-
body: deployRequestBody(envelope
|
|
1123
|
+
body: deployRequestBody(envelope),
|
|
1096
1124
|
headers: {
|
|
1097
1125
|
"Content-Type": "application/json"
|
|
1098
1126
|
},
|
|
@@ -1133,7 +1161,7 @@ async function deployCommand(args) {
|
|
|
1133
1161
|
console.log(`Updated: ${formatOptionalTimestamp(deployed.updatedAt, "unknown")}`);
|
|
1134
1162
|
console.log(`Expires: ${formatOptionalTimestamp(deployed.expiresAt)}`);
|
|
1135
1163
|
if (deployed.claimUrl) {
|
|
1136
|
-
console.log(`Claim: ${
|
|
1164
|
+
console.log(`Claim: ${claimCommandText({ api, capsuleArg })}`);
|
|
1137
1165
|
}
|
|
1138
1166
|
console.log(`Inspect: lakebed inspect ${deployed.deployId}`);
|
|
1139
1167
|
console.log("\nLimits:");
|
|
@@ -1150,7 +1178,7 @@ async function deployCommand(args) {
|
|
|
1150
1178
|
}
|
|
1151
1179
|
if (envelope.claimRequired) {
|
|
1152
1180
|
console.log("\nThis app needs a claimed deploy before server-side fetch or server env can run.");
|
|
1153
|
-
console.log(
|
|
1181
|
+
console.log(`Run ${claimCommandText({ api, capsuleArg })}, then run lakebed deploy again.`);
|
|
1154
1182
|
}
|
|
1155
1183
|
}
|
|
1156
1184
|
|
|
@@ -1203,8 +1231,15 @@ async function claimCommand(args) {
|
|
|
1203
1231
|
return;
|
|
1204
1232
|
}
|
|
1205
1233
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1234
|
+
try {
|
|
1235
|
+
await openUrlInBrowser(claimUrl);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
`Unable to open the claim page in your browser: ${error instanceof Error ? error.message : String(error)}\n\nRun ${claimCommandText({ api, capsuleArg })} --json to read the claim URL.`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
console.log(`Opened claim page for deploy ${result.deployId} in your browser.`);
|
|
1208
1243
|
}
|
|
1209
1244
|
|
|
1210
1245
|
async function anonymousServerCommand(args) {
|
|
@@ -1565,11 +1600,12 @@ async function isInsideGitWorkTree(cwd) {
|
|
|
1565
1600
|
}
|
|
1566
1601
|
|
|
1567
1602
|
function shellQuote(value) {
|
|
1568
|
-
|
|
1569
|
-
|
|
1603
|
+
const text = String(value);
|
|
1604
|
+
if (/^[A-Za-z0-9_./:=,@%+-]+$/.test(text)) {
|
|
1605
|
+
return text;
|
|
1570
1606
|
}
|
|
1571
1607
|
|
|
1572
|
-
return `'${
|
|
1608
|
+
return `'${text.replaceAll("'", "'\\''")}'`;
|
|
1573
1609
|
}
|
|
1574
1610
|
|
|
1575
1611
|
async function promptForCapsuleName() {
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const LAKEBED_VERSION = "0.0.
|
|
1
|
+
export const LAKEBED_VERSION = "0.0.15";
|