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 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] [--ttl 7d] [--api <url>] [--json]
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 a claim URL. Configure GitHub OAuth on the runner, then open that claim URL to attach the anonymous deploy to a developer account:
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 URL. Open that URL, 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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lakebed",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "Agent-native CLI and runtime for building and deploying Lakebed capsules.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -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, requestedTtlSeconds, serverEnv }) {
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 ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
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 : new Date(Date.now() + ttlSeconds * 1000).toISOString(),
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, requestedTtlSeconds, serverEnv }) {
3323
+ async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, serverEnv }) {
3322
3324
  const createdAt = now();
3323
3325
  const normalizedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
3324
- const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
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 ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
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] [--ttl 7d] [--api <url>] [--json]
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 deployRequestBody(envelope, ttl, { serverEnv } = {}) {
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, ttl, { serverEnv: serverEnvForUpdate }),
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, ttl),
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: ${deployed.claimUrl}`);
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("Open the claim URL, then run lakebed deploy again.");
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
- console.log("Open this URL to claim the current project's deploy:");
1207
- console.log(claimUrl);
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
- if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
1569
- return value;
1603
+ const text = String(value);
1604
+ if (/^[A-Za-z0-9_./:=,@%+-]+$/.test(text)) {
1605
+ return text;
1570
1606
  }
1571
1607
 
1572
- return `'${value.replaceAll("'", "'\\''")}'`;
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.14";
1
+ export const LAKEBED_VERSION = "0.0.15";